欢迎光临
我们一直在努力

Day60-Serverless:函数计算改写传统Spring Boot

一、先泼冷水:不是所有 Spring Boot 都适合搬上 Serverless

网上很多 Serverless 教程喜欢画大饼:"零运维、按需付费、弹性无限"。我干这行 20 年,第一个反应是:这饼 Java 工程师得掰开看。

函数计算(FC)的本质是:你只交代码,实例的创建、调度、扩缩容、高可用全由平台负责,按实际执行时间和规格计费。这对短平快的任务型负载(图片处理、消息消费、定时报表、Webhook)是降维打击;但对一个跑了三年、两百个接口、连着 MySQL/Redis/MQ 的单体 Spring Boot 来说,整体搬迁约等于重写。

所以先看这张判断表:

结论:把"尾巴"搬到 Serverless,把"身体"留在 K8s。 尾巴 = 定时/事件/低频接口,这是成本最优化路径。


二、FC 核心概念:一个请求进来发生了什么

函数计算的编程模型非常简单,就三要素:函数代码 + 触发器 + 配置(规格/超时/环境变量)。

对 Java 工程师来说,这张图里唯一的"坑点"就是 冷启动(Cold Start):JVM 启动 + Spring 上下文加载,动辄 5~15 秒,是 Python/Node 函数的几十倍。这是 Java 在 Serverless 领域长期被诟病的根因,也是本文第四节的主角。

先看最常用的两种触发器配置(HTTP 触发器 + 定时触发器),用 Serverless Devs 的 s.yaml 声明:

# s.yaml —— 阿里云 Serverless Devs 2.x 部署描述文件
edition: 3.0.0
name: order-report-app
access: default

resources:
order-report:
  component: fc3           # 函数计算 3.0
  props:
    region: cn-hangzhou
    functionName: order-report
    runtime: java17
    handler: com.laoliang.ReportHandler::handleRequest
    memorySize: 1024        # 实例规格,按需选,直接影响计费
    timeout: 300            # 执行超时(秒),报表任务给足
    code:
      src: ./target         # 部署物:jar 包 + .fcignore
    triggers:
      – triggerName: daily-report
        triggerType: timer  # 定时触发器
        triggerConfig:
          cronExpression: '0 0 2 * * *'   # 每天凌晨 2 点(6 位,含秒)
          enable: true
          payload: '{"type":"daily"}'    # 触发时传入的事件体
      – triggerName: api-gw
        triggerType: http     # HTTP 触发器
        triggerConfig:
          authType: anonymous # 生产建议 function 内校验 JWT
          methods: [GET, POST]

部署就一条命令:s deploy –use-local。之后每天凌晨 2 点,平台自动拉起实例执行你的函数——你不再需要一台 7×24 挂着的 ECS,也不再需要为"定时任务挂了谁来重启"操心。


三、改造实战:Spring Boot 函数化的两条路

1、路线 A:事件函数改写(推荐,改造量小收益大)

把原来写在 @Scheduled 或 Controller 里的逻辑,抽成纯函数入口。下面是真实的对账任务改造:

// 依赖:com.aliyun.fc:fc-runtime-core:2.1.0(Maven 中央仓库)
// 运行时:java17,handler 格式:类全名::方法名
package com.laoliang;

import com.aliyun.fc.runtime.Context;
import com.aliyun.fc.runtime.PojoRequestHandler;

/**
* 每日对账函数:原 XXL-JOB 任务改造而来
* 事件体:{"type":"daily"} —— 由定时触发器 payload 传入
*/
public class ReportHandler implements PojoRequestHandler<ReportEvent, String> {

   // 静态持有 Spring 上下文:一次冷启动只初始化一份,后续复用
   private static final ReportService SERVICE;

   static {
       // 用最小化的 Spring 上下文,只注册需要的 Bean
       // 注意:不要 @SpringBootApplication 全量扫描!
       AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
       ctx.register(DataSourceConfig.class, ReportService.class);
       ctx.refresh();
       SERVICE = ctx.getBean(ReportService.class);
  }

