跳至主要內容

程序员小富大约 11 分钟

大家好,我是小富。

《十万个why》系列持续更新中

上个月我们团队把一个定时对账服务从 ECS 迁移到了阿里云函数计算 FC,迁之前 ECS 月费用是 1200 块,2C4G 的规格跑个定时任务绰绰有余。迁完之后第一个月账单拉出来,3800 块。

三倍。

老板看了账单直接在群里 @ 我:不是说 Serverless 按量付费,用多少花多少吗?怎么花的比以前还多?

我当时也懵了。仔细翻了几天账单明细才搞明白,Serverless 的计费模型远没有宣传的那么简单,"按调用次数付费"只是冰山一角,水面下还藏着好几层你没注意到的费用。

先搞清楚 Serverless 到底怎么计费

很多人对 Serverless 计费的理解停留在"调用一次收一次钱",这个说法不能说错,但非常片面。

以阿里云函数计算 FC 和 AWS Lambda 为例,计费公式其实是这样的:

总费用 = 调用次数费 + 执行时长费 + 资源规格费 + 流量费 + 关联服务费

拆开来看:

  • 调用次数费:每 100 万次请求收一笔钱,AWS Lambda 是 0.20 美元/百万次,FC 大概是 1.33 元/百万次。这部分确实便宜。
  • 执行时长费:按函数实际执行的毫秒数计费,注意是从冷启动开始算,不是从你的业务代码第一行开始算。
  • 资源规格费:你给函数配了多少内存,就按多少内存的单价乘以执行时长。配 512MB 和配 3GB,单价差 6 倍。
  • 流量费:函数的出网流量按量计费,和 ECS 一样。
  • 关联服务费:API 网关、日志服务 SLS、对象存储 OSS、消息队列触发器……这些都是独立计费的。

坑就坑在,大多数人只盯着调用次数费,忽略了后面四项。 而后面四项加起来,往往才是账单的大头。

冷启动:你以为只跑了 200ms,实际被收了 2s 的钱

Serverless 函数最臭名昭著的问题就是冷启动。当一个函数实例不存在或者被回收了,新请求进来时平台需要:

T0: 收到请求
T1: 分配计算资源(容器/微虚拟机)
T2: 拉取函数代码包 / 容器镜像
T3: 初始化运行时环境(JVM 启动、Node.js 加载依赖)
T4: 执行 init handler(数据库连接池初始化、配置加载)
T5: 开始执行你的业务逻辑
T6: 业务逻辑执行完毕,返回响应

从 T0 到 T6 的全部时间都会被计入执行时长。 你的业务代码可能只跑了 T5 到 T6 这 200ms,但 T0 到 T4 的冷启动耗时可能有 1.5 秒甚至更久。

不同语言的冷启动时间差异巨大。Python、Node.js 通常在 100ms ~ 500ms 之间,Go 也差不多。但 Java 就惨了,JVM 启动加上 Spring Boot 初始化,冷启动动不动 3 秒起步,我见过最夸张的一个 Spring Cloud Function 冷启动花了 8 秒。

来看个实际的计费对比:

场景:一个 Java 函数,配置 1GB 内存,每天被调用 5000 次

--- 假设全是热启动 ---
业务执行时间:200ms
每次费用:1GB × 0.2s × 0.00001667元/GB·s = 0.000003334 元
日费用:5000 × 0.000003334 = 0.017 元

--- 假设 30% 冷启动 ---
热启动 3500 次:200ms → 日费用 0.012 元
冷启动 1500 次:3200ms → 每次 0.0000533 元 → 日费用 0.08 元
总日费用:0.092 元

冷启动的 1500 次请求,费用是热启动 3500 次的 6.7 倍。

0.092 元看着不多对吧?但这只是一个函数。如果你有 20 个微服务函数,每个都有不同程度的冷启动,一个月下来,光冷启动带来的额外费用就能有好几百。

高频调用下的固定开销累积

ECS 是包年包月或者按小时计费,不管你的进程一秒处理 1 个请求还是 1000 个请求,费用都一样。进程常驻内存,TCP 连接池复用,数据库连接池复用,一切都是热的。

但 Serverless 每次调用都有固定开销。即使在热启动的情况下,每次请求也要经历:

请求进入 → API 网关解析 → 路由到函数实例 → 函数执行 → 响应返回 → 计费打点

这个"路由到函数实例"的过程,平台内部要做负载均衡、实例选择、上下文注入,这些虽然不会直接体现在你的函数执行时长里,但会被算进 API 网关的请求费用中。

