2953 字
约 9 分钟
18
S18-X18 · 可观测性设计:日志、指标与追踪

线上出问题,最难的往往不是"没数据",而是数据都在但连不起来:日志一堆异常、指标显示 CPU 正常、追踪一堆慢 span,却没人答得出"这次请求在哪一步失败了"。

设计目标不是"多采集",而是让一个问题三步内可定位:整体异常 → 具体请求 → 具体代码段。三者各管一段,缺一段就断链。


1. 三者的分工与只用其一的盲区

类型 回答的问题 数据结构 成本特征
日志 这一刻发生了什么 带时间戳的事件流 量与流量成正比,最贵
指标 整体是否正常、趋势如何 数值时间序列(聚合后) 与标签基数成正比
追踪 一次请求慢在哪一段 有父子关系的 span 树 与采样率成正比

只用其中一种,盲区是固定的:

只有 盲区 后果
日志 整体水位与趋势 只能靠人翻,无法提前发现
指标 单次请求的因果链 知道"错误率 5%",不知哪类请求错
追踪 未被采样的请求 慢请求恰好没采到,问题隐身

三者是索引关系:指标发现异常 → 缩小范围(接口/版本/地域)→ 追踪定位具体请求 → 日志看这次请求的细节。


2. 日志的四个设计要求

要求 具体做法 不做的后果
结构化 输出 JSON,字段固定(ts/level/msg/traceId/业务键) 只能靠正则捞,字段一改全断
级别语义 ERROR 只给"需要人处理"的事件;可预期的业务失败用 WARN/INFO 告警被噪声淹没
请求标识贯穿 入口生成 traceId,全链路透传,每条日志都带 无法把一次请求的日志拼起来
敏感信息不落盘 手机号、证件、token 在序列化层脱敏 合规风险 + 日志滥用
{"ts":"2026-11-09T07:12:03.441Z","level":"ERROR","svc":"order-api",
 "traceId":"4f2a9c1e8b7d3a55","spanId":"9c31aa02","userId":"u_88213",
 "event":"payment_callback_failed","orderId":"O20261109001",
 "errCode":"PG_TIMEOUT","elapsedMs":3210,"msg":"gateway timeout after retry"}

必带字段:traceId(把一次请求串起来)、event(稳定枚举名,写自然语言就无法聚合)、errCode(分组用)、elapsedMs(让日志本身能算耗时分布)。

采样要分类型:

错误 / 慢请求日志:全量保留
正常 INFO 日志:按请求采样,或按 traceId 哈希采样(同一请求要么全留要么全不留)
调试日志:默认关闭,按 traceId 临时开启

3. 指标的四类与"为什么不能用平均值"

类型 语义 典型用途 陷阱
Counter(计数器) 只增不减的累计值 请求总数、错误数 查询时必须用 rate(),直接看累计值无意义
Gauge(计量器) 可增可减的瞬时值 队列长度、内存占用、连接数 采样点间隔决定精度,尖峰可能被跳过
Histogram(直方图) 分桶统计的分布 延迟、响应大小 桶边界决定精度,+Inf 桶必须存在
Summary(摘要型) 客户端算好的分位数 单实例延迟分位 不可跨实例聚合,这是硬伤

延迟是长尾分布:平均值会被少数慢请求拉高,却不告诉你拉高了多少、影响了多少人。

# 错误:平均值,掩盖长尾
rate(http_request_duration_sum[5m]) / rate(http_request_duration_count[5m])

# 正确:分位数(基于 histogram 桶)
histogram_quantile(0.99,
  sum by (le, route) (rate(http_request_duration_bucket[5m])))

两个必须知道的限制:

  1. 分位数不能做算术:p99(A+B) ≠ p99(A) + p99(B),也不能对分位数求平均。
  2. 桶边界决定 p99 可信度:目标区间的桶太稀,插值出的 p99 只是估计值——按实际分布核对桶边界。

4. RED 与 USE:两套视角,别互相替代

视角 指标 适用对象 回答
RED Rate、Errors、Duration 面向请求的服务(HTTP/gRPC 接口) 用户侧体验是否正常
USE Utilization、Saturation、Errors 面向资源的组件(CPU、磁盘、连接池、队列) 哪一环被压满了

RED 站调用方,USE 站被调用方。路径:RED 发现某接口错误率上升 → USE 看它依赖的资源是否饱和 → 定位到具体组件。只用 RED 时,错误率上升而资源全绿说明瓶颈在下游;只用 USE 会漏掉"资源充足但逻辑出错"。


5. 追踪的核心概念

概念 含义 设计要点
trace 一次请求的完整调用树 由入口生成 id,全链路唯一
span 树上的一个节点(一次操作) 必须有开始/结束、操作名、状态、属性
父子关系 谁调用了谁 跨进程时靠上下文传播建立
上下文传播 把 traceId/spanId 塞进请求头 用标准头(traceparent)而非自定义
采样 只保留部分 trace 头部采样决定"采不采",尾部采样决定"留不留"
# W3C Trace Context:跨进程传播的标准头
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
#            版本-traceId(16字节)-parentSpanId(8字节)-flags(01=采样)

采样策略是设计决策,不是配置项:

