50.5 告警(分级 / 症状 vs 原因 / runbook / 降噪)
一句话
告警的唯一目的是「让对的人在对的时间做对的事」;所以按「要不要现在叫醒人」分级、只对用户能感知的症状报警(原因交给看板)、每条告警配一份能止血的 runbook、噪音用分组/抑制/延迟压掉——四条缺一条,告警体系就会退化成一个没人看的群。
分级:按「要不要现在起床」分,不按「组件重不重要」分
| 级别 | 判据(影响面 × 严重度) | 响应时限 | 通知方式 | 例子 |
|---|---|---|---|---|
| P0 | 核心功能不可用 / 资损 / 数据错乱 / 安全事件 | 5 分钟内有人接手 | 电话 + IM + 值班群 | 支付成功率从 99.9% 掉到 90% |
| P1 | 部分用户受影响,或 SLO 在快速燃烧 | 15 分钟 | 电话 + IM | 某地域错误率 5%,预算 1 小时烧掉 10% |
| P2 | 用户可感知但已降级,或预算缓慢燃烧 | 工作时间当天 | IM / 工单 | 重试率上升、队列积压但不影响 SLA |
| P3 | 趋势性、容量预警 | 下一个工作日 | 工单 / 只看板 | 磁盘按当前增速 30 天写满 |
关键判据是影响面 × 严重度,不是「这个组件很重要」。同一个组件可以有不同级别:MySQL 主库切换(P1,自动切换已生效)与 MySQL 数据目录损坏(P0,需要人恢复)不是一回事。
量级参考(经验值,随团队与业务差异大):值班一晚被叫醒超过 1 次,半年内必然出现「瞄一眼就关掉」的习惯;可行动率低于 50% 的告警体系,等价于没有告警——因为它消耗的是人对告警的信任。
症状 vs 原因:只对症状报警
| 信号 | 类型 | 该叫醒吗 | 放哪 |
|---|---|---|---|
| 错误率 / 成功率 / P99 / 队列等待时长 / 成交率 | 症状(用户已在受苦) | 该 | 告警 |
| CPU 80% / GC 时间长 / 单机磁盘 70% / 线程池 90% | 原因(用户可能马上受苦) | 不该 | 看板,挂在症状告警之下 |
| 磁盘 24 h 内写满 / 证书 7 天过期 / 连接数按当前增速 1 h 耗尽 | 可预测的未来症状 | 该 | 告警(带提前量,且给出「还剩多久」) |
| 发布 / 配置变更 / 扩缩容事件 | 上下文 | 不该单独告警 | 标注到看板与告警消息里 |
反例:CPU 85% 触发 P1 电话。半夜叫醒人,用户毫无感知,起来看一眼 CPU 又睡回去。正确做法是:CPU 高但 SLO 未破 → 只上看板;CPU 高导致 P99 破 SLO → 症状告警会响,而看板上 CPU 曲线与延迟曲线的拐点会自动把因果摆出来。
🔑 判别句式:「用户现在疼吗?」疼 → 症状,叫人;「用户快疼了吗?」快 → 未来症状,提前叫人;「这可能是疼的原因」→ 原因,上板。
例外只有一种:原因类信号一旦越界就会不可逆(磁盘写满、证书过期、主从延迟超过 binlog 保留期),此时它就是「未来症状」,必须告警。
告警规则:三个参数决定误报与漏报
# Prometheus 规则:SLO 99.9%(错误预算 0.1%),1 小时燃烧率 > 14.4
groups:
- name: checkout-slo
rules:
- alert: CheckoutErrorBudgetBurnFast
expr: |
(sum(rate(http_requests_total{route="/checkout", code=~"5.."}[1h]))
/ sum(rate(http_requests_total{route="/checkout"}[1h]))) > (14.4 * 0.001)
and
(sum(rate(http_requests_total{route="/checkout", code=~"5.."}[5m]))
/ sum(rate(http_requests_total{route="/checkout"}[5m]))) > (14.4 * 0.001)
for: 2m
labels:
severity: P1
team: checkout
annotations:
summary: "结算错误率 1 小时燃烧率 > 14.4(约 2% 预算/小时)"
description: "当前错误率 {{ $value | humanizePercentage }},先看 runbook 止血"
runbook_url: "https://runbooks.example.com/checkout-error-budget"
| 参数 | 调大 | 调小 | 建议 |
|---|---|---|---|
| 统计窗口 | 平滑、慢半拍 | 灵敏但抖 | 窗口 ≥ 4 × 抓取间隔(15 s 抓取 → 至少 1 m) |
for(持续时间) | 少误报、晚发现 | 快发现、多误报 | 与燃烧速度匹配:P0 用 2–5 m,P2 用 30 m |
| 阈值 | 漏报 | 误报 | 从 SLO 反推,不拍脑袋(99.9% → 阈值取 1% 或燃烧率 14.4) |
基于错误预算的告警(burn rate)
- SLO 99.9% → 每月错误预算 = 30 天 × 0.1% = 43.2 分钟的「全错」时间。
- 燃烧率
BR = 当前错误率 / (1 − SLO);BR = 1表示「刚好在周期内烧完预算」。 - 99.9% 下,
BR = 14.4对应错误率 1.44%:1 小时烧掉 2% 的预算(约 52 秒额度)。要烧完整月预算需要约 50 小时。
| 窗口组合 | 燃烧率 | 消耗的预算 | 级别 | 语义 |
|---|---|---|---|---|
| 1 h 且 5 m | 14.4 | 2% / 1 h | P1 | 快烧,马上叫人 |
| 6 h 且 30 m | 6 | 5% / 6 h | P2 | 慢烧,开单跟进 |
| 3 d 且 6 h | 1 | 10% / 3 d | P3 | 趋势性劣化,进周会 |
两个好处,都是「阈值告警」给不了的:
- 阈值随流量自动缩放:低峰期同样 10 个错误占比更高,BR 自动变敏感;高峰期不会因为绝对数变大而误报。
- 误报天然收敛:告警响的时刻,一定是「预算真的在快速消耗」的时刻,而不是「某个指标碰到了某个数字」。
代价是需要 SLO 与预算先成立(公式、窗口选择与预算策略的主场见 50.7 SLO 与错误预算),而且要区分「快速燃烧」(叫人)与「缓慢燃烧」(开单),否则会用 P0 的噪音处理 P3 的问题。
runbook:一条告警的说明书
五段,缺一段就不算 runbook:
- 这条告警什么意思:一句话 + 阈值 + 窗口(「过去 1 小时结算 5xx 占比 > 1.44%」)。
- 怎么确认:三条可直接复制执行的查询(指标查询、trace 查询、日志查询),带好过滤条件。
- 怎么止血:按顺序的动作,每条写清「预期效果 + 风险 + 怎么回退」——回滚上一个版本 / 扩容 / 限流 / 摘掉某个实例或地域 / 切流到备用集群。
- 怎么定位根因:指向具体看板与依赖图,而不是「检查系统状态」。
- 找谁:owner、升级路径、以及「多久没解决就升级」。
验收标准只有一条:一个没做过这个服务的人,照着 runbook 能在 15 分钟内止血。做不到,那它只是「告警描述」。
反模式:runbook 里写「联系相关负责人」「检查服务是否正常」;三个月没演练过的 runbook(里面的命令早就失效);runbook 放在告警消息的链接里,但链接挂了。
降噪四件套
# Alertmanager:分组 + 分级路由 + 抑制
route:
receiver: im
group_by: [alertname, cluster]
group_wait: 30s
group_interval: 5m
repeat_interval: 4h
routes:
- matchers: [severity="P0"]
receiver: pager
continue: true
inhibit_rules:
# 机器挂了,就别再报这台机器上所有服务的 P2/P3
- source_matchers: [alertname="NodeDown"]
target_matchers: [severity=~"P2|P3"]
equal: [cluster]
💡
matchers语法自 Alertmanager 0.22 起可用(旧版是match:加key: regex)。升级前先确认版本,否则配置会被静默忽略——这本身就是一次「告警没响」。
| 手段 | 解决什么噪音 | 误配后果 |
|---|---|---|
分组 group_by | 一次故障 500 条告警 → 1 条 | 分组键太粗,不同故障被合并,第二个故障被掩盖 |
抑制 inhibit_rules | 上游挂了,下游 30 个服务的依赖告警全部静音 | 规则写反 → 真故障被吞掉 |
静默 silence | 发布窗口、演练期、已知维护 | 忘记设过期时间 → 真故障期间恰好在静默 |
延迟 for | 毛刺、瞬时抖动 | 设太长 → 慢发现,SLO 已经烧掉一半 |
| 去重/聚合 | 同一指纹 N 分钟内只发一次 | 掩盖「重复发生」,于是没人去修根因 |
| 分级路由 | P0 打电话、P3 只进工单 | 全量电话 = 没有电话 |
数字:一次机房级故障,未分组时下游 300 个服务各报 5 条 = 1500 条通知;按 alertname + cluster 分组后 1–3 条。分组的本质是把「一个故障」还原成「一条消息」。
规则上线前:先过三关
# 1) 配置与规则语法
promtool check config /etc/prometheus/prometheus.yml
promtool check rules /etc/prometheus/rules/*.yml
# 2) 规则单测:给定序列,断言告警按预期触发(放进 CI)
promtool test rules /etc/prometheus/rules_test.yml
# 3) 路由(装了 amtool 才有)
amtool check-config /etc/alertmanager/alertmanager.yml
单测文件长什么样、怎么断言,见 50.7 SLO 与错误预算 的多窗口规则一节。
规则单测的价值:改阈值、改窗口、改 for 时,不用等线上演练,CI 里就能看到“该响的没响 / 不该响的响了”。没有单测的告警规则,等于没测过的代码。
告警质量怎么量
| 指标 | 怎么算 | 健康信号 |
|---|---|---|
| 可行动率 | 处理后被确认「确实需要动作」的比例 | > 80% |
| 每周告警数 | 按级别统计 | P0/P1 数量稳定,不随发版暴涨 |
| MTTA / MTTR | 从告警到认领 / 到恢复 | 趋势下降 |
| 重复告警 TOP10 | 同一 alertname 反复响 | 反复响 = 根因没修,应该开技术债 |
| 静默数量与过期率 | 当前静默数 / 已过期的比例 | 静默越少越好 |
| 盲区 | 事后复盘里「没有告警」的事故 | 每个都补一条症状告警 |
⚠️ 最危险的信号不是「告警太多」,是**「有事故但没有告警」**:它说明监控假设错了,而不是阈值错了。
单条告警怎么设计是本节的边界;告警的存量清理、增量准入与定期审计见 56.4 告警治理,值班排班、升级路径与交接见 56.3 on-call (值班 / 升级路径 / runbook)。
反模式
| 反模式 | 后果 |
|---|---|
| 对原因告警(CPU、GC、单机磁盘) | 半夜被叫醒、用户无感;三个月后全组静音 |
| 一条告警塞多个阈值、多个含义 | 收到不知道该怎么办,只能去群里问 |
| 没有 owner | 响一整晚没人认领 |
| 没有 runbook | 值班新人从零排查,MTTR 由运气决定 |
| 静默不设过期 | 真故障恰好在静默期,且没人记得 |
| 固定错误率阈值 | 低峰误报、高峰漏报(用燃烧率解决) |
| 告警依赖日志关键字 | 文案一改就失灵,且日志采样后漏报 |
| 把人当监控(盯大屏) | 没人盯的时间就是盲区 |
| 告警只发不闭环 | 没有认领、没有复盘,同一告警年年响 |
自检
Q1 CPU 使用率 90% 该不该发 P0?为什么?
▸ 答案:不该。CPU 是原因类信号,90% 可能完全不影响用户(余量、短时突发、可压缩的批处理)。P0 的定义是「用户已经在受苦」。正确做法:CPU 上看板;真正的 P0 是「错误率/SLO 破了」,那条告警响的时候,看板上 CPU 与延迟的拐点会告诉你因果。例外:如果这个资源越界后不可逆(磁盘写满、证书过期),它就是「未来症状」,要告警,并且消息里写「还剩多久」。
Q2 SLO 99.9%,1 小时窗口燃烧率 14.4 是什么概念?
▸ 答案:错误预算是 43.2 分钟/月。BR = 14.4 意味着以 14.4 倍速消耗预算,1 小时烧掉 2%(约 52 秒额度),对应错误率约 1.44%。这个量级下必须马上有人看,所以配 P1;而 6 小时 BR = 6(烧 5%)是「慢烧」,开单跟进即可,不必半夜叫人。
Q3 上游数据库挂了,下游 30 个服务同时报「连接超时」,怎么让值班只收到一条?
▸ 答案:分层处理。① 分组:group_by: [alertname, cluster],把同名告警合并成一条;② 抑制:NodeDown/DBDown 作为 source,抑制下游所有依赖它的 P2/P3 告警(用 equal 匹配故障域);③ 只在「症状」层报警:入口错误率一条 P1,其余依赖告警降级为看板信息。三者叠加后,30 个服务只会产生 1 条「入口错误率」+ 1 条「DB 不可用」的根因告警。
Q4 runbook 的最低验收标准是什么?
▸ 答案:一个没做过这个服务的人,照着它能在 15 分钟内止血(不是定位根因)。所以它必须包含:这条告警的含义与阈值、三条可复制的确认查询、按顺序的止血动作(含预期效果、风险、回退方式)、根因排查入口、owner 与升级路径。写「联系相关负责人」「检查系统状态」的不算。
Q5 给告警加 for: 5m 有什么代价?
▸ 答案:晚 5 分钟发现。对「快速燃烧」类故障,5 分钟可能已经烧掉可观预算(99.9% SLO 下 1 小时 2% 的预算 ≈ 52 秒,5 分钟就是半条命)。所以 for 要按级别分:P0/P1 用 2–5 分钟并配合短窗口,P2/P3 用 30 分钟换更少的误报。用「多窗口多燃烧率」可以在不牺牲发现速度的前提下压掉毛刺。