Serverless 把「服务器」藏起来了,也顺手把可观测性的抓手一起藏了:没有主机可以登、没有常驻进程可以 attach profiler、实例随时被冻结或回收、请求可能在异步事件源处断成两段。同一个业务延迟问题,在传统部署里是"看哪台机器慢",在 FaaS 里变成"是冷启动、是下游慢、还是被限流了"。
本文围绕 Serverless 特有的四类信号展开:冷启动(怎么测量、怎么分解)、调用链断点(异步与事件源处的上下文丢失)、并发与限流(throttle 与并发度的真实含义)、日志采集(实例消失后的日志去哪)。最后给出跨 AWS Lambda、Cloud Functions、阿里云函数计算的可落地采集架构。想先理解 Serverless 与传统托管的取舍,可参考 Serverless 与传统托管对比 。
1. FaaS 的可观测性为什么不一样
1.1 三个结构性差异
差异一:实例是"短命且不可寻址"的
传统:一台机器有固定 IP/主机名,可以随时 ssh 上去看
FaaS:实例由平台调度,生命周期可能只有几百毫秒
排查时实例早已回收,事后无现场
差异二:执行环境会被"冻结"
平台在请求结束后冻结实例(freeze),复用时解冻(thaw)
冻结期间的"墙钟时间"不等于"计费时间",也不等于"CPU 时间"
任何依赖后台线程/定时器的监控埋点都会在冻结时静默失效
差异三:一次业务调用可能横跨多次函数执行
API Gateway → Lambda A → SQS → Lambda B → DynamoDB
中间隔了一次消息队列,trace 上下文默认会断掉
1.2 可观测性的四个观测面
| 观测面 | 平台提供 | 需要自建 |
|---|---|---|
| 调用指标 | 调用数、错误数、时长、并发度、throttle | 按业务维度的聚合与 SLO |
| 冷启动 | 部分平台有 Init Duration | 冷启动占比与归因 |
| 日志 | 采集到 CloudWatch/日志服务 | 结构化、采样、关联 trace_id |
| 链路 | 与平台 X-Ray/Trace 集成 | 跨异步边界的上下文传播 |
关键认知:平台只给「函数粒度」的指标,业务粒度的可观测性必须自建。一个函数可能承载多种事件源、多种租户,平台指标不会替你区分。
2. 冷启动的测量与分解
2.1 冷启动的构成
冷启动不是「一个数字」,而是一串可分解的阶段。以 Lambda 为例:
① 调度与实例分配(平台侧,通常不可观测)
② Init 阶段:下载代码包 → 启动 Runtime → 执行初始化代码
- 对应指标:Init Duration(Lambda)
- 大头往往是初始化代码里的:加载模型、建数据库连接池、读配置
③ Invoke 阶段:执行 handler
④ 若开启了 Provisioned Concurrency,则无 ②
用户可影响的部分只有 ② 里的"初始化代码"
2.2 测量方式
# AWS Lambda:从 CloudWatch Logs 的报告行里解析
# 日志中的 REPORT 行包含 Init Duration 字段
aws logs filter-log-events \
--log-group-name /aws/lambda/order-handler \
--filter-pattern "REPORT" \
--start-time $(date -d '-1 hour' +%s000) \
| jq -r '.events[].message' | head -5
# 输出样例:
# REPORT RequestId: 8f2b... Duration: 12.34 ms Billed Duration: 15 ms
# Memory Size: 512 MB Max Memory Used: 89 MB Init Duration: 842.11 ms
关键点:
Init Duration 只在冷启动时出现,可作为冷启动计数的信号
用"含 Init Duration 的日志行数 / 总调用数"估算冷启动比例
若平台不提供 Init Duration,用"时长分布的第二个峰"来识别
2.3 用指标识别冷启动
冷启动在时长分布上表现为一个远离主峰的长尾峰。用直方图的两个分位数对比即可粗略量化:
# 冷启动占比的近似估计:P99 与 P50 的比值异常放大
histogram_quantile(0.99, sum by (le, function) (rate(lambda_duration_bucket[5m])))
/
histogram_quantile(0.50, sum by (le, function) (rate(lambda_duration_bucket[5m])))
判读:
比值 < 3 —— 冷启动影响可忽略
比值 3~10 —— 有明显冷启动,考虑优化初始化
比值 > 10 —— 冷启动是主要延迟来源,需 Provisioned Concurrency
或把初始化工作移出 handler
2.4 降低冷启动的工程手段
| 手段 | 原理 | 代价 |
|---|---|---|
| 预热(Provisioned Concurrency) | 常驻实例,不触发 Init | 按常驻时长计费,成本高 |
| 精简依赖 | 减小代码包体积,加快下载 | 需重构 |
| 延迟初始化 | 把非必需初始化移到首次调用 | 首个请求仍慢 |
| 复用连接 | 在 handler 外建连接,实例复用时复用 | 需处理连接失效 |
| 快照恢复(Snapshot) | 从内存快照恢复运行时 | 平台支持度不一 |
| 换运行时 | 更小的 Runtime(如编译型) | 开发成本 |
⚠️ 注意:把数据库连接、HTTP 客户端等建在 handler 外部(模块作用域)才能在实例复用时生效。建在 handler 内部会导致每次调用都重建连接,既慢又容易打爆下游连接数。
3. 调用链在异步与事件源处的断点
3.1 断点从哪来
同步链路(API GW → Lambda → Lambda)一般能靠平台的 X-Ray/OTel 集成自动串起来。断点几乎都出现在异步边界:
断点一:Lambda → SQS/SNS/Kafka → Lambda
生产者注入的 traceparent 在消息属性里,消费者需要显式提取
断点二:Lambda → 事件源(S3 事件、DynamoDB Streams)
事件负载里没有 trace 上下文,需要靠"事件 ID + 业务 ID"关联
断点三:定时触发(EventBridge/Cron)
没有上游,需要为每次调度生成新的 root trace
断点四:Step Functions
各状态之间需要显式传递上下文
3.2 手动传播上下文
跨异步边界的标准做法是把 W3C traceparent 写进消息属性,消费端提取后作为父上下文。这与 可观测性上下文传播
中讲的一般原则一致,只是载体从 HTTP header 变成了消息属性:
# 生产者:把当前 trace 上下文注入 SQS 消息属性
from opentelemetry import propagate, trace
def publish_with_context(sqs, queue_url, payload):
carrier = {}
propagate.inject(carrier) # 写入 traceparent / tracestate
sqs.send_message(
QueueUrl=queue_url,
MessageBody=payload,
MessageAttributes={
"traceparent": {"DataType": "String", "StringValue": carrier.get("traceparent", "")},
"tracestate": {"DataType": "String", "StringValue": carrier.get("tracestate", "")},
},
)
# 消费者:从消息属性提取上下文,作为本次执行的父 span
def handler(event, context):
for record in event["Records"]:
attrs = record.get("messageAttributes", {})
carrier = {k: v["stringValue"] for k, v in attrs.items()}
ctx = propagate.extract(carrier)
with tracer.start_as_current_span("consume", context=ctx):
process(record["body"])
3.3 没有上下文时的关联兜底
兜底一:业务关联 ID(correlation_id / order_id)
在日志与链路中都打上,跨异步边界靠它人工或自动关联
兜底二:事件源 ID
S3 的 eventID、SQS 的 messageId、Kinesis 的 sequenceNumber
可作为弱关联键,精度取决于平台是否透传
兜底三:时间窗口 + 业务键
最后手段,用于无法注入上下文的第三方事件源
链路的存储与查询侧(采样、后端选型、TraceQL 查询)可参考 分布式链路追踪系统 的实践章节。
4. 并发与限流
4.1 并发度的真实含义
FaaS 的并发模型与线程池完全不同:并发度 = 同时活跃的实例数。平台按区域/账号/函数设置并发上限,超限的请求不是排队,而是直接被拒(throttle)。
Lambda 并发相关指标:
ConcurrentExecutions 当前并发执行数(瞬时)
UnreservedConcurrentExecutions 账号级未预留的可用并发
Throttles 被限流的调用次数
ProvisionedConcurrencyUtilization 预留并发使用率
关键区别:
Throttles(同步调用)→ 客户端收到 429
Throttles(异步调用)→ 消息重回队列,可能引发重复处理
4.2 限流告警与容量
groups:
- name: faas-concurrency
rules:
- alert: FunctionThrottling
expr: sum by (function) (increase(lambda_throttles_total[5m])) > 0
for: 5m
labels:
severity: warning
annotations:
summary: "函数 {{ $labels.function }} 出现限流,检查并发上限与下游配额"
- alert: FunctionNearConcurrencyLimit
expr: |
sum by (function) (lambda_concurrent_executions)
/ sum by (function) (lambda_reserved_concurrency) > 0.85
for: 10m
labels:
severity: warning
annotations:
summary: "函数 {{ $labels.function }} 并发使用率超过 85%"
容量规划要点:
同步链路:并发上限应按 P99 到达率 × 处理时长估算
异步链路:并发上限不足会积压,需监控队列深度
下游保护:函数并发可能瞬间放大 10 倍,必须给下游(DB)设连接池上限
或用队列解耦,避免"函数弹性 → 数据库雪崩"
4.3 并发放大与下游雪崩
这是 Serverless 最典型的次生故障:上游流量突增 → 平台自动扩容 → 数千并发同时打向数据库 → 数据库连接耗尽 → 全部超时 → 函数重试 → 进一步放大。
观测信号(三个指标同时看):
函数 ConcurrentExecutions 陡增
下游数据库连接数打满
函数错误率上升但"函数内部"看起来没报错(是下游超时)
对策:
用预留并发(Reserved Concurrency)给关键函数封顶
下游加连接池 + 队列缓冲
对异步事件源设置最大重试次数与死信队列(DLQ)
给函数设置超时,避免长尾请求长期占住并发
5. 日志采集与结构化
5.1 日志去哪了
FaaS 实例随时消失,日志必须在产生时就被平台捕获并转发。各平台的默认路径:
| 平台 | 默认日志目标 | 实时转发方式 |
|---|---|---|
| AWS Lambda | CloudWatch Logs | 订阅过滤器 → Kinesis/Firehose/Lambda |
| Google Cloud Functions | Cloud Logging | Log Router sink → Pub/Sub |
| Azure Functions | Application Insights | 内置导出 / Event Hub |
| 阿里云函数计算 | SLS 日志服务 | Logtail / SLS 投递 |
5.2 结构化日志与关联字段
函数日志必须每行一条 JSON,并携带链路上下文,否则在聚合查询时无法与其他信号关联:
{"ts":"2026-10-07T09:12:33.412Z","level":"error","service":"order-handler","function":"order-handler","version":"42","request_id":"8f2b-...","trace_id":"4bf92f3577b34da6a3ce929d0e0e4736","span_id":"00f067aa0ba902b7","cold_start":true,"msg":"payment timeout","downstream":"payment-svc","duration_ms":3002}
必带字段:
trace_id / span_id 与链路关联
request_id 平台级唯一请求 ID(AWS 的 RequestId)
cold_start 是否冷启动,便于按冷启动过滤
function / version 函数名与版本,便于灰度对比
duration_ms 处理时长,便于与平台指标交叉验证
5.3 采样与成本
FaaS 日志按量计费,高并发函数一天可以产生 TB 级日志。控制策略:
分层采样:
error / warn 100% 保留
info 按 10% 采样(用 trace_id 做一致性哈希,保证同一条链路全采或全不采)
debug 生产环境关闭
字段裁剪:
去掉冗余的大字段(完整请求体、base64 图片)
长字段截断并标注 truncated=true
生命周期:
热数据 7 天,冷数据转对象存储,30 天后归档
6. 采集架构与成本归因
6.1 跨平台统一采集
推荐架构(OTel 优先):
函数内:OTel SDK(trace + metrics)+ 结构化日志
↓ OTLP
每账号/区域一个 OTel Collector(可由函数或 ECS 承载)
↓ 批处理、采样、脱敏、字段裁剪
后端:Prometheus/Mimir(指标)+ Tempo/Jaeger(链路)+ Loki(日志)
要点:
用 OTel Collector 做"边缘聚合",避免每个函数直连后端
Collector 负责统一资源属性(cloud.provider/region/function.version)
采样放在 Collector,函数内不做复杂采样逻辑(实例短命,状态难维护)
6.2 成本归因
FaaS 的计费维度是 调用次数 × 执行时长 × 内存规格,天然适合按函数归因:
归因公式(Lambda 为例):
单次成本 = GB-秒 × 单价 = (内存GB × 时长秒) × 单价
月度成本 ≈ Σ(调用次数 × 平均时长 × 内存GB) × 单价 + 请求费
可观测性用途:
找出"内存超配"的函数:Max Memory Used / Memory Size < 0.4
→ 降配可直接降成本
找出"超时边缘"的函数:P99 时长接近配置的超时时间
→ 容易触发重试,放大成本
找出"被重试放大"的调用:调用次数 / 业务事件数 > 1.2
# 内存配置浪费:峰值内存远低于配置值
max_over_time(lambda_max_memory_used_bytes[7d])
/ (lambda_memory_size_bytes) < 0.4
7. 最佳实践
□ 平台指标(调用/错误/时长/并发)全量采集,并建立函数级 SLO
□ 单独量化冷启动占比,而非只看平均延迟
□ 初始化代码放在 handler 外部,连接与客户端复用
□ 跨异步边界显式传播 traceparent,消费端 extract 后建子 span
□ 无法注入上下文时,用业务关联 ID 兜底
□ 为关键函数设置预留并发上限,保护下游
□ 异步调用配置最大重试与死信队列,监控 DLQ 深度
□ 日志一律结构化 JSON,必带 trace_id / request_id / cold_start
□ 日志分层采样,error 全留、info 采样,控制成本
□ 用 OTel Collector 做边缘聚合与统一采样
□ 定期按内存超配、超时边缘、重试放大三个维度做成本审计
对于面向外部用户的关键函数,还应当从「用户视角」定期验证可用性,这与 合成监控 的探针思路互补:平台指标告诉你函数在跑,合成探针才告诉你用户能不能用。
小结
Serverless 可观测性的难点不在"指标少",而在平台给的粒度和排障需要的粒度不匹配。抓住四条主线即可:冷启动要用 Init Duration 或时长分布的第二个峰来量化,并从初始化代码入手优化;调用链断点几乎全在异步边界,靠显式传播 traceparent 或业务关联 ID 缝合;并发与限流要盯住 Throttles 与并发使用率,尤其防止自动扩容打垮下游;日志必须在产生时就被采集并结构化,携带 trace_id 与 cold_start 便于关联与过滤。最后,FaaS 的计费维度天然可归因,把内存超配、超时边缘、重试放大三类浪费纳入例行审计,可观测性就直接变成了成本控制。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。