策略 机制 优点 缺点
固定比例头部采样 入口按概率决定 简单、开销低 慢请求/错误请求可能恰好没采到
尾部采样 先全收,结束后按规则决定 能保证"错误与慢请求必留" 需要缓冲,实现复杂
按属性采样 按路由/租户/版本分流 关键路径全量、其余低采样 规则要随业务演进维护

可用组合:头部采样保基础覆盖率 + 尾部采样保错误与慢请求。纯固定比例的典型故障是 0.1% 采样下,只在万分之一请求出现的超时恰好没被采到。


6. 三者如何串起来

请求进入 → 生成 traceId → 注入日志上下文(MDC) + 响应头 + 下游请求头
        → 指标按 route/status 打标签(不含 userId)→ span 记录每段耗时
异常:指标告警 → 看 route/version 分布
     → 用 traceId/时间窗过滤 → 打开 trace 看最慢 span
     → 用 traceId 查日志 → 看到 errCode 与参数

串联的三个技术前提:

  1. 日志里必须有 traceId,否则跳不过去;
  2. 指标标签里要有能过滤到 trace 的维度(route、version、status),但不能是 userId;
  3. trace 里要记业务属性(orderId、tenantId),否则只看到"慢"看不到"为什么慢"。

7. SLO:把"正常"定义成可计算的

要素 选择方式 反例
SLI 用户可感知的比率,如成功率、p99 达标率 用 CPU 使用率当 SLI
目标值 按历史水位与容忍度定,不是拍 99.99% 全线统一 99.99%,成本失控
统计窗口 滚动窗口(28 天)比自然月稳 月末归零,行为被扭曲
错误预算 1 - 目标值 允许的失败额度 只算不控,烧完没人停发布
slo:
  name: order_api_availability
  sli: |   # 成功率 = 非 5xx / 总请求
    sum(rate(http_requests_total{route="/api/orders",status!~"5.."}[5m]))
    / sum(rate(http_requests_total{route="/api/orders"}[5m]))
  target: 0.999      # 按业务容忍度核对,不要照抄
  window: 28d
  burn_rate_alerts:  # 多窗口燃烧率:短窗口灵敏,长窗口稳
    - {window: 1h, factor: 14.4}
    - {window: 6h, factor: 6}

错误预算的用途是决策依据:预算充足可提高发布频率,烧得快就冻结变更——把"要不要发"从争论变成算术。


8. 告警设计

原则 做法 反例
按症状告警 告警"下单成功率下降",不告警"CPU 高" CPU 高但服务正常 → 无效告警
分级 P1 用户可感知中断 / P2 部分降级 / P3 趋势预警 全部同一个通道,全部 @所有人
可执行 每条告警附"下一步查什么"的链接或命令 告警只说"异常",值班人从头查
抑制与聚合 同一根因的多条告警合并 一次 DB 抖动触发 200 条告警

疲劳的机制很直接:误报率高 → 值班人开始忽略 → 真告警一起被忽略。宁可少几条,也要让每条都指向真实症状。


9. 成本控制

成本项 膨胀机制 控制手段
日志量 每个请求多条 + 无采样 + 长字段 结构化 + 分级采样 + 字段长度上限
指标基数 标签用了高基数字段(userId、orderId、URL 全路径) 只打低基数维度;路径做模板化(/orders/:id)
追踪存储 采样率过高 + span 属性过多 尾部采样 + 限制属性数量

基数爆炸是最常见的自伤:给指标加 userId 标签,一百万用户就多一百万条时间序列,写入与查询同时崩。判定规则只有一条:这个标签的取值数量是否可控(几百以内)。


10. 排错:五种"有数据但查不出"

现象 原因 处理
日志搜不到某次请求 没有 traceId 或没透传到下游 入口生成 + 拦截器统一注入
指标面板上延迟"看起来正常" 用的是平均值 换成 histogram 分位
两个服务的时间对不上 时钟未同步、时区混用 统一 UTC + NTP,日志存 ISO8601 带时区
trace 断在某个服务 该服务没接上下文传播 检查 HTTP 客户端是否注入 traceparent
告警一直响但看不出问题 告警按原因而非症状 改告警对象为错误率/成功率

11. 边界:可观测性不替代什么

问题 可观测性能给答案吗 该用什么
某段代码为什么这么慢(算法级) 只能定位到函数 profiler
数据不一致的根因 部分(靠日志比对) 数据校验任务、审计表
用户主观体验 间接(延迟/成功率) 前端 RUM + 真实用户反馈

一句话:可观测性回答"发生了什么、在哪一段",不回答"这段代码为什么这样写"。


12. 自测

  1. 只有指标没有追踪,排查一次具体请求的失败会卡在哪一步?
  2. 为什么延迟必须用分位数而不是平均值?分位数为什么又不能互相做算术?
  3. Summary 类型为什么不能跨实例聚合?
  4. 固定比例采样在什么情况下会漏掉最该看的请求?尾部采样怎么补?
  5. 一条告警应该按"症状"还是"原因"设计?举一个反例说明后果。
  6. 给指标加 userId 标签会发生什么?判定一个标签是否安全的规则是什么?
S18-X18 · 可观测性设计:日志、指标与追踪
https://www.clxhxhhr.top/posts/5940/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。