告警治理:把 pager 从噪声源治回决策信号

英文版:English

Why

立项的时候,这个 pager 已经不能作为任何事情的证据了。Phase 0 的量化 baseline 说明了问题:367 条 alert rule 打着进 OpsGenie 的路由 label,散落在 30 个规则文件里;全局 OpsGenie channel 每天吞下约 960 条告警,其中约 85% 是 P3,原因很简单,不写 priority 时 P3 就是默认值。仅 infra 团队的 OpsGenie channel 就以每小时约 12 条的速率运行,一天约 290 条。

细看比总量更糟。一条 for: 0m 的 OOM-kill 规则,因为 18 个 pod 连锁 crash,三天产出 80 条告警;Top 3 规则在同一窗口合计 145 条 page。一条 ALB QPS 归零告警单次触发同时 fan-out 到 6 个 Slack channel,两分钟后又来一轮。一个 channel 在 6 分钟内收到 8 条 OpsGenie 告警。批处理调度器的超时告警对同一个卡住的 job 每小时 page 一次,三小时三条,问题只有一个;超出平均 28% 的 job 和超出 300% 的 job 拿到同一个 P1。采样那一天,仅这一族告警就产出 10 条以上 P1,每条都 @ subteam,没有一条带诊断链接。基础设施相关的告警 channel 有 12 个,其中 2 个早已无消息,也没人察觉。

我在年度自评里把代价写得很直接:低信号 page 造成的 alert fatigue,恰恰在响应质量最重要的时刻拖慢 triage。治理的第一性原则先于一切动作确定,并且始终只有一句:不能指导行为的 alert,关掉或降级。

How

方法是一棵决策树,逐条告警过判定,全部落在证据上:每个结论都能追溯到一条采集的真实告警样本或一段规则定义,而不是感觉。

flowchart TB A["告警触发"] --> B{"能否指导行为?"} B -- "不能" --> OFF["关掉 / 去掉 @ / 降级到信息类 channel"] B -- "能" --> SEV{"偏离基线多少?"} SEV -- "低于 2x" --> SKIP["不告警"] SEV -- "2x 到 4x" --> P2["P2:进 channel,不 @"] SEV -- "高于 4x" --> P1["P1:page 并 @ owner"] P1 --> Q["质量阶梯:预填链接 → 附 runbook → 自动修复"] OFF --> R["唯一 primary channel + dedup alias + owner label"] P2 --> R P1 --> R

审计定位了四个结构性根因:进 OpsGenie 的 label 是一行代码的免费开关,没有任何准入门槛;Alertmanager 里多条 continue: true 的重叠路由,把一条告警扇出到多个 receiver 和 channel;所有 receiver 都没配 dedup alias,同一问题每次重新 firing 都生成一条新告警;还有一条旁路脚本直接向 OpsGenie 写入,priority 硬编码 P3,对所有路由和 inhibition 完全不可见。

执行按阶段推进,测量先于改动。Phase 0 建 baseline dashboard,也就是上面那组数字。Phase 1 是 config 止血,打包成两个可 review 的 PR,带 lint 预检、分步 apply 和回滚路径:给最吵的规则加去抖窗口(for: 0m 改 10m);加一道硬准入门槛,只有 P1/P2 才允许进 OpsGenie;给批处理告警按偏离倍数分级;所有 receiver 加 dedup alias 并开 update_alerts,重复触发变成计数递增而不是再 page 一次。Phase 2 把共享同一个 OpsGenie API key 的 4 个 receiver 拆成按团队独立的 integration,合并冗余路由,补齐因果明确的 inhibition 链,配置 auto-close 策略。Phase 3 把修复固化为结构:旁路脚本改道走 Alertmanager,每条规则强制携带 team ownership label。每个阶段都对照 baseline 设验收门禁:OpsGenie 日总量 960 先降到 400 以下再到 200 以下;Top 3 噪声源从 3 天 145 条降到 40 以下再到 15 以下;P3 占比从 85% 降到 50% 以下再到 30% 以下;oncall 从盯 12 个 channel 收敛到 3 个。同一轮治理还把部署审批这类 toil 委托了出去:两天 11 条 @ infra oncall 的审批请求全部来自 preprod 环境,而系统里已存在的自动通过路径证明 infra 审批在这里不产生任何价值,于是下放给 team lead。

为什么告警会腐化

这个项目的分析章,也是我认为真正的核心,给出的是机制而不是坏规则清单。告警系统是一个没有外部做功就必然熵增的系统,审计材料恰好完整展示了为什么。

激励是不对称的。加告警便宜且可见:出过一次事故,加一行 label 就把新规则路由进 pager,没有任何准入门槛,作者还显得尽责。367 条 paging 规则就是这样积累出来的。删告警有风险且不可见:删错了要为漏掉的故障负责,删对了则什么可观测的事情都不会发生。于是流量只朝一个方向走。

与此同时,每条规则的语义随着脚下系统的演化静默失效,而失效不产生任何信号。审计就是这种现象的目录:一条 90% 阈值的节点内存告警,这个阈值诞生于 90% 还不是 Kubernetes 节点正常水位的年代;一条 300Mbps 的流量 page,但流量高本身不是故障,oncall 看了也无事可做;一条 QPS 归零告警,建立在"流量永远存在"的假设上,对本来就没有请求的端点持续误报。priority 以同样的方式腐化:P3 是无人审视的默认值,85% 的告警挤在同一档,这个字段不再携带任何信息,轻微偏差和严重故障以完全相同的方式 page。连拓扑本身也在腐化:12 个 channel 里 2 个已经死掉,系统还长出了绕开自身治理的旁路。这一切不是任何人的失误。在一个加告警免费、删告警受罚、失效无声的系统里,这就是默认轨迹。

治理完如何预防:反熵机制

一次性清理只是把熵拉回低点,防止复发需要持续做功的结构。从这个项目的素材里建立或明确了三类:其一是准入控制,OpsGenie 前面的 P1/P2 门槛、每条规则强制的 ownership label,以及规划中的 CI lint,拒绝任何不带 runbook_url 的 paging 规则;其二是常设测量,Phase 0 的 baseline dashboard 保留为长期的 alert SLO dashboard,外加自动化的 weekly toil report 定期发到工程 channel,让 pager 质量本身成为被监控的指标;其三是 review 回路,按 owner 分配、带明确 severity 阈值的季度告警评审,这一条我作为团队级流程向上提了出去。设计意图是让腐化必须对抗系统,而不是搭系统默认值的便车。

Takeaways

  • 告警系统服从热力学第二定律:没有持续的外部做功就必然积累噪声,因为加告警便宜且可见,删告警有风险且不可见。治理就是把这份功制度化。
  • 清理改变的是状态,准入控制改变的是速率。pager 前面那道门(priority 门槛、owner、runbook)比任何一次大扫除都值钱。
  • 把 pager 本身变成 SLO 对象。告警总量、priority 分布、dedup 比例不上 dashboard,它们的退化就不可见,而这正是熵第一次溜进来的方式。