值班群里一天弹出几百条告警,真出事的那条常常被埋在噪声里,翻十分钟才捞出来。复盘一看,真正影响用户的只有一小时,其余全是磁盘使用率超了 5%、某台机器 CPU 抖了一下这类没人会动手的消息。
问题的根源多半不在监控工具本身,而在报警对象的选取。多数团队的告警池是逐年“长”出来的:每次故障加一条规则,规则只增不减,告警数量自然和故障数量彻底脱钩。Google SRE 团队给过一个很硬的判据:处理一次线上事件(含根因分析、修复、写复盘)平均要花 6 小时,因此每个 12 小时值班班次最多承载 2 次事件;他们把「每班呼叫次数 < 2、每日工单数 < 5」当成可量化的运维过载指标。
按这个标准回看自己的告警池,绝大多数团队超标十倍以上。下面按「分诊、定标准、压扇出、加度量」四步给出治理路径,所有参数都来自 Google SRE 与 Prometheus 官方文档,可以直接抄。
一、先分诊:把「原因告警」从呼叫降到看板
SRE Book 第 6 章把这件事说得很清楚:监控系统要回答两个问题——what's broken(什么坏了)和 why(为什么坏)。前者是症状,后者是原因。接下来那句才是重点:
应该把多得多的精力花在捕捉症状上,而不是原因;对于原因,只关心那些非常确定、非常迫近的原因。
对应的告警设计就是:症状类告警进呼叫通道,原因类告警降级为看板或工单。 典型的原因是 CPU 高、内存高、磁盘使用率、连接数、Pod 重启次数、队列积压。这些指标涨起来的时候用户可能毫无感知(批处理任务就是会让 CPU 冲到 90%),而它们跌下去也不代表故障恢复。
真正的症状是用户能感受到的东西。SRE Book 给的四个黄金信号是:延迟、流量、错误、饱和度。
如果只能量四个面向用户的指标,就聚焦这四个。
一条告警该不该进呼叫通道,SRE Book 提供了四个自检问题,值得打印出来贴在评审流程里:
- 这条规则检测的问题,是不是「既紧急、又可行动、并且正在或即将对用户可见」?
- 我将来能不能明知它是无害的而忽略它?如果能,为什么会有这种场景,怎么消除?
- 我能对它采取行动吗?这行动紧急吗,还是可以等到早上?能否安全自动化?是长期修复还是临时绕过?
- 这个问题是不是已经在叫别人了,导致至少有一个呼叫是多余的?
第 4 条是抑制机制的人肉版本,第 2 条则直接判了「阈值型基础设施告警」的死刑。
还有一条容易被忽略的边界:
除非是在做范围极窄的安全审计,否则绝不应该仅仅因为「好像有点不对劲」就触发告警。
SRE Book 拿 Bigtable 的过度告警做过一次公开复盘,原文的描述是:很多呼叫并不紧急,因为都是基础设施里早已理解的问题,响应要么是机械式的,要么根本没人响应。这类告警的存在本身就在训练值班人忽略告警。
当呼叫过于频繁,员工会怀疑、扫一眼甚至直接忽略进来的告警,有时连一个真告警也被噪声掩盖掉。
二、定标准:用错误预算燃烧率替代静态阈值
分诊解决「该不该报」,第二步解决「达到什么程度才报」。静态阈值(比如错误率 > 1%、P99 > 500ms)的毛病在于:它和你的可靠性目标没有任何数学关系,1% 是拍出来的,而且不看持续时间。
Google SRE Workbook 第 5 章给出了替代方案,直接定义燃烧率:错误预算被消耗的速度,相对于 SLO 的倍数。燃烧率 1 表示按这个速度正好在周期结束时耗尽预算;燃烧率 10 表示只用十分之一的时间耗尽。
| 燃烧率 |
99.9% SLO 对应错误率 |
预算耗尽时间 |
| 1 |
0.1% |
30 天 |
| 2 |
0.2% |
15 天 |
| 10 |
1% |
3 天 |
| 1000 |
100% |
43 分钟 |
有了这张表,「告警阈值」就变成了一句可讨论的话:30 天预算的 2% 在 1 小时内烧掉,对应燃烧率 14.4。 这是 Workbook 推荐的分页起始参数,完整的三档如下:
| 严重级别 |
长窗口 |
短窗口 |
燃烧率 |
消耗的预算 |
| 呼叫(Page) |
1 小时 |
5 分钟 |
14.4 |
2% |
| 呼叫(Page) |
6 小时 |
30 分钟 |
6 |
5% |
| 工单(Ticket) |
3 天 |
6 小时 |
1 |
10% |
注意「长短双窗」这个设计。只用长窗口会有一个坏毛病:故障在 10 分钟内结束了,告警却还要继续响到窗口滑出去为止。Workbook 给出的经验法则是短窗口取长窗口的 1/12。它的效果原文写得很具体:1 小时窗口配 5 分钟短窗,告警只在预算烧掉 2% 后才触发,但故障停止 5 分钟后就不再响,相比之下单窗口方案要响满 1 小时才复位。
反过来,只有短窗口又会漏掉慢速失血,所以两个窗口必须同时超过阈值才触发。落成 PromQL 大致是这样(slo_errors / slo_requests 是业务侧导出的两个计数器,记录规则按 10 分钟窗口算比率):
groups:
- name: slo-burn-rate
rules:
- record: job:slo_errors_per_request:ratio_rate5m
expr: |
sum(rate(slo_errors[5m])) by (job)
/
sum(rate(slo_requests[5m])) by (job)
- alert: ErrorBudgetFastBurn
expr: |
job:slo_errors_per_request:ratio_rate1h > (14.4 * 0.001)
and
job:slo_errors_per_request:ratio_rate5m > (14.4 * 0.001)
for: 2m
labels:
severity: page
annotations:
summary: "{{ $labels.job }} 错误预算快速燃烧"
runbook_url: "https://runbooks.example.com/fast-burn"
- alert: ErrorBudgetSlowBurn
expr: |
job:slo_errors_per_request:ratio_rate3d > 0.001
and
job:slo_errors_per_request:ratio_rate6h > 0.001
labels:
severity: ticket
annotations:
summary: "{{ $labels.job }} 错误预算持续失血"
把阈值写成 14.4 * 0.001 而不是 0.0144,是为了让读者一眼看出这个数字的来源。
两处必须知道的副作用。第一,多档位会互相重叠:1 小时内烧掉 10% 预算,同时也意味着 6 小时内烧了 5%、1 小时内烧了 2%,三档会同时满足。Workbook 原文明确提醒这一点,需要在通知层做抑制,否则一次故障喊三遍。第二,Workbook 自己的优缺点表里把「参数多、规则难维护」列为这套方法的主要缺点。
顺带说清一个常见误用:Workbook 里 Approach 1 那种「只对错误率设阈值」的做法,原文的批评是可能每天收到多达 144 条告警、一条都不处理,而 SLO 依然达标。判断告警好坏的第一性标准是它是否能改变你的行动,而不是它响了多少次。
如果服务已经有 24 小时或 3 天的燃烧率档位,Workbook 示例配置里还给了 24 小时配 2 小时、燃烧率 3 的工单档变体,比上面的 3 天档更早发现慢性问题,可以按团队响应能力取舍。
三、压扇出:分组、抑制、静默三件套
告警定得再准,一次机房间断也能在 30 秒内触发上千条规则。Prometheus 官方文档把边界划得很清楚:告警规则擅长判断「现在什么坏了」,但它不是完整的通知方案,汇总、通知限速、静默、告警依赖这些事要交给 Alertmanager。
分组(Grouping) 解决的就是上面那种雪崩。Alertmanager 官方文档给的定义是:把性质相近的告警合并成一条通知,集群里几百个实例同时连不上数据库时,你只想收到一条呼叫,同时还能看清哪些实例受影响。
分组的节奏由这些参数控制,默认值如下:
| 参数 |
默认值 |
作用 |
group_wait |
30s |
新告警组等多久发出第一条通知,给同组告警留合并时间 |
group_interval |
5m |
同一告警组内新增告警后,隔多久再发一次通知 |
repeat_interval |
4h |
告警未恢复时,重复通知的最小间隔 |
resolve_timeout |
5m |
告警不带 EndsAt 时,多久后判定为已恢复 |
continue |
false |
路由匹配后是否继续匹配同级后续路由 |
三个容易踩的细节:
- 子路由会继承父路由的三个间隔,不写就用父级的值。把
group_wait 调大能减少通知条数,但也会推迟第一条通知,对真正紧急的告警要单独设小值。
repeat_interval 应该是 group_interval 的整数倍,否则会被向上取整到下一个倍数。想做「4 小时重复一次」却把 group_interval 设成 50 分钟,实际会变成 4 小时 10 分钟。
- 如果
repeat_interval 比启动参数 --data.retention 还长,通知会在数据保留期结束时重复,而不是按你写的间隔。
group_by 的取值需要按服务形态设计。按 cluster + alertname 分组适合实例众多的场景;官方的极端选项是把 '...' 作为唯一标签名,那样会完全关闭聚合,每个告警实例各发一条,通常只在调试期用。
抑制(Inhibition) 处理的是「上游已经坏了,下游别喊了」。官方定义:当某些告警已经触发时,压制另外一些告警的通知。经典的配置是「整个集群不可达」触发时,静默该集群下所有其他告警。
inhibit_rules:
- source_matchers: [severity = "critical", alertname = "ClusterUnreachable"]
target_matchers: [severity =~ "warning|critical"]
equal: [cluster]
equal 里列的标签必须在源告警与目标告警上取值相同,抑制才生效。官方文档还补了一条容易误判的语义:缺失的标签与值为空的标签被视为同一回事,所以如果 equal 里列的标签在源和目标上都不存在,规则反而会生效。多个机房共用一套配置文件时,这一点会让抑制意外跨机房命中。
回到第二节留下的那个尾巴:多档燃烧率会同时满足,1 小时档与 6 小时档一起叫。办法是给规则加一个区分窗口的标签,再用一条抑制规则让快档压住其余档位:
# 规则侧:1 小时档加 burn_window: "1h",6 小时档加 burn_window: "6h"
inhibit_rules:
- source_matchers: [severity = "page", burn_window = "1h"]
target_matchers: [severity = "page"]
equal: [job]
这样一次故障只会产生一条呼叫,而不是三档各喊一遍。注意 equal: [job] 不能省:省掉之后 A 服务的快档会去压住 B 服务的告警。
静默(Silence) 是人工按 matcher 临时静音一段时间,在 Alertmanager 的 Web 界面上配置,跟抑制的区别是它不由告警状态驱动,靠人来开和关。发布窗口、已知的维护作业用它。注意静默不设过期提醒就是个定时炸弹,建议约定「静默必须写在变更单里、必须带结束时间」。
另外,Alertmanager 还有个兜底能力值得打开:按告警名限制并发活跃告警数(启动参数 --alerts.per-alertname-limit)。达到上限后新告警被丢弃,已有告警的心跳继续处理。某条规则因为配置失误瞬间产生几万个实例时,这个开关能防止通知通道被打爆。被丢弃的总数可以从 alertmanager_alerts_limited_total 指标看到,这个指标不为零本身就说明有规则写错了。
最后提一句部署形态:Alertmanager 支持集群做高可用,但不要在 Prometheus 与 Alertmanager 之间做负载均衡,正确的做法是把 Prometheus 指向所有 Alertmanager 实例。做错会出现「同一告警被两个实例各发一次」。
四、加度量:用「每班响应数」给治理闭环
前三步做完,还需要一个数字来衡量效果,否则噪声会在几个月后重新长回来。
用 SRE Book 第 11 章的指标:每班呼叫次数。他们的经验值是每班 < 2 次、每日工单 < 5 条。他们还有一句判断很值得引用:呼叫的分布应该非常平坦、中位数大概为 0;如果某个组件每天都触发告警,那么中位数 > 1,迟早有一天它会和别的故障撞在一起,把班次推到不可承受的负载上。
第二个数字是告警事件比。原文的要求是:
系统性地一次事件产生多条告警的噪声告警,应该被调整到接近 1:1 的告警/事件比。
做法就是上一节的分组与抑制;度量方式是看「一次事件期间通知条数」的分布,尾部较长的那些规则就是要改的对象。
第三个动作是让每条告警自带上下文。Prometheus 的 annotations 字段就是为此存在的,官方文档明确说它适合放较长的信息,比如告警描述或 runbook 链接,而且值支持模板变量,$labels 取告警实例的标签、$value 取表达式求值结果:
annotations:
summary: "{{ $labels.job }} 5 分钟错误率 {{ $value | humanizePercentage }}"
runbook_url: "https://runbooks.example.com/slo-fast-burn"
SRE Book 第 11 章对可行动性的表述比第 6 章更直接:所有呼叫类告警都必须是可行动的。判断标准可以简化成一句:值班人看到这条告警后,能不能立刻知道第一步该敲什么命令。敲不出来,这条告警就还不合格。
还剩两个 Prometheus 侧的参数,用来收掉「抖动」和「假恢复」:
for:可选,不写时告警在第一次求值命中就进入 active 并触发;写了则要求表达式在这段时间内持续成立才转为 firing,中间状态叫 pending。给静态阈值类的规则加 for 能挡掉瞬时尖刺。
keep_firing_for:可选,在最后一次满足触发条件之后,继续维持 firing 一段时间。官方列出的用途正是防止告警抖动、以及因数据缺失导致的假恢复。
排查期可以直接用 ALERTS 序列判断状态:Prometheus 会为 pending 与 firing 的告警生成 ALERTS{alertstate="pending|firing", ...} 序列,值为 1,状态结束即标记为 stale。写「哪些告警长期处于 pending 从未 firing」这类巡检时,它比翻日志可靠。
五、适用边界与下一步
这套方法解决的是「告警是否值得叫人」,不是「监控是否覆盖完整」。
它不适用的两类场景:
- 流量极小的服务。 燃烧率本质是比率,分母很小时短窗口的样本量不足,分子有一两个错误就能算出很高的比率,会持续抖动。这类服务更适合用绝对错误数(比如「5 分钟内失败请求数 > N」)配合
for 来做门限,燃烧率只留长窗口一档作为补充。这一点是基于公式本身的工程判断,不是官方结论,落地时要拿自己服务的真实流量验证一遍。
- 还没有 SLO 的服务。 燃烧率告警的前提是有一个明确的 SLI 与目标值。这一步没做,先补 SLI 定义再回来。
下一步可以从三件小事开始,按投入从小到大:
- 导出全部告警规则,按「症状/原因」两列做一次人工分诊,标出所有原因类告警的当前去向。这一步不需要改任何代码。
- 统计上周每班的呼叫次数与告警事件比。两个数字出来后,改进的优先级自然就有了。
- 挑一条「既紧急、又可行动」的服务,定义它的 SLI,按上面的三档参数配一次多窗燃烧率告警,与旧规则并行跑两周,对比触发次数与真阳性比例,再决定是否替换。
治理告警不是把规则删掉,而是让每一条留下来的规则都回答得了一个问题:它响的时候,会有人立刻去做一件具体的事。如果你也在梳理团队的监控与告警体系,云栈社区 里还有更多 SRE 实战内容可以继续翻。