文档目录

8.5 告警设计:燃烧率与领先指标

上一节:8.4 生产观测:SLI 与错误预算 | 下一节:8.6 流量回放与金丝雀发布 配套代码:08-continuous-performance/05-alerting


一句话结论

告警的目标不是「发现所有异常」,而是「发现值得人介入的异常」。 因此:用燃烧率代替瞬时阈值(避免误报)、用饱和度作为领先指标(更早发现)、每条告警都配 runbook(否则只是制造焦虑)。


一、用「火灾报警」理解告警设计

一栋楼的火灾报警器有两种设计:

设计 结果
有烟就响 做饭、抽烟、蒸汽都会触发 → 所有人把电池拆了
探测烟浓度 + 温升速率 真实火灾才响 → 大家相信它

第二种设计的两个要点:

  1. 看"速率"而不是"瞬时值"——浓度上升快才是危险。
  2. 多传感器交叉验证——烟 + 温升同时满足才报警。

告警设计完全一样:

❌ P99 > 200ms → 告警       (看瞬时值,天天响)
✅ 燃烧率 > 14.4 → 告警      (看消耗速度 + 双窗口交叉验证)

二、燃烧率告警(错误预算的核心用法)

什么是燃烧率

燃烧率 = 当前错误率 / 允许的错误率

例:SLO = 99.9% → 允许错误率 = 0.1%
    当前错误率 = 1%
    → 燃烧率 = 1% / 0.1% = 10
    → 意味着"照这个速度,30 天的预算 3 天就用完了"

常用的燃烧率阈值

燃烧率 含义 窗口 处理
14.4 2 天烧完 30 天预算 1 小时 + 5 分钟 page(立即叫人)
6 5 天烧完 6 小时 + 30 分钟 ticket(工单)
1 正好用完 3 天 观察

为什么是 14.4:30 天 / (1 小时 / 30 天) ≈ 14.4 × 2——它保证「1 小时内消耗的预算不超过 2%」,同时能在 2 天内耗尽预算时及时报警。

双窗口设计的原理

长窗口(1 小时):抑制误报——偶发的尖刺不会让 1 小时的平均错误率超标
短窗口(5 分钟):保证灵敏——真实的持续故障会立刻让 5 分钟的错误率飙升

两个窗口【同时】满足才告警 → 既灵敏又不误报
# 只用一个窗口的问题
- alert: Bad
  expr: error_ratio_5m > 0.0144    # ❌ 5 分钟窗口太短,偶发尖刺就触发

- alert: AlsoBad
  expr: error_ratio_1h > 0.0144    # ❌ 1 小时窗口反应太慢,故障持续 50 分钟才报警

# ✅ 双窗口
- alert: ErrorBudgetBurnFast
  expr: |
    error_ratio_1h > 14.4 * 0.001
    and
    error_ratio_5m > 14.4 * 0.001
  for: 2m

三、领先指标告警(重要)

延迟和错误率是滞后指标——它们恶化时,用户已经在受影响。饱和度是领先指标。

指标 类型 提前量
连接池 pending 领先 几分钟到几十分钟
队列深度 领先 分钟到小时
GC 停顿 P99 领先 分钟
容器 CPU 节流 领先 分钟
磁盘 await 领先 分钟到小时
文件描述符使用率 领先 小时到天
P99 延迟 滞后 0
错误率 滞后 0

四条必加的领先指标告警