我算一笔账你就明白了:

场景:一个 HTTP API,QPS 平均 50,峰值 200

月请求总量:50 × 3600 × 24 × 30 = 1.296 亿次

API 网关费用(以阿里云为例):
  调用费:1.296 亿 / 100 万 × 3.6 元 = 466.56 元
  出流量(假设每个响应 2KB):1.296 亿 × 2KB ≈ 247GB × 0.8 元/GB = 197.6 元

函数计算费用:
  调用次数费:1.296 亿 / 100 万 × 1.33 元 = 172.37 元
  执行时长费(512MB,平均 100ms):
    1.296 亿 × 0.5GB × 0.1s × 0.00001667 = 1080.4 元

月总计:466.56 + 197.6 + 172.37 + 1080.4 ≈ 1917 元

同样的 QPS 50 的服务,一台 2C4G 的 ECS 包年大概 800 ~ 1000 元/月就能搞定。Serverless 贵了将近一倍。

这里的关键在于:当你的服务有持续稳定的流量时,Serverless 的按量计费反而比常驻进程的固定计费更贵。 按量计费的优势只在流量波动大、大量时间段无流量的场景下才能体现出来。

关联服务费:看不见的吸血鬼

这个是最容易被忽略的部分。一个 Serverless 函数几乎不可能独立运行,它总要和别的云服务打交道。

我把那个对账服务的账单拆开看了,费用结构大概是这样的:

  • 函数计算本身:680 元
  • API 网关:420 元
  • 日志服务 SLS(函数执行日志自动采集):310 元
  • 对象存储 OSS(函数读取对账文件):180 元
  • VPC NAT 网关(函数访问 RDS 数据库要走 VPC):520 元
  • 其他杂项:若干

加起来 2110 元,再加上那些零零碎碎的,总共 3800 元。

函数计算本身只占总账单的 18%。 剩下 82% 都是关联服务的费用。

其中 VPC NAT 网关这一项最坑。Serverless 函数默认运行在平台托管的网络环境中,如果你要访问 VPC 内的 RDS、Redis 等资源,就需要给函数配置 VPC 访问能力。很多云厂商的实现方式是通过 NAT 网关或者弹性网卡 ENI 打通网络,这个是额外收费的。

在 ECS 上,你的服务和 RDS 在同一个 VPC 里,内网直连,不要钱。但 Serverless 函数要访问同一个 RDS,中间可能就多了一层 NAT 网关的流量费。

内存配置的隐形陷阱

Serverless 平台的计费单位通常是 GB-秒(GB·s),也就是内存大小乘以执行秒数。

问题来了:你怎么知道该给函数配多少内存?

配少了,函数可能因为内存不足导致频繁 GC 甚至 OOM,执行时间变长,反而更贵。配多了,多出来的内存你没用上但照样收费。

更隐蔽的是,很多平台会把 CPU 算力和内存绑定。AWS Lambda 是这样的,你配 128MB 内存只能拿到 0.08 个 vCPU,配 1769MB 才能拿到一个完整的 vCPU。

128MB  → 0.08 vCPU  → CPU 密集型任务慢到怀疑人生
512MB  → 0.3 vCPU   → 勉强能跑
1769MB → 1 vCPU     → 正常速度
3008MB → 1.7 vCPU   → 比较快

假设一个 CPU 密集型的对账逻辑,在 512MB 配置下跑了 5 秒,在 1769MB 配置下只需要 0.8 秒:

512MB × 5s = 2.56 GB·s → 费用 0.0000427 元
1769MB × 0.8s = 1.415 GB·s → 费用 0.0000236 元

配更多内存反而更便宜,因为 CPU 上来了执行时间大幅缩短。 但大多数人的直觉是"省内存省钱",结果适得其反。

这种"配置 - 性能 - 费用"三者之间的非线性关系,没有实际压测你根本找不到最优解。

并发控制带来的额外实例

ECS 上一个 Tomcat 进程可以同时处理 200 个并发请求(200 个线程),这 200 个请求共享同一份内存、同一个连接池。

但 Serverless 函数的并发模型很不一样。以 AWS Lambda 为例,默认一个函数实例同一时间只处理一个请求。如果你有 200 个并发请求,平台就需要启动 200 个函数实例。

ECS 模式:
  1 个进程 × 200 并发 = 1 份内存开销

Lambda 模式(单实例单并发):
  200 个实例 × 1 并发 = 200 份内存开销

  +----+  +----+  +----+       +----+
  |实例1|  |实例2|  |实例3| ...  |实例200|
  | req |  | req |  | req |     | req  |
  +----+  +----+  +----+       +----+
    ↑        ↑        ↑            ↑
    各自独立的运行时、内存、连接

