Serverless 架构实践笔记

别急着给Serverless 架构下定义,先看这次卡在哪。

这次搞 Serverless 架构,从 AWS Lambda 到腾讯云函数、阿里云函数计算,前后折腾了半年。

为什么选 Serverless

一开始的想法很简单:手头有个 API 服务,流量不大但也不稳定,偶尔有峰值,但大部分时间都在睡大觉。传统服务器要么配大点浪费钱,要么配小点峰值扛不住。Serverless 听起来就很适合这种场景——按调用次数和执行时间付费,不用的时候几乎零成本。

后来发现,这只是表层的理由。真正让我继续用下去的是:

  1. 不用关心扩容:流量突然涨十倍,云厂商自动处理
  2. 免去大部分运维:不用管操作系统升级、安全补丁、日志收集这些琐事
  3. 按需付费:深夜睡觉的时候几乎不花钱

但这些好处的前提是:你的场景确实适合,而且你愿意接受一些取舍。

云函数入门:第一个 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 这些库,很容易就超了。

解决方案有两个:

  1. 使用 Lambda Layers:把公共依赖放到 Layer 里
  2. 用容器镜像:用 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 秒,查了半天发现是因为函数已经有一段时间没被调用,触发了一次冷启动。

冷启动的原因很好理解:当一段时间没有请求到来时,云厂商会回收函数实例。下次请求来的时候,需要重新初始化运行时环境、加载代码、分配资源。这个过程可能从几百毫秒到几秒不等,取决于函数大小、语言运行时、是否需要拉取容器镜像等因素。

graph LR A[请求到达] --> B{有热实例?} B -->|是| C[直接执行] B -->|否| D[冷启动流程] D --> D1[分配资源] D1 --> D2[加载运行时] D2 --> D3[初始化代码] D3 --> D4[执行函数] C --> E[返回结果] D4 --> E

实测数据(AWS Lambda,Python 3.9,512MB 内存):

场景首次冷启动热调用闲置 10 分钟后冷启动
简单函数(~1MB)~300ms~10ms~350ms
中等函数(~10MB)~800ms~20ms~900ms
复杂函数(~50MB,容器镜像)~1500ms~30ms~1800ms

三种函数体量下,冷启动与热调用的延迟差距可以差两个数量级——下图把表格里的实测数据放在一起对比。

AWS Lambda 冷启动对比:简单/中等/复杂函数在首次冷启动、热调用与闲置 10 分钟后冷启动的延迟(ms,512MB 内存)

函数包越大、容器镜像越重,冷启动惩罚越明显;热实例路径则始终稳定在 10–30ms 量级。

如果服务的 SLA 要求是 P99 < 200ms,那冷启动就成了大问题。

解决思路有几个:

  1. 保持热度:用定时任务定期 ping 函数
  2. 预热:在流量高峰前主动触发函数
  3. 优化代码:减少初始化时的加载,懒加载依赖
  4. 升级配置:增加内存(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/月

看起来都不贵,但要注意隐藏成本:

  1. API 网关费用:三家都需要用各自的网关,这是单独计费的
  2. 日志存储:CloudWatch、CLS 都要花钱,长期积累不便宜
  3. 监控告警:高级监控功能通常额外收费
  4. 数据传输:跨可用区、跨地域传输都可能加钱

性能对比

同一套逻辑在三家平台上跑的压力测试结果(1000 并发,持续 5 分钟):

指标AWS Lambda腾讯云函数阿里云函数计算
P50 延迟45ms60ms50ms
P99 延迟350ms480ms420ms
P99.9 延迟1200ms1800ms1500ms
错误率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 的适用场景有了更清晰的认识。

适合的场景

  1. 不规律流量:波动大、偶发峰值的服务
  2. 事件驱动任务:文件上传处理、定时任务、消息队列消费者
  3. 轻量级 API:逻辑简单、执行时间短的服务
  4. 快速验证想法:不想前期投入太多运维成本的项目

不太适合的场景

  1. 长运行任务:视频转码、复杂计算等超过 15 分钟的任务
  2. 高性能需求:需要极低延迟、高性能计算的场景
  3. 复杂状态管理:需要维护复杂 session 状态的服务
  4. 合规要求严:数据必须留在特定物理位置的场景

实践建议

如果决定上 Serverless,有几点经验可以参考:

  1. 从小开始:先找个小服务试试水,别一开始就把核心服务迁过去
  2. 监控要到位:P99、冷启动频率、错误率这些指标都要盯着
  3. 成本预估要准:别只看基础费用,把网关、日志、监控都算进去
  4. 保留回退方案:如果 Serverless 不符合预期,要有回退到传统架构的能力

结语

Serverless 不是万能药,但也不是噱头。它的核心价值是"减少运维心智负担",但前提是你愿意接受一些约束和权衡。

半年前我以为 Serverless 是"不用管服务器"的魔法,现在明白它更像是"把服务器藏起来,但你要懂它的脾气"。冷启动要管、成本要算、调试要花心思,这些麻烦并不会凭空消失,只是换了个形式存在。

但如果你的场景真的适合,这些付出是值得的。现在我的 API 服务不用半夜起来扩容,也不用担心安全补丁,代码推上去就能跑,账单虽然不像零但有数。这种"少操心"的感觉,确实很爽。

技术选择从来不是非黑即白,Serverless 也不例外。把它当工具,而不是信仰,用它适合的场景,剩下的交给传统架构,这才是务实的做法。

最后一句建议:别被概念迷惑,先动手试一个函数,亲自感受一下它的脾气,再做决定。

版权声明: 本文首发于 指尖魔法屋-Serverless 架构实践笔记https://blog.thinkmoon.cn/post/112-serverless-architecture-theory-practice-guide/) 转载或引用必须申明原指尖魔法屋来源及源地址!