groups:
  - name: leading-indicators
    rules:
      # ① 连接池排队(最早的信号)
      - alert: ConnectionPoolSaturated
        expr: db_pool_pending > 0
        for: 5m
        labels: { severity: ticket }
        annotations:
          summary: "连接池出现排队(pending > 0 持续 5 分钟)"
          runbook: |
            ① 【先查慢查询,不要先加池】
               psql -c "SELECT calls, mean_exec_time, total_exec_time, left(query,50)
                        FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 10;"
            ② 查长事务:SELECT pid, now()-xact_start, state FROM pg_stat_activity
                          WHERE xact_start IS NOT NULL ORDER BY 2 DESC LIMIT 10;
            ③ 只有在查询正常、并发确实高时,才用 Little's Law 调整池大小
            ④ 详见第 6.5 节

      # ② GC 停顿(影响尾延迟)
      - alert: GcPauseHigh
        expr: |
          histogram_quantile(0.99,
            sum by (le) (rate(jvm_gc_pause_seconds_bucket[5m]))) > 0.2
        for: 10m
        labels: { severity: ticket }
        annotations:
          summary: "GC 停顿 P99 > 200ms"
          runbook: |
            ① 与 P99 尖刺做时间对齐(第 6.6 节)——不对齐则排除 GC
            ② 采分配火焰图找分配热点
            ③ 检查堆大小与 GC 选型
            ④ 详见第 7.7 节

      # ③ 容器 CPU 节流("本地好、上线慢"的头号原因)
      - alert: ContainerCpuThrottled
        expr: |
          rate(container_cpu_cfs_throttled_periods_total[5m])
          / rate(container_cpu_cfs_periods_total[5m]) > 0.25
        for: 10m
        labels: { severity: ticket }
        annotations:
          summary: "容器被 CPU 配额节流 > 25%"
          runbook: |
            ① 确认 CPU limit 是否过低
            ② 看 CPU 需求是否合理(可能是日志/序列化浪费)
            ③ 详见第 4.8 节

      # ④ 队列积压(无界队列 OOM 的前兆)
      - alert: ExecutorQueueBacklog
        expr: executor_queue_depth > 100
        for: 10m
        labels: { severity: ticket }
        annotations:
          summary: "线程池队列积压 > 100"
          runbook: |
            ① 查线程在忙什么(可能是在等 IO,不是真的在算)
               jcmd <pid> Thread.print | grep -A3 BLOCKED
            ② 检查是否有慢依赖
            ③ 详见第 6.5 节

注意每条告警的 runbook——没有 runbook 的告警只是制造焦虑。


四、告警分级

不是所有告警都该半夜叫人。

级别 触发条件 响应 渠道
page(立即) 错误预算快速燃烧(14.4×)、核心功能不可用 立即处理(可能有 SLA) 电话/短信/PagerDuty
ticket(工单) 领先指标异常(pending、GC、节流)、预算缓慢消耗 当天处理 工单系统/Slack
log(仅记录) 轻微波动、趋势变化 定期 review 看板/周报

分级的核心原则:

只有【用户已经在受影响,且需要立刻介入】才 page。
其余都是 ticket 或 log。

如果 page 太多:会被忽略(告警疲劳);如果 page 太少:可能漏掉真实故障。判断标准:每次 page 是否都需要人立刻行动?如果连续 5 次 page 都是「看一眼,没事」,就应该降级为 ticket。


五、告警疲劳:最常见的失败模式

① 某个告警一天响 10 次
② 工程师开始忽略它(或设成静音)
③ 三个月后真实故障发生时 → 也被忽略
④ 告警实际上已经死了

与门禁误报是同一个问题(第 8.1 节)。

五个防治手段

手段 做法
阈值 > 噪声 用第 5 章的噪声底线确定
双窗口 长窗口抑制误报
for 持续时间 异常持续 N 分钟才告警(避免瞬时波动)
runbook 让每次告警都有明确的处理步骤(减少"看一眼没事"的情况)
定期 review 每月统计「触发次数 / 有效次数」,无效的降级或删除

定期 review 的模板

## 告警复盘(2025-01)

| 告警 | 触发次数 | 有效次数 | 有效率 | 处理 |
| --- | --- | --- | --- | --- |
| ErrorBudgetBurnFast | 2 | 2 | 100% | 保持 |
| ConnectionPoolSaturated | 15 | 3 | 20% | 阈值从 pending>0 改成 pending>5,for 从 5m 改成 10m |
| GcPauseHigh | 30 | 0 | 0% | ⛔ 降级为 ticket(或删除,因为 GC 停顿与 P99 不对齐) |
| ContainerCpuThrottled | 1 | 1 | 100% | 保持 |

**结论**:GcPauseHigh 的 0% 有效率说明阈值(200ms)太紧 —— 要么放宽,要么删除

注意第二行:把阈值从 pending > 0 改成 pending > 5——这就是用实测数据校准阈值。


六、一个完整的告警清单

## 必加告警(按优先级)

### P0:page(立即叫人)
- [ ] 错误预算燃烧率 > 14.4(双窗口:1h + 5m)
- [ ] 核心接口完全不可用(成功率 < 90% 持续 2 分钟)
- [ ] 数据库不可用

### P1:ticket(当天处理)
- [ ] 连接池 pending > 5(持续 10 分钟)
- [ ] 容器 CPU 节流 > 25%(持续 10 分钟)
- [ ] 线程池队列深度 > 100(持续 10 分钟)
- [ ] 错误预算消耗 > 80%
- [ ] 磁盘使用率 > 85%