   @Override
   public String handleRequest(ReportEvent event, Context ctx) {
       long start = System.currentTimeMillis();
       String requestId = ctx.getRequestId(); // 平台请求ID,打日志必备

       try {
           ReportResult r = SERVICE.generate(event.getType());
           ctx.getLogger().log(String.format(
               "[%s] report done, rows=%d, cost=%dms",
               requestId, r.getRows(), System.currentTimeMillis() – start));
           return "OK: " + r.getRows();
      } catch (Exception e) {
           ctx.getLogger().log("[" + requestId + "] FAILED: " + e.getMessage());
           throw e;   // 抛出异常 → 平台标记执行失败 → 触发重试/告警
      }
  }
}

三个改造要点,都是踩过坑才懂的:

  • 别用 @SpringBootApplication 全量启动。冷启动时长 ≈ JVM 启动 + Spring 扫描的 Bean 数。两百个 Bean 的单体上下文要 8 秒,只注册 5 个 Bean 的最小上下文只要 1.5 秒。用 AnnotationConfigApplicationContext 精确控制。

  • 静态初始化 + 复用:FC 实例在一次冷启动后会被保温复用(处理后续多个请求),把重资源(DataSource、Spring 上下文)放 static 块或 initializer 中,只在冷启动时付出一次成本。

  • Context.getLogger() 而不是 System.out:前者的日志自动带上 RequestId 关联到日志服务,排障时能按请求串起全链路。

  • 2、路线 B:Custom Runtime 直接跑 Spring Boot jar(几乎零改造)

    如果接口多、不想改代码,用自定义运行时(Custom Runtime):平台给你一个容器,你自己起 HTTP 服务,监听 9000 端口即可。

    # s.yaml —— Custom Runtime 部署 Spring Boot
    resources:
    legacy-app:
      component: fc3
      props:
        region: cn-hangzhou
        functionName: legacy-order-api
        runtime: custom          # 自定义运行时
        caPort: 9000             # 平板会把请求转发到这个端口
        memorySize: 2048
        timeout: 60
        customRuntimeConfig:
          command:
            – java
            – -jar
            – order-api.jar
            – –server.port=9000
            – –spring.profiles.active=fc
        code:
          src: ./target          # 目录里放 order-api.jar + bootstrap 文件
        triggers:
          – triggerName: http-trigger
            triggerType: http
            triggerConfig:
              authType: anonymous
              methods: [GET, POST]

    代码目录只需两样东西:order-api.jar 和一个名为 bootstrap 的可执行脚本(内容就是启动命令)。Controller、Service、MyBatis 一行不用改,代价是 JVM + 完整 Spring 上下文的冷启动(约 8~15 秒)全盘继承——所以路线 B 必须配合下一节的冷启动治理。

    两条路线的选型我总结成一句话:新写任务/小接口选 A,存量系统快速上云选 B,B 上线后按接口流量逐步往 A 迁。


    四、冷启动治理:Java 函数的生死线

    冷启动是 Serverless Java 的头号敌人,但它不是一个开关能解决的,而是一套分层组合拳:

    逐层展开说人话:

    第 1 层:减负。冷启动时间的 70% 花在加载类和初始化 Bean 上。用 java -verbose:class 跑一次看看加载了多少类,把没用的 starter 踢出 pom,是性价比最高的优化。

    第 2 层:提速。FC 支持 Java17 运行时,配合 AppCDS(类数据共享)能把 JVM 类加载时间砍掉一半以上;激进一点可以直接用 GraalVM 原生镜像(第 7 天讲过),启动从秒级进到 50ms 级,代价是构建复杂度上升。

    第 3 层:预热钩子。FC 提供 initializer 入口,在 handler 首次执行之前由平台调用,专门用来做重资源初始化——注意它只保证"先于第一个请求执行",不缩短冷启动总时长,但把 DB 连接池预热等成本从用户请求中挪走,用户感知的首次延迟会明显下降。

    第 4 层:预留实例(治本)。对延迟敏感的场景,直接配置最小实例数常驻,冷启动物理消失。这是用钱换体验,但比 ECS 便宜——预留实例只在配置的规格上收费,且能配合定时策略"只在业务时段预留":

    # s.yaml 中的弹性配置:业务时段保 2 个热实例,其余时间归零
        provision:                 # 预留实例配置
          target: 2
          scheduledActions:
            – name: business-hours
              startTime: '2026-08-17T08:00:00'   # 每天早8点前
              endTime:   '2026-08-17T23:00:00'   # 到晚11点
              target: 2            # 时段内常驻 2 实例
            – name: midnight-zero
              startTime: '2026-08-17T23:05:00'
              endTime:   '2026-08-18T07:55:00'
              target: 0            # 夜间归零,按量兜底

    第 5 层:定时心跳(土办法但管用)。实例执行完会保温几分钟(平台策略),低频服务可以用 cron 每 4 分钟 GET 一次健康接口,让实例永不冷却。费用约为常驻 ECS 的 1/10,适合预算紧张又怕冷启动的场景。

    我接手过一个典型案例:客户的发票导出接口走 Custom Runtime,冷启动 12 秒,用户投诉"点了没反应"。最后组合拳是:踢掉 6 个无用 starter(-3s)+ 最小化 Spring 上下文(-4s)+ initializer 预热连接池(用户侧 -1.5s)+ 业务时段预留 1 实例兜底。上线后 P99 稳定在 800ms。


    五、成本账:FC 按量付费 vs ECS 包年包月

    Serverless 最诱人的宣传是省钱,但省钱只发生在低频负载上,得用数据说话。

    FC 计费公式:费用 ≈ 调用次数费 + (内存GB × 执行时长秒 × GB·秒单价)。以 1GB 规格、杭州地域为参考(单价以官网实时为准),对比跑同一批定时任务:

     

    三个结论直接抄作业:

  • 低频任务迁移 = 成本砍 99%:ECS 上那些"每天跑十分钟"的定时任务,是 FC 的最佳猎物。

  • 中度流量有甜点区:日均几万次调用、平均耗时几百毫秒的内部服务,FC 通常比 ECS 便宜且免运维。

  • 高 QPS 长稳负载别硬上:常驻预留实例的单价溢价摆在那,这种负载 K8s + HPA(上一篇)才是正解。


  • 六、建议

  • 从定时任务开刀,别从核心服务开刀。找一台"专门跑脚本"的老 ECS,把上面的 XXL-JOB 小任务逐个搬到 FC 定时触发器,一台机器当月下线。改造量小、风险低、省的钱看得见,是团队建立 Serverless 信心的最佳第一步。

  • 冷启动先做减法,再花钱买预留。90% 的冷启动问题靠"精简依赖 + 最小化 Spring 上下文 + initializer"就能压到 2 秒内;预留实例是最后的手段,且必须配合"业务时段预留、夜间归零"的定时策略,否则成本反向爆炸。

  • 函数里禁止本地状态。实例随时销毁、可能多实例并发,本地文件缓存、内存 Map 计数器、static 可变状态都是定时炸弹。状态一律外置到 Redis/OSS/RDS——这不是 FC 的特殊要求,而是所有弹性架构的通用纪律。


  • Serverless 不是把服务器藏起来了,而是把"是否值得为一台服务器付费"这个问题,摆到了每个接口面前。

    七、下篇预告

    Day 61:《Docker部署AI推理服务:Ollama + Open WebUI生产实践》——自建大模型推理服务的完整路线:Ollama 模型管理与 API 调用、GPU 加速配置、vLLM 高并发推理部署与压测数据,私有化部署大模型的成本账一并算清。


    专栏:《Java高级进阶之路》 · 数据军师·老梁 码字不易,非授权禁止转载。

    赞(0)
    未经允许不得转载:171主机测评 » Day60-Serverless:函数计算改写传统Spring Boot
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址