Serverless 与 FaaS 可观测性:冷启动、调用链断点与并发限流

系统讲解 Serverless 与 FaaS 可观测性工程:冷启动的测量与阶段分解、函数实例生命周期与冻结机制、调用链在异步消息与事件源处的断点与上下文传播、并发度与限流(throttle)指标、日志采集与结构化、成本归因,以及跨平台统一采集架构与最佳实践清单。

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 LambdaCloudWatch Logs订阅过滤器 → Kinesis/Firehose/Lambda
Google Cloud FunctionsCloud LoggingLog Router sink → Pub/Sub
Azure FunctionsApplication 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 的计费维度天然可归因,把内存超配、超时边缘、重试放大三类浪费纳入例行审计,可观测性就直接变成了成本控制。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「infra」更多文章

  1. 仪表盘设计与可读性工程:信息层级、图表选型与避免误读
  2. 日志采样、去重与降噪:从每天 TB 级日志里捞出信号
  3. OpenMetrics 与指标规范治理:暴露格式、命名单位与兼容性