### P2:log(定期 review)
- [ ] GC 停顿 P99 > 200ms
- [ ] 文件描述符使用率 > 70%
- [ ] 缓存命中率下降 > 20%
- [ ] 慢请求计数异常上升

注意 GC 停顿被放在 P2——因为它常常与 P99 尖刺不对齐(第 6.6 节),也就是说它经常不是真问题。先观察,积累数据后再决定是否升级。


七、本节小结

  1. 告警的目标是「发现值得人介入的异常」,不是「发现所有异常」。
  2. 用燃烧率代替瞬时阈值:燃烧率 = 当前错误率 / 允许错误率;14.4 是常用阈值(2 天烧完 30 天预算)。
  3. 双窗口设计:长窗口抑制误报 + 短窗口保证灵敏,两个同时满足才告警。
  4. 用饱和度作为领先指标:连接池 pending、队列深度、GC 停顿、容器节流都早于延迟恶化。
  5. 告警分级:page(用户已受影响且需立刻介入)/ ticket / log;page 太多会被忽略。
  6. 告警疲劳与门禁误报是同一个问题——五个防治手段:阈值 > 噪声、双窗口、for 持续、runbook、定期 review。
  7. 每条告警都要有 runbook——否则只是制造焦虑。
  8. 定期 review 告警有效率,用数据校准阈值(甚至删除无效告警)。

八、自测

  1. SLO 是 99.9%,当前窗口内错误率是 0.5%。请计算燃烧率,并说明「照这个速度,30 天预算多久用完」。
  2. 为什么要用「双窗口」(1 小时 + 5 分钟)而不是只用一个窗口?请分别说明只用长窗口和只用短窗口的问题。
  3. 一个团队有 40 条告警规则,其中 8 条每天都在响。请说出你该怎么处理,以及判断「该保留哪条」的标准。
  1. 计算:燃烧率 = 0.5% / 0.1% = 5。预算多久用完:30 天 / 5 = 6 天。含义与处理:照这个速度,30 天的错误预算会在 6 天内耗尽——这是一个需要立刻介入的信号(虽然还没到 14.4 的"立即 page"级别)。按照分级:燃烧率 6 对应「ticket(当天处理)」,同时应该加强金丝雀观察、暂停非必要发布,并立刻排查原因。注意:还要看这个 5 是持续的还是一次尖峰——如果是突发的(比如一次部署导致的短暂故障),恢复后燃烧率会迅速回落;如果是持续的,必须修复。
  2. 只用长窗口(1 小时)的问题:反应太慢——如果故障从 12:00 开始,1 小时窗口要到 12:30 之后才会明显超标(因为需要积累足够的错误样本),此时用户已经受影响半小时。只用短窗口(5 分钟)的问题:容易误报——短窗口样本少,偶发的几个错误(比如一次 GC 停顿、一次网络抖动导致的几个超时)就能让 5 分钟错误率瞬间超过阈值,导致频繁误报。双窗口的价值:长窗口保证"这不是偶发波动"(抑制误报),短窗口保证"现在还在发生"(保证灵敏)——两个条件同时满足,才说明这是一个正在持续的、有实质影响的故障。这样既能在故障持续几分钟内就报警,又不会因为偶发尖刺而误报。
  3. 处理方式:① 先统计每条告警的"有效率"(触发次数 vs 有效次数),至少看一个月的记录;② 对每天响的 8 条做分类:如果是「阈值太紧」(比如用的是噪声级别),放宽阈值;如果是「窗口太短」,加长窗口或加 for 持续时间;如果是「指标与真实问题不相关」(比如 GC 停顿与 P99 尖刺不对齐),降级或删除;③ 把它从 page 降为 ticket 或 log(减少打扰);④ 补 runbook——很多"看一眼没事"的告警是因为没有明确的处理步骤。判断「该保留哪条」的标准:「这条告警触发时,是否有人需要立刻采取行动?」 —— ① 如果每次触发都需要行动 → 保留(且可能是 page);② 如果偶尔需要 → 保留但降级为 ticket;③ 如果从不需要行动 → 删除(它只是在消耗注意力)。核心理念:告警的价值不在于覆盖多少异常,而在于每一条都值得信任——40 条里如果有 8 条天天误报,另外 32 条也会被一起忽略(告警疲劳)。