无服务器架构(Serverless)
2014 年,AWS 推出了 Lambda,这是第一个主流的 Serverless 计算平台。2016 年,阿里云推出函数计算。2018 年,Azure 推出 Azure Functions。Serverless 从此进入快速普及阶段。
但 Serverless 很快暴露出了问题。2018 年,某公司把整个后端迁移到 Lambda,第一个月账单就爆了——Lambda 的计费模式(按请求次数 × 执行时间)和他们的高并发场景完全不匹配,费用是原来服务器的 5 倍。
Serverless 不是「不需要服务器」,而是「不需要关心服务器」。它的按需执行模型有自己的适用场景和不适用场景。
Serverless 的核心理念
Serverless 的核心价值有两个:按需执行和按使用量计费。
按需执行
传统服务器(无论是物理机、虚拟机还是容器)都是「常驻进程」:启动后就一直运行,直到被手动停止或崩溃。无论有没有请求,服务器都在消耗资源。
Serverless 不同:函数只在有请求时启动,处理完请求后自动释放资源。没有请求时,不消耗任何资源。
传统服务器的资源利用:
时间 →
|████████████████████████████████████████████|
请求曲线:▲ ▲▲▲▲ ▲
资源利用:▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
问题:即使没有请求,服务器也在运行(白花钱)
Serverless 的资源利用:
时间 →
| ▲ ▲▲▲▲ ▲ |
请求曲线: 请求 请求
资源利用: ▓▓▓▓ ▓▓▓▓▓▓ ▓▓▓▓
函数启动 执行完毕 函数启动
优势:没有请求时完全不消耗资源
按使用量计费
传统服务器是「包月制」:无论用多少资源,都是固定的月费。Serverless 的计费模型是「用多少付多少」。
# AWS Lambda 计费模式
# 计费单位:请求次数 × 执行时间(GB-秒)
# 免费额度:每月 100 万次请求 + 40 万 GB-秒
# 示例 1:低频场景
月请求量:1000 次
每次执行时间:200ms
每次内存:128MB
费用:$0.00(免费额度内)
# 示例 2:中频场景
月请求量:1000 万次
每次执行时间:100ms
每次内存:256MB
费用:约 $2.00
# 示例 3:高并发场景
每秒请求:1000 QPS
每次执行时间:50ms
每次内存:512MB
费用:约 $1,000/月
# 问题:这种场景下,传统服务器可能更便宜
计费模型的适用性:
IaaS / PaaS / Serverless 的对比
云计算的三种形态代表了不同的「责任分工」:
责任分工对比:
IaaS:你负责一切
┌────────────────────────────────────────────┐
│ 应用 │ 运行时(Node/Python/Java)│ OS │
│ 运行时 │ 中间件 │ 基础设施 │
│ ←─────── 你要管的 ───────→ │
└────────────────────────────────────────────┘
PaaS:你管应用,平台管运行时
┌────────────────────────────────────────────┐
│ 应用 │ 平台管理 │ 云厂商 │
│ ←─────── 你要管的 ───────→ │
└────────────────────────────────────────────┘
Serverless:你只管业务代码
┌────────────────────────────────────────────┐
│ 业务代码 │ 平台全管 │ 云厂商 │
│ ←─ 你要管的 ─→ │
└────────────────────────────────────────────┘
FaaS 的核心机制
FaaS(Function as a Service,函数即服务)是 Serverless 的核心技术形态。一个 FaaS 函数本质上是一个被事件触发的执行单元。
函数执行模型
// AWS Lambda 函数示例
public class ImageProcessor implements RequestHandler<S3Event, String> {
@Override
public String handleRequest(S3Event event, Context context) {
// 1. 获取触发事件(S3 上传了一个图片)
S3ObjectInputStream s3InputStream = event.getRecords().get(0).getS3()
.getObject().getObjectContent();
// 2. 处理图片(缩略图、水印等)
BufferedImage originalImage = ImageIO.read(s3InputStream);
BufferedImage resizedImage = resize(originalImage, 800, 600);
// 3. 上传到另一个 S3 bucket
ByteArrayOutputStream os = new ByteArrayOutputStream();
ImageIO.write(resizedImage, "jpg", os);
s3Client.putObject(bucketOutput, key, os.toString());
return "Image processed: " + key;
}
}
函数的触发源可以是:
函数生命周期
┌─────────────────────────────────────────────────────────────┐
│ 函数生命周期 │
└─────────────────────────────────────────────────────────────┘
请求进入
↓
函数实例启动(冷启动)← 第一次调用或实例被回收后
↓ ↓
│ │ 预热(可选)
↓ ↓
函数代码加载 保持一个预热实例
↓ ↓
执行业务逻辑 处理请求
↓ ↓
返回结果 返回结果
↓ ↓
实例保留(热启动) 保留一段时间
│ ↓
│ (有更多请求) 回收(冷启动)
↓
继续执行
冷启动问题及优化策略
冷启动的定义
冷启动(Cold Start)是指函数实例从「不存在」到「可以处理请求」的过程。这个过程包括:分配资源、启动容器、拉取代码、执行初始化代码。
冷启动 vs 热启动的延迟对比:
冷启动(首次或实例被回收后):
请求 → 启动容器(1~3秒)→ 加载代码(500ms~2s)→ 初始化(100~500ms)→ 执行 → 返回
总延迟:2~5 秒
热启动(实例已存在):
请求 → 直接执行 → 返回
总延迟:< 10ms
影响冷启动时间的因素
优化策略
策略一:选择合适的运行时
# 对比:Node.js vs Java Lambda 冷启动时间
Node.js 函数(简单计算):
冷启动:约 100ms
热启动:< 5ms
Java 函数(Spring Boot):
冷启动:约 3~5 秒
热启动:约 100ms
建议:延迟敏感的场景避免使用 Java
策略二:配置预留并发
# AWS Lambda 预留并发配置
# 预留并发 = 始终保持 N 个预热实例
provisioned_concurrency:
- function_name: image-processor
provisioned_concurrency: 5
# 代价:预留实例按时间计费,即使没有请求也要付费
# 收益:消除冷启动延迟,保证 P99 延迟稳定
策略三:优化代码结构
// 优化前:所有代码在 handler 中初始化
public class BadLambda implements RequestHandler {
public String handleRequest(...) {
// 每次调用都执行初始化
DatabaseClient db = new DatabaseClient();
MLModel model = new MLModel(); // 加载模型耗时 2 秒
return db.query(model.predict(input));
}
}
// 优化后:静态初始化在类加载时执行
public class GoodLambda implements RequestHandler {
private static DatabaseClient db;
private static MLModel model;
static {
// 只在冷启动时执行一次
db = new DatabaseClient();
model = new MLModel(); // 模型加载在启动时完成
}
public String handleRequest(...) {
return db.query(model.predict(input));
}
}
适用场景与不适用场景
适用场景
事件驱动型任务:函数只在事件触发时执行,不需要常驻进程。
# 典型场景 1:图片处理
触发条件:S3 上传图片
处理逻辑:生成缩略图 + 水印 + 存储
特点:任务短、执行频率低、触发不可预测
# 典型场景 2:Webhook 处理
触发条件:第三方 API 回调
处理逻辑:验证签名 + 解析数据 + 存储
特点:请求频率低、但需要快速响应
# 典型场景 3:定时任务
触发条件:Cron 表达式(每天凌晨 2 点)
处理逻辑:生成报表 + 发送邮件
特点:执行频率可预测,但只在特定时间运行
突发流量处理:流量突增时自动扩容,不需要提前准备资源。
# 秒杀场景
传统方式:
- 需要准备 100 台服务器应对峰值
- 平时流量只有 10%,资源浪费
- 扩容需要 30 分钟
Serverless 方式:
- 配置最大并发 1000
- 流量来时自动扩容
- 流量结束时自动缩容
- 按实际使用量付费
开发和测试环境:开发测试环境使用频率低,Serverless 按需执行的成本优势明显。
不适用场景
长时间运行任务:Lambda 的超时限制(15 分钟)不适合长任务。
# 不适合 Lambda 的场景
视频转码(单文件可能需要 1 小时)
大数据批量处理(单任务可能需要数小时)
长连接 WebSocket(需要保持连接)
有状态应用:Serverless 函数是无状态的,有状态逻辑需要额外存储支持。
有状态应用的问题:
# 问题:用户会话状态存在哪里?
# 方案 1:每次请求都查询数据库(增加延迟和成本)
# 方案 2:使用 Redis Session(需要额外的服务支持)
# 方案 3:客户端存储状态(安全隐患)
如果应用需要大量状态管理,Serverless 可能不是最佳选择
需要保持连接的应用:数据库连接池、长连接 WebSocket 等场景。
// 问题场景:数据库连接池
public class DatabaseLambda implements RequestHandler {
// 错误做法:每次请求创建新连接
public String handleRequest(...) {
Connection conn = DriverManager.getConnection(url);
// 每次都建立连接(约 20~50ms)
// Lambda 按时间计费,多付钱
}
// 更好的做法:使用 RDS Proxy(但增加了成本)
// 或者:避免在 Lambda 中使用数据库,改用完全无状态的 API
}
代价分析
厂商锁定
Serverless 函数与云厂商的绑定很深。不同厂商的 API、触发器、配置方式都不同。
厂商锁定的影响:
迁移成本:
- AWS Lambda → 阿里云函数计算:重写所有触发器和配置
- API 格式完全不同
- 测试用例需要重新执行
- 回归测试成本高
依赖锁定:
- 厂商特定 SDK
- 厂商特定配置格式
- 厂商特定监控工具
建议:使用 abstraction layer(如 Serverless Framework)来减少绑定
调试困难
Serverless 函数的执行环境是云端,本地调试能力有限。
调试挑战:
1. 本地模拟不完整
- AWS SAM Local / Serverless Framework offline
- 只能模拟部分触发器
- 无法完全复现云端环境
2. 线上日志不直观
- CloudWatch Logs 分散
- 请求 ID 追踪链路复杂
- 日志关联分析困难
3. 性能调优受限
- 无法 profiling 云端执行
- 只能通过日志猜测
成本不可预测
计费模式复杂,账单可能超出预期。
# 成本陷阱 1:高频小请求
场景:每分钟调用 1000 次 Lambda
单次执行时间:5ms
内存:128MB
月费用:$0.77(看起来不多)
# 但如果用户请求量增长 100 倍:
月费用:$77(可能是预算的 10 倍)
# 成本陷阱 2:内存配置过高
场景:函数只需要 64MB 内存
但配置了 512MB 内存
Lambda 计费 = 执行时间 × 内存配置
费用是实际需要的 8 倍
云厂商 Serverless 方案对比
选择建议:
国内业务:优先阿里云函数计算
- 备案合规要求
- 中文文档和技术支持
- 与阿里云其他服务集成好
海外业务:AWS Lambda
- 全球节点覆盖广
- 生态最完善
- 文档和社区资源丰富
微软技术栈:Azure Functions
- 与 Office 365、Azure AD 集成方便
- .NET 支持最好
总结
Serverless 的核心价值是「让开发者只关心业务逻辑」,它通过按需执行和按使用量计费降低了资源浪费。
但 Serverless 也有明确的适用场景边界:
- 适合:事件驱动型任务、突发流量处理、低频或间歇性工作负载
- 不适合:长时间任务、有状态应用、高频持续请求
什么时候应该考虑 Serverless? 当你的业务有以下特征时:
- 流量有明显的高低峰,低谷期资源利用率低
- 有大量短任务(如图片处理、数据转换)
- 需要快速上线,不需要复杂的部署流程
- 预算有限,无法提前准备大量服务器
下一节,我们来看架构演进的决策框架:如何判断一个系统应该演进到哪个阶段。
思考题
问题 1:某公司有一个图片分享应用,用户上传图片后需要生成缩略图、提取元数据、加水印。目前使用 ECS 服务器,每个月账单 5000 元。用户量稳定,日均上传量约 10 万张。是否应该迁移到 Serverless?
参考答案
需要看具体情况。图片处理是典型的 Serverless 适用场景,但也要考虑成本因素。
成本估算:Lambda 计费 = 请求次数 × 执行时间 × 内存配置。如果每次处理 200ms、256MB,10 万张图片的费用约 $5~10/月,远低于 ECS 的 5000 元。
迁移风险:需要评估 Lambda 的冷启动是否影响用户体验(图片处理通常不需要实时,可以接受 2~3 秒延迟)。同时需要考虑厂商锁定,如果将来可能切换云厂商,需要用 Serverless Framework 等抽象层。
建议:可以渐进式迁移,先把图片处理函数迁移到 Serverless,观察效果和成本。原 ECS 继续运行其他服务。
问题 2:Serverless 函数如何处理需要访问数据库的场景?有什么最佳实践?
参考答案
有几种处理方式:
-
使用数据库连接池代理(如 RDS Proxy):RDS Proxy 复用连接池,避免每次请求都建立新连接。但会增加成本。
-
使用 Serverless 兼容的数据库:如 AWS DynamoDB、阿里云 TableStore,这些数据库设计时就考虑了无服务器场景,不需要管理连接。
-
减少数据库查询:在函数内部缓存热点数据,减少数据库访问次数。
-
异步处理:把需要数据库操作的请求放入消息队列,函数异步处理,减少数据库连接数。
-
批量操作:合并多个请求,一次数据库操作处理多条数据。
问题 3:Serverless 的厂商锁定问题有多严重?如何降低迁移成本?
参考答案
厂商锁定的影响确实存在,但可以通过以下方式降低风险:
-
使用抽象层:Serverless Framework、Terraform 等工具可以统一管理不同云厂商的配置,迁移时只需要修改 provider 配置。
-
避免厂商特有功能:尽量使用标准的触发器和 API,避免使用只有某个厂商有的特性。
-
分层架构:业务逻辑层不依赖云厂商特有 SDK,数据访问层通过抽象接口隔离。
-
保持代码纯净:业务逻辑代码应该是纯函数,可以轻松移植到任何运行时。
迁移成本评估:如果应用规模大,迁移成本可能超过「继续用原厂商」的成本。在决定迁移前,需要评估实际迁移工作量。