Day 21 Medium Observability Metrics/Logs/Traces SLO OpenTelemetry

可观测性 — 当系统挂了,你能问出『为什么』吗Observability: Metrics, Logs, Traces, OpenTelemetry & SLO-driven On-call

问题场景 + 需求约束

设计一个支撑 1000+ 微服务、每秒百万 span 的可观测性平台。某天上午 p99 延迟从 80ms 飙到 2s,用户开始投诉——你要在 5 分钟内定位到是哪个服务、哪个依赖、哪类请求出了问题。这不是『加几个监控图』,而是一个数据规模堪比业务本身的系统。

监控(monitoring)回答『系统是否健康』(已知问题),可观测性(observability)回答『为什么不健康』(未知问题)。前者是预设好的 dashboard 和告警,后者是事故现场任意切片、下钻的能力。

高层架构

graph LR
    APP["服务 + OTel SDK
埋点 / context 传播"] COL["OTel Collector
批处理 · 采样 · 路由"] M["Metrics TSDB
Prometheus / Mimir"] T["Trace 存储
Tempo / Jaeger"] L["日志存储
Loki / ES"] Q["查询 + 关联层
Grafana"] A["告警引擎
Alertmanager"] OC["On-call
PagerDuty"] APP -->|OTLP| COL COL --> M COL --> T COL --> L M --> Q T --> Q L --> Q M --> A -->|burn rate 超限| OC classDef svc fill:#1a2530,stroke:#64c8ff,color:#e8eef5 classDef pipe fill:#0e2030,stroke:#5eead4,color:#e8eef5 classDef store fill:#1a1a30,stroke:#ffb450,color:#e8eef5 classDef sink fill:#2a1530,stroke:#ff7ab6,color:#e8eef5 class APP svc class COL,Q pipe class M,T,L store class A,OC sink

SDK 统一埋点 → Collector 做采样/路由解耦应用与后端 → 三种信号靠 trace_id / exemplar 互相跳转

关键设计:应用只依赖 OTel SDK + Collector,不绑定任何后端厂商(避免 vendor lock-in)。Collector 是控制点——采样、限流、PII 脱敏、降采样都在这里做,改采样策略不用重新发布几百个服务

关键技术点

1. 三支柱 Metrics / Logs / Traces — 不是三个工具,而是一份数据的三种切片

原理Metrics 是预聚合的数值时间序列(counter/gauge/histogram),便宜、适合 dashboard 与告警,但只有你预先定义的维度。Logs 是离散事件,信息最全但检索贵、无结构难关联。Traces 把一个请求跨服务的因果链串成一棵 span 树,回答『慢在哪一跳』。三者的本质区别是基数承受力:metric 加一个高基数 label(如 user_id)会让时间序列数爆炸成百万级;trace/structured-log 天生适合高基数。

Trade-off: 正确姿势:用 metric 发现异常 → 用 exemplar 跳到一条代表性 trace → 用 trace_id 拉出该请求全部日志。三支柱靠 trace_id 缝合,而不是三个割裂的孤岛。
现实案例:

2. 分布式追踪与 OpenTelemetry — context 传播 + 采样的两难

原理:一个请求经过 A→B→C,要把它们的 span 归到同一棵树,靠 context propagation:入口生成 trace_id,每跳生成 span_id 并记录 parent_id,通过 HTTP header(W3C traceparent)往下游传。OpenTelemetry(OTel)统一了这套 API/SDK/协议(OTLP),让埋点与后端解耦。核心难题是采样:百万 span/s 全存会破产,但采样又可能漏掉出错的那条。

Head sampling vs Tail sampling:
# OTel Collector tail_sampling 策略 (pseudo-config)
processors:
  tail_sampling:
    decision_wait: 10s          # 等一条 trace 的 span 聚齐
    policies:
      - type: status_code       # 出错的全留
        status_code: {status_codes: [ERROR]}
      - type: latency           # 慢的全留
        latency: {threshold_ms: 1000}
      - type: probabilistic     # 其余按 1% 采
        probabilistic: {sampling_percentage: 1}
# ⚠️ 多个 Collector 实例时, 同一 trace_id 的 span 必须路由到同一实例
#    (load-balancing exporter 按 trace_id 哈希), 否则聚不齐
现实案例:

3. SLI / SLO / SLA 与 Error Budget — 把『可靠性』变成可决策的预算

原理SLI(指标)= 好请求 / 总请求,如『延迟 < 200ms 的请求占比』。SLO(目标)= 对 SLI 设的内部门槛,如『99.9% 月度』。SLA(协议)= 对外承诺 + 违约赔偿,通常比 SLO 松(留缓冲)。关键发明是 error budget(错误预算)= 1 − SLO:99.9% 意味着一个月可以『坏』43 分钟。这把可靠性从『越高越好』的口水仗,变成一个可以花的预算——预算没花完就允许大胆发版(创新),花超了就冻结发布专心修稳定性。它对齐了产品团队和 SRE 的激励。

告警怎么用 error budget:传统阈值 vs burn rate
# 多窗口 burn rate 告警 (PromQL 思路)
# 快窗 1h 与慢窗 5m 同时超 14.4x 才 page (防抖)
(
  error_budget_burn_rate{window="1h"} > 14.4
  and
  error_budget_burn_rate{window="5m"} > 14.4
)
# burn_rate = (1 - SLI) / (1 - SLO)
#   = 实际错误率 / 预算允许错误率;  >1 表示在透支预算
现实案例:

4. Metrics 聚合陷阱 — 为什么 p99 不能再求平均

原理:百分位数(percentile)不可二次聚合。十台机器各自的 p99 求平均,得不到全局 p99——这是数学上错的。正确做法:客户端用 histogram(按 bucket 累计计数),把各机器的 bucket 计数相加后再用 histogram_quantile() 在查询时算分位数。summary 类型在客户端就把分位数算死了,跨实例无法合并。这也是 Prometheus 官方推荐 histogram > summary 的核心原因。

Histogram vs Summary:
现实案例:

扩展与优化

常见陷阱 + 面试追问

1. 看均值不看分位:平均延迟 80ms 很美好,但 p99 是 2s——1% 的用户在受苦,而恰恰是高价值重度用户。永远监控分位数。
2. 高基数 label 炸 TSDB:给 metric 加 user_id / request_id / 完整 URL,时间序列从几千暴涨到几百万,Prometheus OOM。高基数维度属于 trace/log。
3. head sampling 丢了出错请求:1% 头部采样下,偶发的 0.1% 错误请求几乎采不到,事故时无 trace 可看。错误/慢请求要 tail sampling 兜底。
4. 告警疲劳:几百条静态阈值告警,80% 是噪音 → on-call 麻木 → 真事故被淹没。改 SLO burn-rate 告警 + 严格区分 page(要人立刻起床)与 ticket。
5. 可观测性系统自己挂了:监控和被监控系统共用基础设施,故障时监控也瞎。可观测性栈要独立部署、独立故障域,并有外部黑盒探测兜底。

高频面试题:① metrics / logs / traces 各解决什么、为什么不能只用一个?② head vs tail sampling 的取舍,tail 怎么保证同 trace 的 span 聚齐?③ SLO / error budget 怎么定,burn-rate 多窗口告警为什么比静态阈值好?④ 为什么 p99 不能跨机器求平均?histogram 和 summary 怎么选?⑤ 怎么控制可观测性成本(采样 + 基数 + 降采样三板斧)?

深入资源

深入思考(点击展开答案)

1. 一个 metric 加 user_id label 看似方便,为什么是灾难?算一笔账:1000 万用户 × 10 个其他 label 维度会怎样?

TSDB 的存储成本正比于活跃时间序列数(active series),而非请求量。每个唯一的 label 组合 = 一条独立序列。

  • 不加 user_id:假设 5 个 endpoint × 4 个 status × 3 个 region = 60 条序列,毫无压力。
  • 加 user_id:60 × 1000 万 = 6 亿条序列。每条序列内存几 KB(标签 + 最近样本 + 索引),直接几 TB 内存 → Prometheus OOM 崩溃。

本质:metric 适合『有界、低基数』维度(枚举型)。user_id、request_id、完整 URL、错误堆栈是『无界、高基数』,属于 trace 和结构化日志——它们的成本随事件而非维度笛卡尔积增长。这正是 Honeycomb 主张『宽事件』的理由:高基数查询是调试的核心能力,但要放对存储。治理手段:上线前评审 label、Collector 端 relabel drop、对疑似高基数维度做 cardinality 监控告警。

2. Tail sampling 要等一条 trace 的所有 span 聚齐再决策。在多个 Collector 实例的集群里,这引入了什么棘手的分布式问题?

核心矛盾:同一条 trace 的 span 来自不同服务、不同时间到达,可能落到不同的 Collector 实例。要做 tail 决策必须把它们聚到同一处。

  • 路由一致性:需要一层 load-balancing exporter 按 trace_id 哈希,保证同 trace 永远进同一个后端 Collector。实例扩缩容时哈希环变化会导致迁移期 span 错配(类似 Day 4 的 resharding 问题)。
  • 状态与内存:Collector 要 buffer 住未决策的 span(decision_wait 窗口,如 10s)。流量大时这是巨大内存压力,且 span 迟到/丢失会让 trace 残缺。
  • 决策延迟:必须等够窗口才知道留不留,trace 落库有固有延迟,实时性差于 head。
  • 窗口外的尾巴:超长 trace(> decision_wait)的尾部 span 会被当成孤儿,要么误丢要么单独处理。

这就是为什么很多团队用『head 低采样保底 + tail 只对 error/slow 兜底』的混合:把 tail 的状态压力限制在少量值得保留的 trace 上。

3. SLO 定 99.9% 还是 99.99%?多一个 9 的真实代价是什么?为什么『越高越好』是错的?

每多一个 9,允许的月度停机从 43 分钟(99.9%)压到 4.3 分钟(99.99%)再到 26 秒(99.999%)。代价不是线性而是指数级

  • 架构成本:4 个 9 要多区冗余、自动 failover;5 个 9 要无单点、跨区强一致——成本可能翻几倍。
  • 速度代价:error budget 越小,能用来发版/实验的空间越小,创新被锁死。99.999% 意味着几乎不能停机部署。
  • 边际收益递减:用户和下游依赖往往感知不到比『他们自己的可靠性 + 网络抖动』更高的 9。如果用户的网络本身只有 99.9%,你做到 99.999% 是白烧钱。

所以 SLO 应该从业务/用户体验反推『多可靠才够』,而不是工程师追求完美。100% 是错误目标:它消灭了 error budget,等于禁止一切变更,反而因无法迭代而更脆弱。正确的 SLO 是『刚好让用户不抱怨』的那条线,把剩下的预算花在发布速度上。

4. 串联 Day 6(一致性):metrics 本身是『最终一致 + 有损』的。这对告警意味着什么风险?

监控数据本身就不是强一致、不是精确的,这条经常被忽略却很致命:

  • 采集延迟:Prometheus 按 scrape interval(如 15s)拉取,告警评估又有自己的周期,端到端可能滞后 30s-1min。说『1 分钟内告警』时这部分要算进去。
  • 采样有损:trace 采样后,稀有事件统计天然失真;基于采样数据估算错误率要乘回采样率,且小样本方差大。
  • 聚合丢信息:metric 是预聚合的,p99 histogram 的精度受 bucket 边界限制,边界设错会系统性高估/低估。
  • 抓取缺口:target 短暂不可达 = 数据空洞,告警规则要区分『指标是 0』和『指标缺失』(用 absent()),否则服务全挂时反而因为没数据而不告警。

设计含义:告警要容忍数据的最终一致性——用多窗口(短窗抓急性、长窗去抖)、设置 for 持续时间避免单点毛刺、对『监控数据本身缺失』单独告警。永远别假设 dashboard 上的数字是当下精确真相。

5. 事故中,从『p99 报警』到『定位根因服务』,理想的下钻路径是什么?三支柱在这条链上各自扮演什么角色?

这是可观测性价值的终极检验——能不能把 MTTR(平均修复时间)压到几分钟。理想链路:

  1. Metric 发现(What):burn-rate 告警触发,RED dashboard 显示某 endpoint p99 从 80ms→2s,error rate 上升。确认『有问题、影响多大、何时开始』。
  2. Metric 缩小范围(Where):按 region/version/dependency 维度切分 metric,发现只有 v2.3 + us-east 的请求受影响 → 怀疑某次发布或某区依赖。
  3. Exemplar 跳 Trace(Why-1):从 p99 那个 bucket 的 exemplar 一键打开一条代表性慢 trace,span 树立刻显示『时间花在 service-C 调 DB 的那一跳,占了 1.8s』——根因服务锁定。
  4. Trace 拉 Log(Why-2):用该 trace 的 trace_id 过滤日志,看到 service-C 的 DB 连接池耗尽报错堆栈,确认机理。

角色分工:metric=雷达(便宜、全覆盖、发现异常),trace=GPS(因果定位到哪一跳),log=显微镜(单点细节)。三者靠 trace_id/exemplar 缝合;缺任何一环,下钻就断链,只能靠人肉猜——这正是『三个割裂工具』vs『真正可观测性』的差别。