无服务器架构(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/月
# 问题:这种场景下,传统服务器可能更便宜

计费模型的适用性

场景Serverless 优势场景特点
低频请求免费额度内几乎零成本网站活动、限时功能
突发流量自动扩容,无需提前准备秒杀、抢购
高频请求可能比服务器贵日均百万级以上请求
长时间任务有超时限制,成本高批量处理、长连接

IaaS / PaaS / Serverless 的对比

云计算的三种形态代表了不同的「责任分工」:

维度IaaSPaaSServerless
控制权最大(自己管理一切)中等(平台提供运行时)最小(只写业务逻辑)
运维负担最高(OS、运行时、应用全要管)中等(应用和配置)最低(只管代码)
扩缩容手动或半自动自动(有实例概念)完全自动(按请求触发)
计费模式包月/包年实例数 × 时间请求次数 × 执行时间
适用场景需要控制底层长期运行的服务事件驱动、短任务
冷启动无(实例常驻)无或很短有(首次调用需要启动)
责任分工对比:

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;
    }
}

函数的触发源可以是:

触发源示例
对象存储S3 上传文件时触发图片处理
消息队列Kafka/RabbitMQ 消息触发处理逻辑
API 网关HTTP 请求触发函数执行
定时任务Cron 表达式触发定时任务
数据库变更DynamoDB/ Cosmos DB 变更触发
CDN 事件CloudFront 缓存 miss 时触发

函数生命周期

┌─────────────────────────────────────────────────────────────┐
│                    函数生命周期                              │
└─────────────────────────────────────────────────────────────┘

请求进入

函数实例启动(冷启动)← 第一次调用或实例被回收后
    ↓                         ↓
    │                         │ 预热(可选)
    ↓                         ↓
函数代码加载             保持一个预热实例
    ↓                         ↓
执行业务逻辑             处理请求
    ↓                         ↓
返回结果                 返回结果
    ↓                         ↓
实例保留(热启动)         保留一段时间
    │                         ↓
    │ (有更多请求)          回收(冷启动)

继续执行

冷启动问题及优化策略

冷启动的定义

冷启动(Cold Start)是指函数实例从「不存在」到「可以处理请求」的过程。这个过程包括:分配资源、启动容器、拉取代码、执行初始化代码。

冷启动 vs 热启动的延迟对比:

冷启动(首次或实例被回收后):
请求 → 启动容器(1~3秒)→ 加载代码(500ms~2s)→ 初始化(100~500ms)→ 执行 → 返回
总延迟:2~5 秒

热启动(实例已存在):
请求 → 直接执行 → 返回
总延迟:< 10ms

影响冷启动时间的因素

因素影响优化方向
语言运行时Java/Python 冷启动慢(启动时间 1~3 秒);Node.js/Go 快(< 100ms)选择轻量级运行时
代码体积依赖越多、代码越大,加载越慢精简依赖,使用分层打包
内存配置内存越高,CPU 越强,启动越快适当增加内存(如 512MB)
VPC 配置如果函数需要访问 VPC 资源,额外增加 10~20 秒使用 VPC Peering 或非 VPC 函数
并发配置预留并发可以避免冷启动配置 Provisioned Concurrency

优化策略

策略一:选择合适的运行时

# 对比: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
最大执行时间15 分钟10 分钟(可配置)无限制(消耗计划)
最大内存10GB3GB1.5GB(Premium)
免费额度100 万请求/月100 万调用/月100 万请求/月
支持的运行时Node.js, Python, Java, Go, Ruby, .NETNode.js, Python, Java, PHP, Go.NET, JavaScript, Java, Python
Cold Start100ms~3s100ms~2s200ms~5s
生态完善度最完善国内最完善中等
价格(国内)较高适中适中
适合场景全球业务国内业务微软技术栈
选择建议:

国内业务:优先阿里云函数计算
- 备案合规要求
- 中文文档和技术支持
- 与阿里云其他服务集成好

海外业务:AWS Lambda
- 全球节点覆盖广
- 生态最完善
- 文档和社区资源丰富

微软技术栈:Azure Functions
- 与 Office 365、Azure AD 集成方便
- .NET 支持最好

总结

Serverless 的核心价值是「让开发者只关心业务逻辑」,它通过按需执行和按使用量计费降低了资源浪费。

但 Serverless 也有明确的适用场景边界:

  • 适合:事件驱动型任务、突发流量处理、低频或间歇性工作负载
  • 不适合:长时间任务、有状态应用、高频持续请求

什么时候应该考虑 Serverless? 当你的业务有以下特征时:

  1. 流量有明显的高低峰,低谷期资源利用率低
  2. 有大量短任务(如图片处理、数据转换)
  3. 需要快速上线,不需要复杂的部署流程
  4. 预算有限,无法提前准备大量服务器

下一节,我们来看架构演进的决策框架:如何判断一个系统应该演进到哪个阶段。

思考题

问题 1:某公司有一个图片分享应用,用户上传图片后需要生成缩略图、提取元数据、加水印。目前使用 ECS 服务器,每个月账单 5000 元。用户量稳定,日均上传量约 10 万张。是否应该迁移到 Serverless?

参考答案

需要看具体情况。图片处理是典型的 Serverless 适用场景,但也要考虑成本因素。

成本估算:Lambda 计费 = 请求次数 × 执行时间 × 内存配置。如果每次处理 200ms、256MB,10 万张图片的费用约 $5~10/月,远低于 ECS 的 5000 元。

迁移风险:需要评估 Lambda 的冷启动是否影响用户体验(图片处理通常不需要实时,可以接受 2~3 秒延迟)。同时需要考虑厂商锁定,如果将来可能切换云厂商,需要用 Serverless Framework 等抽象层。

建议:可以渐进式迁移,先把图片处理函数迁移到 Serverless,观察效果和成本。原 ECS 继续运行其他服务。

问题 2:Serverless 函数如何处理需要访问数据库的场景?有什么最佳实践?

参考答案

有几种处理方式:

  1. 使用数据库连接池代理(如 RDS Proxy):RDS Proxy 复用连接池,避免每次请求都建立新连接。但会增加成本。

  2. 使用 Serverless 兼容的数据库:如 AWS DynamoDB、阿里云 TableStore,这些数据库设计时就考虑了无服务器场景,不需要管理连接。

  3. 减少数据库查询:在函数内部缓存热点数据,减少数据库访问次数。

  4. 异步处理:把需要数据库操作的请求放入消息队列,函数异步处理,减少数据库连接数。

  5. 批量操作:合并多个请求,一次数据库操作处理多条数据。

问题 3:Serverless 的厂商锁定问题有多严重?如何降低迁移成本?

参考答案

厂商锁定的影响确实存在,但可以通过以下方式降低风险:

  1. 使用抽象层:Serverless Framework、Terraform 等工具可以统一管理不同云厂商的配置,迁移时只需要修改 provider 配置。

  2. 避免厂商特有功能:尽量使用标准的触发器和 API,避免使用只有某个厂商有的特性。

  3. 分层架构:业务逻辑层不依赖云厂商特有 SDK,数据访问层通过抽象接口隔离。

  4. 保持代码纯净:业务逻辑代码应该是纯函数,可以轻松移植到任何运行时。

迁移成本评估:如果应用规模大,迁移成本可能超过「继续用原厂商」的成本。在决定迁移前,需要评估实际迁移工作量。