抛弃所有基于 CPU、内存利用率的静态阈值告警。基础设施指标飙升不代表业务受损,海量无意义的 Warning 只会带来 On-Call 疲劳,最终导致真实的 P0 故障被淹没。本文直接给出基于 Prometheus (v2.45.0) 的多时间窗口(Multi-Window)Error Budget 燃烧率告警落地代码,将告警噪音降低 80% 以上,实现仅在用户体验真实受损时才触发 Page。
为什么静态阈值与基础设施告警一定会摧毁 On-Call 体系?
在近期的某次系统重构排查中,我看到业务线的 Prometheus 里堆满了这类告警规则:
- alert: NodeCPUUsageHigh
expr: 100 - (avg by(instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 85
for: 3m
这种告警逻辑在生产环境简直是一场灾难。Go Runtime GC、大促期间的瞬时突发流量、甚至是某个后台定时批处理任务,都会导致 CPU 短暂飙升。结果就是 On-Call 工程师在深夜被频繁唤醒,看一眼面板发现“已经恢复了”,默默点个 Ack 接着睡。 狼来了的故事上演三次后,墨菲定律就会生效: 当 CPU 飙升是因为死锁或真实的流量雪崩引起时,疲惫的工程师习惯性地认为“等 5 分钟自己就降下去了”,从而错失最佳止血时机。
监控的本质是度量系统提供服务的质量,而不是度量系统的健康状态。用户根本不在乎你的 CPU 是 90% 还是 10%,他们在乎的是:请求有没有报错(Error Rate)?响应够不够快(Latency)?
告别直觉:SLI 建模与 Error Budget 燃烧率(Burn Rate)
真正的防御性告警设计,必须围绕 SLO(服务等级目标)展开。假设我们定了一个核心 API 的 SLO 为 99.9%(30 天为一个周期),这意味着我们在 30 天内允许的错误预算(Error Budget)为 0.1%。
如果错误率恰好保持在 0.1%,那么预算将在 30 天后耗尽,此时的燃烧率(Burn Rate)就是 1。
如果错误率飙升到 1%,是允许值的 10 倍,燃烧率就是 10。预算将在 30天 / 10 = 3天 内耗尽。
为了兼顾告警的灵敏度(快速发现大范围故障)与精确度(不被瞬时抖动误导),业界标准做法是引入 Multi-Window(多时间窗口)判断逻辑: 必须同时满足长窗口(评估故障规模)和短窗口(确认故障正在持续)的燃烧率阈值,才触发告警。
Multi-Window 燃烧率告警在 Prometheus 的落地实战
不要在告警规则里写几百个字符的复杂 PromQL,这会导致 Prometheus 每次评估告警时产生严重的 CPU 开销和查询超时。必须通过 Recording Rules 提前聚合 SLI。
1. 定义 SLI 聚合规则 (Recording Rules)
基于 Prometheus v2.45.0,我们为核心服务配置以下 Recording Rules。这里以 5m, 30m, 1h, 2h, 6h 五个窗口的错误率为例:
groups:
- name: sli_http_error_rate
interval: 1m
rules:
# 预聚合 5 分钟窗口错误率
- record: job:http_requests:error_rate5m
expr: >
sum by(job) (rate(http_requests_total{status=~"5.."}[5m]))
/
sum by(job) (rate(http_requests_total[5m]))
# 预聚合 1 小时窗口错误率
- record: job:http_requests:error_rate1h
expr: >
sum by(job) (rate(http_requests_total{status=~"5.."}[1h]))
/
sum by(job) (rate(http_requests_total[1h]))
# (30m, 2h, 6h 的规则同理,此处省略以保持排版整洁)
2. 构建多窗口燃烧率告警 (Alerting Rules)
目标:SLO = 99.9%(允许错误率 0.001)。 级别分类:
-
P0 (Page, 电话呼叫): 14.4x 燃烧率(预算将在 2 天内耗尽),长窗口 1h,短窗口 5m。
-
P1 (Ticket, 企微/钉钉/Jira): 6x 燃烧率(预算将在 5 天内耗尽),长窗口 6h,短窗口 30m。
groups:
- name: slo_burn_rate_alerts
rules:
# P0 级别告警:短时间大量燃烧,必须立即人工介入 (14.4x)
# 条件:(1h错误率 > 0.001 * 14.4) AND (5m错误率 > 0.001 * 14.4)
- alert: HighErrorRate_Page
expr: >
(
job:http_requests:error_rate1h > (1 - 0.999) * 14.4
and
job:http_requests:error_rate5m > (1 - 0.999) * 14.4
)
labels:
severity: page
annotations:
summary: "High Error Budget Burn Rate (14.4x)"
description: "Service {{ $labels.job }} error budget is burning rapidly. 2% of 30d budget consumed in 1h."
# P1 级别告警:慢性燃烧,不需要半夜起床,但上班后需首要处理 (6x)
# 条件:(6h错误率 > 0.001 * 6) AND (30m错误率 > 0.001 * 6)
- alert: HighErrorRate_Ticket
expr: >
(
job:http_requests:error_rate6h > (1 - 0.999) * 6
and
job:http_requests:error_rate30m > (1 - 0.999) * 6
)
labels:
severity: ticket
annotations:
summary: "Significant Error Budget Burn Rate (6x)"
3. Alertmanager (v0.25.0) 的防御性路由配置
有了精准的标签打标(severity: page vs severity: ticket),Alertmanager 的路由配置就能彻底隔离噪音,保护 On-Call 人员的睡眠。
route:
receiver: 'default_slack'
group_by: ['job', 'alertname']
group_wait: 30s
group_interval: 5m
repeat_interval: 4h
routes:
# 彻底静默没有任何业务影响的基础设施 Warning
- matchers:
- severity="warning"
receiver: 'blackhole'
# 真实的 P0,触发 PagerDuty/电话
- matchers:
- severity="page"
receiver: 'pagerduty_voice'
group_wait: 10s # P0 不等聚合,立刻发
repeat_interval: 1h
# P1 生成 Jira 工单或发工作群,不弹窗、不打电话
- matchers:
- severity="ticket"
receiver: 'jira_and_slack'
经过这套重构,系统告警量通常能直接断崖式下跌。你不再需要去猜测“MySQL Load 飙到 50 到底要不要起夜”,只要 HighErrorRate_Page 没响,业务吞吐和正确率就是稳的,天塌下来也等天亮再排查。
常见问题
Q1: 对于异步消费 MQ 的后台任务,没有 HTTP 状态码,怎么定义 SLI 和燃烧率?
不能用队列深度(Queue Depth)作为核心 SLI,因为消费速率和产生速率是动态变化的,队列深不一定影响业务最终时效。
应使用 端到端延迟 (End-to-End Latency) 或 处理成功率 (Pipeline Success Rate)。在入口处记录消息产生的时间戳,在消费者端计算 CurrentTime - ProduceTime。SLI 定义为:“99% 的消息必须在 5 分钟内处理完毕”。用 Histogram 抓取延迟,计算超出 5 分钟的比例作为“Error Rate”来计算燃烧率。
Q2: 触发了 Page,排查后发现是因为下游第三方 API 挂了导致的,我的 SLO 受损但不是我的锅怎么办?
这体现了容错架构的缺失。你的 SLI 衡量的是你对客户提供的服务质量,下游挂了导致你的用户报错,这就是你的问题。正确的做法不是把告警转给下游,而是进行防御性编程:为下游依赖增加 Circuit Breaker(断路器),并在降级策略中返回默认值或缓存值。只要成功执行了降级返回 200 OK,你的系统就守住了 SLO。
Q3: 每次发版发布(Deployment)时,Pod 重启总会引发瞬时的 5xx 错误,导致 5m 短窗口的燃烧率报警,怎么处理这种误报? 这是 Multi-Window 的典型副作用。解法有二:
-
联动 CI/CD 抑制: 部署流水线在开始前调用 Alertmanager API 注入一个带
matcher: service=XXX的 Silence,部署结束后删除。 -
调优短窗口容忍度: 如果系统实在做不到无损发布(比如没配置 K8s preStop hook 和优雅下线),可以将针对短窗口 5m 的判断改为
job:http_requests:error_rate5m_offset5m,或者直接在 PromQL 中附加部署事件的规避逻辑。但这治标不治本,根本解法还是治理应用层的优雅启停机制。