8.5 告警设计:燃烧率与领先指标
上一节:8.4 生产观测:SLI 与错误预算 | 下一节:8.6 流量回放与金丝雀发布 配套代码:08-continuous-performance/05-alerting
一句话结论
告警的目标不是「发现所有异常」,而是「发现值得人介入的异常」。 因此:用燃烧率代替瞬时阈值(避免误报)、用饱和度作为领先指标(更早发现)、每条告警都配 runbook(否则只是制造焦虑)。
一、用「火灾报警」理解告警设计
一栋楼的火灾报警器有两种设计:
| 设计 | 结果 |
|---|---|
| 有烟就响 | 做饭、抽烟、蒸汽都会触发 → 所有人把电池拆了 |
| 探测烟浓度 + 温升速率 | 真实火灾才响 → 大家相信它 |
第二种设计的两个要点:
- 看"速率"而不是"瞬时值"——浓度上升快才是危险。
- 多传感器交叉验证——烟 + 温升同时满足才报警。
告警设计完全一样:
❌ 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 节),也就是说它经常不是真问题。先观察,积累数据后再决定是否升级。
七、本节小结
- 告警的目标是「发现值得人介入的异常」,不是「发现所有异常」。
- 用燃烧率代替瞬时阈值:
燃烧率 = 当前错误率 / 允许错误率;14.4 是常用阈值(2 天烧完 30 天预算)。 - 双窗口设计:长窗口抑制误报 + 短窗口保证灵敏,两个同时满足才告警。
- 用饱和度作为领先指标:连接池 pending、队列深度、GC 停顿、容器节流都早于延迟恶化。
- 告警分级:page(用户已受影响且需立刻介入)/ ticket / log;page 太多会被忽略。
- 告警疲劳与门禁误报是同一个问题——五个防治手段:阈值 > 噪声、双窗口、
for持续、runbook、定期 review。 - 每条告警都要有 runbook——否则只是制造焦虑。
- 定期 review 告警有效率,用数据校准阈值(甚至删除无效告警)。
八、自测
- SLO 是 99.9%,当前窗口内错误率是 0.5%。请计算燃烧率,并说明「照这个速度,30 天预算多久用完」。
- 为什么要用「双窗口」(1 小时 + 5 分钟)而不是只用一个窗口?请分别说明只用长窗口和只用短窗口的问题。
- 一个团队有 40 条告警规则,其中 8 条每天都在响。请说出你该怎么处理,以及判断「该保留哪条」的标准。
- 计算:
燃烧率 = 0.5% / 0.1% = 5。预算多久用完:30 天 / 5 = 6 天。含义与处理:照这个速度,30 天的错误预算会在 6 天内耗尽——这是一个需要立刻介入的信号(虽然还没到 14.4 的"立即 page"级别)。按照分级:燃烧率 6 对应「ticket(当天处理)」,同时应该加强金丝雀观察、暂停非必要发布,并立刻排查原因。注意:还要看这个 5 是持续的还是一次尖峰——如果是突发的(比如一次部署导致的短暂故障),恢复后燃烧率会迅速回落;如果是持续的,必须修复。 - 只用长窗口(1 小时)的问题:反应太慢——如果故障从 12:00 开始,1 小时窗口要到 12:30 之后才会明显超标(因为需要积累足够的错误样本),此时用户已经受影响半小时。只用短窗口(5 分钟)的问题:容易误报——短窗口样本少,偶发的几个错误(比如一次 GC 停顿、一次网络抖动导致的几个超时)就能让 5 分钟错误率瞬间超过阈值,导致频繁误报。双窗口的价值:长窗口保证"这不是偶发波动"(抑制误报),短窗口保证"现在还在发生"(保证灵敏)——两个条件同时满足,才说明这是一个正在持续的、有实质影响的故障。这样既能在故障持续几分钟内就报警,又不会因为偶发尖刺而误报。
- 处理方式:① 先统计每条告警的"有效率"(触发次数 vs 有效次数),至少看一个月的记录;② 对每天响的 8 条做分类:如果是「阈值太紧」(比如用的是噪声级别),放宽阈值;如果是「窗口太短」,加长窗口或加
for持续时间;如果是「指标与真实问题不相关」(比如 GC 停顿与 P99 尖刺不对齐),降级或删除;③ 把它从 page 降为 ticket 或 log(减少打扰);④ 补 runbook——很多"看一眼没事"的告警是因为没有明确的处理步骤。判断「该保留哪条」的标准:「这条告警触发时,是否有人需要立刻采取行动?」 —— ① 如果每次触发都需要行动 → 保留(且可能是 page);② 如果偶尔需要 → 保留但降级为 ticket;③ 如果从不需要行动 → 删除(它只是在消耗注意力)。核心理念:告警的价值不在于覆盖多少异常,而在于每一条都值得信任——40 条里如果有 8 条天天误报,另外 32 条也会被一起忽略(告警疲劳)。