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 趋势性劣化,进周会

两个好处,都是「阈值告警」给不了的:

  1. 阈值随流量自动缩放:低峰期同样 10 个错误占比更高,BR 自动变敏感;高峰期不会因为绝对数变大而误报。
  2. 误报天然收敛:告警响的时刻,一定是「预算真的在快速消耗」的时刻,而不是「某个指标碰到了某个数字」。

代价是需要 SLO 与预算先成立(公式、窗口选择与预算策略的主场见 50.7 SLO 与错误预算),而且要区分「快速燃烧」(叫人)与「缓慢燃烧」(开单),否则会用 P0 的噪音处理 P3 的问题。

runbook:一条告警的说明书

五段,缺一段就不算 runbook:

  1. 这条告警什么意思:一句话 + 阈值 + 窗口(「过去 1 小时结算 5xx 占比 > 1.44%」)。
  2. 怎么确认:三条可直接复制执行的查询(指标查询、trace 查询、日志查询),带好过滤条件。
  3. 怎么止血:按顺序的动作,每条写清「预期效果 + 风险 + 怎么回退」——回滚上一个版本 / 扩容 / 限流 / 摘掉某个实例或地域 / 切流到备用集群。
  4. 怎么定位根因:指向具体看板与依赖图,而不是「检查系统状态」。
  5. 找谁: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 分钟换更少的误报。用「多窗口多燃烧率」可以在不牺牲发现速度的前提下压掉毛刺。

关联