200 个实例意味着 200 次冷启动的可能性、200 份内存计费、200 个数据库连接(还容易打爆连接池)。

虽然现在有些平台支持了单实例多并发(比如阿里云 FC 可以配置单实例最多处理 10 个并发请求),但即便如此,200 QPS 仍然需要 20 个实例同时运行,每个实例都在独立计费。

怎么解决?搞清楚你的场景再决定用不用

方案一:识别适合 Serverless 的场景

不是所有服务都适合上 Serverless。简单说有个判断标准:

适合 Serverless 的场景:
  - 流量有明显的波峰波谷,大量时间 QPS 接近 0
  - 定时任务、事件驱动型任务(文件上传触发、消息触发)
  - 开发测试环境、临时性任务
  - 日均调用量在几十万次以下

不适合 Serverless 的场景:
  - 持续稳定的高流量(QPS > 10)
  - 对延迟敏感的在线服务(冷启动不可接受)
  - CPU 密集型长时间运行的任务
  - 需要频繁访问 VPC 内部资源的服务

如果你的服务 7x24 小时都有稳定流量,老老实实用 ECS 或者 K8s 反而更省钱。Serverless 省钱的前提是你有大量的"空闲时间"可以不付费。

方案二:优化冷启动

如果确实要用 Serverless,冷启动必须优化。几个实操手段:

Java 项目用 GraalVM Native Image 编译成原生可执行文件,冷启动从 3 秒降到 100ms 以内。或者直接换成 Go、Rust 这种编译型语言写函数,天然冷启动快。

// 传统 Spring Boot Lambda:冷启动 3~8 秒
@SpringBootApplication
public class OrderFunction {
    // 每次冷启动都要走完整的 Spring 容器初始化
}

// 用 AWS Lambda SnapStart 或 CRaC:冷启动降到 200ms 以内
// JVM 在部署时做一次完整初始化,把内存快照存下来
// 冷启动时直接从快照恢复,跳过初始化阶段

另外可以用预留实例(Provisioned Concurrency)保持一定数量的热实例常驻,避免冷启动。但注意,预留实例是要收常驻费用的,这时候你需要算一下预留实例的费用和 ECS 哪个更划算。

方案三:精细化成本监控

上 Serverless 之前就要建立完整的费用监控。把每个函数的调用次数、执行时长、内存使用率都监控起来,再加上关联服务的费用汇总。

# 函数级别的费用告警配置示例(阿里云云监控)
alarm:
  - metric: fc_function_invocations
    threshold: 1000000   # 日调用超过 100 万次告警
    period: 86400
  - metric: fc_function_duration_avg
    threshold: 3000      # 平均执行时长超过 3 秒告警
    period: 3600
  - metric: fc_function_memory_usage_percent
    threshold: 30        # 内存使用率低于 30% 说明配多了
    comparator: lessThan
    period: 86400

特别是内存使用率,如果一个函数配了 1GB 内存但实际只用了 200MB,你在白白多付 80% 的钱。定期做一次函数的 right-sizing,把内存配置调到实际使用量的 1.3 倍左右。

方案四:混合架构

最实际的做法是混合架构。核心链路、高频服务跑在 ECS 或 K8s 上,低频的定时任务、事件驱动的处理逻辑放到 Serverless 上。

我后来就是这么处理的——那个对账服务的核心对账逻辑搬回了 ECS,只把"对账完成后生成报表发邮件"这种低频触发的后置任务留在函数计算上。月费用从 3800 降回到了 1400,其中 ECS 还是 1200,函数计算加关联服务只有 200 块。

说在最后

回头看这个问题,Serverless 贵不贵其实取决于你怎么用。它的计费模型本身没有问题——按量付费确实是按量付费。但"量"不只是调用次数,还包括执行时长、内存配置、冷启动开销、关联服务的用量,这些加在一起才是你的真实成本。

很多人被"按调用付费"的宣传吸引,以为自己能省钱,没有认真算过总账就一股脑把服务搬上去,结果反而花更多。冷启动膨胀了执行时长,高频调用累积了固定开销,关联服务的费用又在暗处悄悄叠加,三重因素一叠就超了预算。

Serverless 的账单本质不是"用了多少付多少",而是"用了多少、怎么用的、用了什么周边服务"这三者的乘积。看懂了乘积结构,才能真正按需付费。


我是小富,下期见。

上次编辑于: