Serverless 架构实践笔记
别急着给Serverless 架构下定义,先看这次卡在哪。
这次搞 Serverless 架构,从 AWS Lambda 到腾讯云函数、阿里云函数计算,前后折腾了半年。
为什么选 Serverless
一开始的想法很简单:手头有个 API 服务,流量不大但也不稳定,偶尔有峰值,但大部分时间都在睡大觉。传统服务器要么配大点浪费钱,要么配小点峰值扛不住。Serverless 听起来就很适合这种场景——按调用次数和执行时间付费,不用的时候几乎零成本。
后来发现,这只是表层的理由。真正让我继续用下去的是:
- 不用关心扩容:流量突然涨十倍,云厂商自动处理
- 免去大部分运维:不用管操作系统升级、安全补丁、日志收集这些琐事
- 按需付费:深夜睡觉的时候几乎不花钱
但这些好处的前提是:你的场景确实适合,而且你愿意接受一些取舍。
云函数入门:第一个 AWS Lambda
先从 AWS Lambda 开始,因为它是最成熟的。我用 Python 写了一个简单的 HTTP API:
import json
def lambda_handler(event, context):
# 获取请求参数
path = event.get('path', '/')
method = event.get('httpMethod', 'GET')
body = event.get('body', '{}')
# 解析 body
if isinstance(body, str):
try:
body = json.loads(body)
except:
body = {}
# 简单路由
if path == '/hello' and method == 'GET':
return {
'statusCode': 200,
'headers': {'Content-Type': 'application/json'},
'body': json.dumps({'message': 'Hello from Lambda'})
}
elif path == '/echo' and method == 'POST':
return {
'statusCode': 200,
'headers': {'Content-Type': 'application/json'},
'body': json.dumps({'you_sent': body})
}
else:
return {
'statusCode': 404,
'headers': {'Content-Type': 'application/json'},
'body': json.dumps({'error': 'Not found'})
}
这个函数看起来简单,但部署的时候第一坑就来了:AWS Lambda 默认有超时和内存限制,Python 的依赖包需要打包进 deployment package,而 AWS 默认的 zip 解压大小限制是 50MB(未压缩时 250MB)。如果你的项目用了 pandas、numpy 这些库,很容易就超了。
解决方案有两个:
- 使用 Lambda Layers:把公共依赖放到 Layer 里
- 用容器镜像:用 Docker 打包,限制是 10GB
我一开始用了 Layer,但后来发现 Layer 也有版本管理问题,最后还是转到容器镜像。Deploy 命令简化为:
# 构建 Docker 镜像
docker build -t lambda-demo .
# 登录 ECR
aws ecr get-login-password --region us-east-1 | \
docker login --username AWS --password-stdin <account-id>.dkr.ecr.us-east-1.amazonaws.com
# 推送镜像
docker tag lambda-demo:latest <account-id>.dkr.ecr.us-east-1.amazonaws.com/lambda-demo:latest
docker push <account-id>.dkr.ecr.us-east-1.amazonaws.com/lambda-demo:latest
# 更新 Lambda 函数
aws lambda update-function-code \
--function-name lambda-demo \
--image-uri <account-id>.dkr.ecr.us-east-1.amazonaws.com/lambda-demo:latest
第一个服务跑起来后,我盯着 CloudWatch 的日志看了一下午,心想这玩意儿确实简单——直到冷启动问题砸到脸上。
冷启动:看不见的性能杀手
第一次发现问题是在凌晨三点。监控显示某个 API 的响应时间突然从 50ms 跳到 3 秒,查了半天发现是因为函数已经有一段时间没被调用,触发了一次冷启动。
冷启动的原因很好理解:当一段时间没有请求到来时,云厂商会回收函数实例。下次请求来的时候,需要重新初始化运行时环境、加载代码、分配资源。这个过程可能从几百毫秒到几秒不等,取决于函数大小、语言运行时、是否需要拉取容器镜像等因素。
实测数据(AWS Lambda,Python 3.9,512MB 内存):
| 场景 | 首次冷启动 | 热调用 | 闲置 10 分钟后冷启动 |
|---|---|---|---|
| 简单函数(~1MB) | ~300ms | ~10ms | ~350ms |
| 中等函数(~10MB) | ~800ms | ~20ms | ~900ms |
| 复杂函数(~50MB,容器镜像) | ~1500ms | ~30ms | ~1800ms |
三种函数体量下,冷启动与热调用的延迟差距可以差两个数量级——下图把表格里的实测数据放在一起对比。

函数包越大、容器镜像越重,冷启动惩罚越明显;热实例路径则始终稳定在 10–30ms 量级。
如果服务的 SLA 要求是 P99 < 200ms,那冷启动就成了大问题。
解决思路有几个:
- 保持热度:用定时任务定期 ping 函数
- 预热:在流量高峰前主动触发函数
- 优化代码:减少初始化时的加载,懒加载依赖
- 升级配置:增加内存(AWS Lambda 内存和 CPU 是绑定的)
我最后用了组合拳:把初始化重的逻辑移到函数外部(比如建立数据库连接),增加内存到 1GB,配合一个 CloudWatch Events 定时任务每小时调用一次。P99 从 3 秒降到了 500ms,虽然不如纯热实例快,但勉强能接受。
多云尝试:腾讯云函数和阿里云函数计算
AWS 用了一段时间后,考虑到合规和成本问题,我又试了腾讯云函数和阿里云函数计算。总体感觉是:三家的核心概念差不多,但细节差异很大。
腾讯云函数
腾讯云函数的 Python 入口写法略有不同:
import json
def main_handler(event, context):
# event 结构和 AWS Lambda 不完全一样
request_context = event.get('requestContext', {})
path = event.get('path', '/')
method = event.get('httpMethod', request_context.get('httpMethod', 'GET'))
body = event.get('body', '')
headers = event.get('headers', {})
# 解析 body
if body and headers.get('Content-Type', '').find('application/json') >= 0:
try:
body = json.loads(body)
except:
pass
if path == '/hello':
return {
'statusCode': 200,
'headers': {'Content-Type': 'application/json'},
'body': json.dumps({'message': 'Hello from Tencent Cloud'})
}
else:
return {
'statusCode': 404,
'headers': {'Content-Type': 'application/json'},
'body': json.dumps({'error': 'Not found'})
}
腾讯云的部署工具是 scf cli,安装和配置:
# 安装
npm install -g @cloudbase/cli
# 登录
tcb login
# 初始化项目
tcb init
# 部署
tcb functions:deploy
腾讯云的优势是国内访问快、文档中文友好、价格有竞争力。但缺点是调试工具不如 AWS 完善,冷启动时间略长。
阿里云函数计算
阿里云函数计算用的是 Go 写的,性能据说比 Python 好,但入坑后发现调试起来麻烦多了。一个简单的 HTTP 触发器函数:
package main
import (
"encoding/json"
"fmt"
"net/http"
)
type Response struct {
StatusCode int `json:"statusCode"`
Headers map[string]string `json:"headers"`
Body string `json:"body"`
}
type Event struct {
Path string `json:"path"`
Method string `json:"httpMethod"`
Body string `json:"body"`
Headers map[string]string `json:"headers"`
}
func HandleRequest(w http.ResponseWriter, r *http.Request) {
var event Event
// 简化处理,实际需要解析请求体
event.Path = r.URL.Path
event.Method = r.Method
if event.Path == "/hello" && event.Method == "GET" {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(200)
json.NewEncoder(w).Encode(map[string]string{"message": "Hello from Alibaba Cloud"})
} else {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(404)
json.NewEncoder(w).Encode(map[string]string{"error": "Not found"})
}
}
func main() {
http.HandleFunc("/", HandleRequest)
http.ListenAndServe(":9000", nil)
}
部署用 fun 工具:
# 安装
npm install -g @alicloud/fun
# 初始化
fun init
# 本地调试
fun local start
# 部署
fun deploy
阿里云的强项是国内云生态集成做得好,和 OSS、RDS、MaxCompute 这些服务配合起来很顺。但工具链稳定性一般,时不时遇到部署失败或者命令行卡死。
成本和性能的权衡
实际跑了几个月后,看了一下账单和性能数据。几个关键发现:
成本结构对比
| 项目 | AWS Lambda | 腾讯云函数 | 阿里云函数计算 |
|---|---|---|---|
| 基础费用 | $0.20/百万请求 | ¥1.33/百万请求 | ¥1.33/百万请求 |
| 计算费用 | $0.00001667/GB-秒 | ¥0.0000167/GB-秒 | ¥0.0000167/GB-秒 |
| 流量费用 | $0.09/GB | ¥0.50/GB | ¥0.50/GB |
| 免费额度 | 100万请求/月 | 100万请求/月 | 100万请求/月 |
我的服务月均 50 万次调用,每次平均执行 300ms,内存 512MB:
- AWS Lambda:约 $4.5/月
- 腾讯云函数:约 ¥30/月
- 阿里云函数计算:约 ¥30/月
看起来都不贵,但要注意隐藏成本:
- API 网关费用:三家都需要用各自的网关,这是单独计费的
- 日志存储:CloudWatch、CLS 都要花钱,长期积累不便宜
- 监控告警:高级监控功能通常额外收费
- 数据传输:跨可用区、跨地域传输都可能加钱
性能对比
同一套逻辑在三家平台上跑的压力测试结果(1000 并发,持续 5 分钟):
| 指标 | AWS Lambda | 腾讯云函数 | 阿里云函数计算 |
|---|---|---|---|
| P50 延迟 | 45ms | 60ms | 50ms |
| P99 延迟 | 350ms | 480ms | 420ms |
| P99.9 延迟 | 1200ms | 1800ms | 1500ms |
| 错误率 | 0.01% | 0.03% | 0.02% |
| 冷启动频率 | 12% | 18% | 15% |
AWS 在稳定性上确实更好,尤其是极端情况下的表现。但如果你的用户主要在国内,访问延迟会被网络因素抵消,腾讯云和阿里云的实际体验反而可能更好。
踩过的坑
半年下来,踩过的坑比预期的多,这里挑几个有代表性的。
1. 本地调试和线上不一致
在本地 Docker 容器里跑得好好的,部署上去就报错。查了半天发现是本地用的 Python 3.10,而云函数的运行时是 3.9,某些库的兼容性问题暴露出来了。
教训:本地开发环境和线上运行时必须严格一致。后来写了个 Dockerfile 确保环境统一:
FROM public.ecr.aws/lambda/python:3.9
COPY requirements.txt .
RUN pip install --target "${LAMBDA_TASK_ROOT}" -r requirements.txt
COPY handler.py ${LAMBDA_TASK_ROOT}
CMD [ "handler.lambda_handler" ]
2. 超时设置太激进
一开始为了省钱,把超时设成 3 秒。结果某个第三方 API 偶尔响应慢,导致函数频繁超时,重试反而增加了成本。
教训:预留合理余量。现在超时设成 10 秒,同时监控 P99.5,如果超过 7 秒就告警。
3. 没考虑连接池复用
每次函数执行都新建数据库连接,在高并发下连接数暴增,数据库被打爆了。
教训:连接池要放在函数外部初始化:
import psycopg2
from psycopg2 import pool
# 全局连接池
connection_pool = None
def get_connection():
global connection_pool
if connection_pool is None:
connection_pool = psycopg2.pool.SimpleConnectionPool(
minconn=1,
maxconn=5,
host=db_host,
database=db_name,
user=db_user,
password=db_password
)
return connection_pool.getconn()
def lambda_handler(event, context):
conn = get_connection()
try:
# 使用连接
pass
finally:
connection_pool.putconn(conn)
4. 忘了处理异步任务
有个函数内部调了第三方 API,以为会很快返回,结果对方卡了 10 秒,函数超时但 API 请求还在继续,导致数据重复。
教训:对异步任务要明确处理策略,要么设置合理的超时和重试,要么转到消息队列异步处理。
5. 环境变量管理混乱
开发和生产环境用同一套环境变量,不小心把测试 API key 推到了生产环境。
教训:环境变量要分离,用不同的部署配置:
# config.dev.yaml
environment:
API_KEY: test_key_123
DB_HOST: dev.db.example.com
# config.prod.yaml
environment:
API_KEY: prod_key_456
DB_HOST: prod.db.example.com
适用场景和边界
折腾了一圈后,对 Serverless 的适用场景有了更清晰的认识。
适合的场景
- 不规律流量:波动大、偶发峰值的服务
- 事件驱动任务:文件上传处理、定时任务、消息队列消费者
- 轻量级 API:逻辑简单、执行时间短的服务
- 快速验证想法:不想前期投入太多运维成本的项目
不太适合的场景
- 长运行任务:视频转码、复杂计算等超过 15 分钟的任务
- 高性能需求:需要极低延迟、高性能计算的场景
- 复杂状态管理:需要维护复杂 session 状态的服务
- 合规要求严:数据必须留在特定物理位置的场景
实践建议
如果决定上 Serverless,有几点经验可以参考:
- 从小开始:先找个小服务试试水,别一开始就把核心服务迁过去
- 监控要到位:P99、冷启动频率、错误率这些指标都要盯着
- 成本预估要准:别只看基础费用,把网关、日志、监控都算进去
- 保留回退方案:如果 Serverless 不符合预期,要有回退到传统架构的能力
结语
Serverless 不是万能药,但也不是噱头。它的核心价值是"减少运维心智负担",但前提是你愿意接受一些约束和权衡。
半年前我以为 Serverless 是"不用管服务器"的魔法,现在明白它更像是"把服务器藏起来,但你要懂它的脾气"。冷启动要管、成本要算、调试要花心思,这些麻烦并不会凭空消失,只是换了个形式存在。
但如果你的场景真的适合,这些付出是值得的。现在我的 API 服务不用半夜起来扩容,也不用担心安全补丁,代码推上去就能跑,账单虽然不像零但有数。这种"少操心"的感觉,确实很爽。
技术选择从来不是非黑即白,Serverless 也不例外。把它当工具,而不是信仰,用它适合的场景,剩下的交给传统架构,这才是务实的做法。
最后一句建议:别被概念迷惑,先动手试一个函数,亲自感受一下它的脾气,再做决定。
版权声明: 本文首发于 指尖魔法屋-Serverless 架构实践笔记(https://blog.thinkmoon.cn/post/112-serverless-architecture-theory-practice-guide/) 转载或引用必须申明原指尖魔法屋来源及源地址!
评论
使用 GitHub 账号登录后即可留言,支持 Markdown。