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 声明:

java 复制代码
# 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 里的逻辑,抽成纯函数入口。下面是真实的对账任务改造:

java 复制代码
// 依赖: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;   // 抛出异常 → 平台标记执行失败 → 触发重试/告警
        }
    }
}

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

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

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

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

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

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

java 复制代码
# 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 便宜------预留实例只在配置的规格上收费,且能配合定时策略"只在业务时段预留":

java 复制代码
# 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 规格、杭州地域为参考(单价以官网实时为准),对比跑同一批定时任务:

三个结论直接抄作业:

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

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

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


六、建议

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

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

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


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

七、下篇预告

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


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

相关推荐
池以遇1 小时前
云原生——k8s的pod管理及优化
云原生·容器·kubernetes
QQ_21696290961 小时前
【项目编号:project86564】SpringBoot供应链管理系统:采购、供应商、库存、销售、统计报表一体化实战
java·spring boot·后端
m0_587383001 小时前
课程培训系统开发实战:从技术选型到核心模块设计
java·spring boot·架构·系统架构
suaizai_7 小时前
Fastjson高危漏洞紧急修复指南
spring boot
gugucoding14 小时前
59. 【Java】Spring Boot 入门:第一个 Web 应用
java·开发语言·spring boot
lxw202302711614 小时前
k8s的控制器
云原生·容器·kubernetes
FoldWinCard16 小时前
K8s -- 控制器管理
云原生·容器·kubernetes
王琦031817 小时前
haproxy
运维·云原生
容器魔方17 小时前
云容器引擎 CCE 2026-Q2 优化升级:AI 推理负载、备份中心、Gateway API能力全新上线!
人工智能·云原生·容器·开源