Prometheus 告警规则设计实战——什么样的告警有价值,什么样的是噪音
Prometheus 告警规则设计实战——什么样的告警有价值,什么样的是噪音
适读人群:SRE、后端工程师 | 阅读时长:约18分钟 | 核心价值:从告警设计原则到具体规则编写,让告警真正有意义,而不是让人麻木
我们团队曾经有一段时间,Slack 的 #alerts 频道每天能收到 400-500 条告警消息。
我做了个统计:这 400-500 条告警中,真正需要人处理的,大概 10 条左右。其余的要么是噪音(系统正常波动触发的),要么是"已知问题的告警"(大家都知道那个指标偶尔会高,但还没修),要么是重复告警(同一个问题触发了 10 条告警)。
时间长了,工程师开始自动屏蔽 #alerts 频道。有一次真正的生产故障,告警发了 40 分钟,没有人处理,因为大家已经习惯性地忽略了。
告警信噪比太低,是比没有告警更糟糕的状态。
这篇文章讲告警规则的设计——不只是语法,更重要的是设计思路。
告警设计的核心原则
原则一:告警应该是"需要人处理的"
每一条告警都应该能回答:"接到这条告警,值班工程师需要做什么?"
如果回答是"看一眼,没啥大问题,关掉"——这条告警就不应该存在。
告警应该对应可操作的响应(Actionable Response):
- 要么是需要立刻处理的故障
- 要么是需要在规定时间内处理的风险
- 绝不是"提供信息给你参考"的日报
纯信息性的通知,用 Dashboard,不要用告警。
原则二:症状 vs 原因
告警应该基于症状(用户感受到的问题),而不是原因(系统内部问题)。
反例:
# 错误的告警:基于内部原因
- alert: HighCpuUsage
expr: cpu_usage > 80
for: 5m
annotations:
summary: "CPU usage is high"CPU 高不一定意味着用户受影响,可能只是一个批处理任务在跑。
正确做法:
# 正确的告警:基于用户可感知的症状
- alert: HighErrorRate
expr: |
rate(http_requests_total{status=~"5.."}[5m])
/
rate(http_requests_total[5m]) > 0.01
for: 2m
annotations:
summary: "Error rate exceeded 1%"
description: "Service {{ $labels.service }} error rate is {{ $value | humanizePercentage }}"用户遇到 5xx 错误,这才是真正的症状。CPU 高只是可能的原因之一,应该作为诊断依据,而不是告警条件。
原则三:告警分级
不同严重程度的告警应该有不同的通知方式和响应时间要求:
| 级别 | 含义 | 通知方式 | 响应时间 |
|---|---|---|---|
| P1 Critical | 服务不可用,用户严重受影响 | 电话/PagerDuty 打给值班 | 立刻 |
| P2 High | 部分功能降级,用户受影响 | Slack @oncall + 短信 | 15 分钟内 |
| P3 Medium | 性能降级,尚可接受 | Slack 频道消息 | 1 小时内 |
| P4 Low | 需要关注的趋势 | 邮件日报 | 下一个工作日 |
Prometheus 告警规则语法
基本结构
# alerting-rules.yml
groups:
- name: service-availability # 规则组名称
interval: 30s # 评估间隔(默认继承全局设置)
rules:
- alert: ServiceDown # 告警名称
expr: up == 0 # 触发条件(PromQL)
for: 1m # 持续多久才触发(防抖)
labels:
severity: critical # 标签,用于路由告警
team: platform
annotations:
summary: "Service {{ $labels.job }} is down"
description: |
Service {{ $labels.job }} on instance {{ $labels.instance }}
has been down for more than 1 minute.
runbook_url: "https://wiki.company.com/runbooks/service-down"几个关键点:
for子句非常重要——如果不写for,指标一超过阈值就立刻触发告警,任何短暂的波动都会产生告警。写了for: 1m,表示条件需要持续 1 分钟才触发,可以过滤掉短暂抖动。labels里的标签用于 Alertmanager 的路由配置,把不同级别的告警路由到不同的接收者。annotations里写的是人类可读的信息,summary是短描述,description是详细描述,runbook_url是处理手册的链接(强烈建议每条告警都有 runbook)。
实用告警规则集合
HTTP 服务的 RED 指标告警
RED = Rate(请求速率)、Error(错误率)、Duration(延迟)
groups:
- name: http-red-metrics
rules:
# 错误率告警
- alert: HighHTTPErrorRate
expr: |
sum(rate(http_server_requests_seconds_count{status=~"5.."}[5m])) by (service, method, uri)
/
sum(rate(http_server_requests_seconds_count[5m])) by (service, method, uri)
> 0.05 # 5% 错误率
for: 2m
labels:
severity: high
annotations:
summary: "High HTTP error rate on {{ $labels.service }}"
description: |
Service {{ $labels.service }}, endpoint {{ $labels.method }} {{ $labels.uri }}
has error rate {{ $value | humanizePercentage }} for the last 2 minutes.
# P99 延迟告警
- alert: HighHTTPLatency
expr: |
histogram_quantile(0.99,
sum(rate(http_server_requests_seconds_bucket[5m])) by (service, le)
) > 2 # P99 超过 2 秒
for: 5m
labels:
severity: high
annotations:
summary: "High P99 latency on {{ $labels.service }}"
description: "P99 latency is {{ $value | humanizeDuration }}"
# 请求量突降(可能是服务挂了但没有 error)
- alert: LowRequestRate
expr: |
sum(rate(http_server_requests_seconds_count[5m])) by (service)
< sum(rate(http_server_requests_seconds_count[5m] offset 1h)) by (service) * 0.3
and
sum(rate(http_server_requests_seconds_count[5m] offset 1h)) by (service) > 1
for: 5m
labels:
severity: high
annotations:
summary: "Sudden drop in request rate for {{ $labels.service }}"
description: "Request rate dropped to less than 30% of 1 hour ago"JVM 相关告警
groups:
- name: jvm-alerts
rules:
# GC 停顿时间过长
- alert: HighGCPause
expr: |
rate(jvm_gc_pause_seconds_sum[5m])
/
rate(jvm_gc_pause_seconds_count[5m])
> 0.5 # 平均 GC 停顿超过 500ms
for: 5m
labels:
severity: high
annotations:
summary: "High GC pause time on {{ $labels.service }}"
description: "Average GC pause: {{ $value | humanizeDuration }}"
# 堆内存使用率过高(接近 OOM)
- alert: HighHeapUsage
expr: |
jvm_memory_used_bytes{area="heap"}
/
jvm_memory_max_bytes{area="heap"}
> 0.9
for: 10m
labels:
severity: critical
annotations:
summary: "Heap memory usage above 90% on {{ $labels.service }}"
description: |
Instance {{ $labels.instance }} heap usage is {{ $value | humanizePercentage }}.
OOM is imminent.
# 线程池耗尽告警
- alert: ThreadPoolNearExhaustion
expr: |
tomcat_threads_busy_threads
/
tomcat_threads_config_max_threads
> 0.9
for: 3m
labels:
severity: high
annotations:
summary: "Tomcat thread pool near exhaustion on {{ $labels.service }}"数据库连接池告警
groups:
- name: database-alerts
rules:
# 连接池使用率
- alert: DatabaseConnectionPoolNearExhaustion
expr: |
hikaricp_connections_active
/
hikaricp_connections_max
> 0.8
for: 5m
labels:
severity: high
annotations:
summary: "HikariCP connection pool near exhaustion"
description: |
Pool {{ $labels.pool }} is {{ $value | humanizePercentage }} utilized.
Current active: {{ $labels.active }}, max: {{ $labels.max }}
# 等待连接超时
- alert: DatabaseConnectionWaitHigh
expr: |
rate(hikaricp_connections_timeout_total[5m]) > 0.1
for: 2m
labels:
severity: critical
annotations:
summary: "High rate of DB connection timeouts"Alertmanager 配置:路由和去重
告警规则只是第一步,Alertmanager 负责对告警进行路由、去重、静默。
# alertmanager.yml
global:
resolve_timeout: 5m
slack_api_url: 'https://hooks.slack.com/services/xxx/yyy/zzz'
# 路由规则
route:
group_by: ['alertname', 'service', 'env']
group_wait: 30s # 收到第一条告警后等待 30s,聚合同组的告警
group_interval: 5m # 同组告警最多每 5 分钟发一次通知
repeat_interval: 4h # 4 小时后重复发送未解决的告警
receiver: 'slack-ops'
routes:
# P1 告警:直接打电话
- match:
severity: critical
receiver: 'pagerduty-critical'
group_wait: 0s # 立刻通知,不等待
continue: true # 匹配后继续匹配其他路由
# 分发到各个团队频道
- match:
team: payments
receiver: 'slack-payments'
- match:
team: platform
receiver: 'slack-platform'
receivers:
- name: 'pagerduty-critical'
pagerduty_configs:
- service_key: 'YOUR_PD_SERVICE_KEY'
description: '{{ range .Alerts }}{{ .Annotations.summary }}\n{{ end }}'
- name: 'slack-ops'
slack_configs:
- channel: '#alerts'
send_resolved: true
title: '{{ if eq .Status "firing" }}:fire:{{ else }}:white_check_mark:{{ end }} {{ .CommonAnnotations.summary }}'
text: |
{{ range .Alerts }}
*Alert:* {{ .Annotations.summary }}
*Description:* {{ .Annotations.description }}
*Severity:* {{ .Labels.severity }}
{{ if .Annotations.runbook_url }}*Runbook:* {{ .Annotations.runbook_url }}{{ end }}
{{ end }}
inhibit_rules:
# 如果服务整体 down,压制该服务的细粒度告警(避免告警风暴)
- source_match:
alertname: ServiceDown
target_match_re:
alertname: HighHTTPErrorRate|HighHTTPLatency
equal: ['service']inhibit_rules 非常重要——当一个服务整体不可用时,所有基于这个服务的细粒度告警都会被压制,否则一个故障可以产生几十条告警。
踩坑实录
踩坑一:for 设置太短导致告警噪音
有一次我把 HighHeapUsage 的 for 设成了 1m,结果每次 GC 后堆内存回收前的那段时间,都会触发一条告警,每天要收到几十条告警。
修改 for: 10m 之后,只有真正长时间高内存使用才会触发,告警数量降到每天 0-1 条。
原则:for 的时间一定要大于你可以接受的波动时间。
踩坑二:告警规则里的 PromQL 太复杂导致误判
我写过一个告警:
expr: |
sum(rate(errors[5m])) by (service) > 100
and
sum(rate(requests[5m])) by (service) > 1000逻辑是:错误数超过 100 且请求量超过 1000 才告警(避免低流量时的误判)。
但这有一个问题:sum(...) by (service) 和 sum(...) by (service) 做 and 运算,要求标签完全匹配。如果 errors metric 和 requests metric 的标签集合不完全相同,and 运算会返回空,告警永远不会触发。
用 on(service) 明确指定 join key:
expr: |
sum(rate(errors[5m])) by (service) > 100
and on(service)
sum(rate(requests[5m])) by (service) > 1000踩坑三:没有告警值班表,告警全打给一个人
我们最开始所有告警都打给 CTO(因为他的号码是第一个写进去的),久而久之他对告警也麻木了。有一次凌晨真正的故障,他以为又是误告警,看了一眼没处理,延误了半小时。
告警一定要有轮值机制,确保:
- 每个时段有人负责
- 告警打给当前值班人,而不是固定的一个人
- 超时未响应有升级机制(打给下一个人)
PagerDuty、OpsGenie、国内的各种值班工具都可以实现这个。
深度解析:SLO 驱动的告警设计
最先进的告警设计思路是基于 SLO(Service Level Objectives,服务等级目标)的告警,而不是基于指标阈值。
什么是 SLO?
SLO 是你对服务质量的承诺,比如:
- 过去 30 天,99.9% 的请求在 500ms 以内完成
- 过去 30 天,99.5% 的请求返回成功(非 5xx)
SLO 是用户视角的目标,而不是系统视角的指标。
基于 Error Budget(错误预算)的告警
如果你的可用性 SLO 是 99.9%,那么你每个月允许约 43 分钟的不可用时间(30天 × 24小时 × 60分钟 × 0.1% ≈ 43分钟)。这 43 分钟就是你的"错误预算"。
基于 Error Budget 的告警,不是告警"当前错误率超过 1%",而是告警"过去 1 小时消耗了本月 5% 的错误预算",或者"按当前速度,本月错误预算将在 X 小时后耗尽"。
这种告警的优势:
- 和 SLO 直接挂钩:告警告诉你的是"用户体验是否在下降",而不是某个内部指标超阈值了
- 紧迫性是清晰的:消耗了 80% 的月度错误预算,比"P99 超过 500ms"更能传达紧迫感
Prometheus 里实现 Error Budget 告警的 PromQL:
# 当前小时的 Error Rate
- record: job:request_error_rate:ratio_rate5m
expr: |
sum(rate(http_requests_total{status=~"5.."}[5m])) by (job)
/
sum(rate(http_requests_total[5m])) by (job)
# Error Budget 消耗速率告警
- alert: ErrorBudgetBurnRateHigh
expr: |
# 1小时窗口的错误率 > SLO 目标的 14.4 倍
# 14.4 = 1 / (1小时/720小时) * 某个告警阈值
job:request_error_rate:ratio_rate5m > (1 - 0.999) * 14.4
for: 2m
annotations:
summary: "Error budget burn rate too high for {{ $labels.job }}"
description: "At current rate, monthly error budget will be exhausted in {{ (1 / $value * 0.001 * 720) | humanizeDuration }}"这种设计方法来自 Google SRE 的实践,在大规模系统里被证明非常有效。
深度解析:告警 Runbook 的重要性
告警规则写好了,但如果没有 Runbook(操作手册),告警在半夜打来的时候,值班工程师还是会不知所措。
好的 Runbook 包含什么?
每条告警对应一个 Runbook 页面,内容包括:
# 告警:HighHTTPErrorRate
## 含义
service-name 的 HTTP 错误率超过 5%,表明大量用户请求失败。
## 可能的原因
1. 数据库连接池耗尽(最常见)
2. 上游依赖服务不可用
3. 代码部署引入 bug
4. 流量突增超过服务容量
## 诊断步骤
1. 检查 Grafana Dashboard,确认错误开始的时间点
- 如果和某次部署时间吻合,回滚部署
2. 检查数据库连接池指标(hikaricp_connections_active / max > 0.9)
3. 检查下游服务的错误率(Grafana → 依赖服务 Dashboard)
4. 查看服务日志里的 ERROR 级别日志
## 处理方法
- 数据库连接池问题:临时增大连接池上限,长期需要优化慢查询
- 下游服务问题:切换到降级逻辑,通知下游服务 oncall
- 代码问题:回滚到上一个稳定版本
## 升级条件
处理 15 分钟后错误率仍然 > 5%,升级给 Tech Lead把 Runbook URL 放进告警的 annotations 里,半夜收到告警,点链接,照着 Runbook 操作,大幅降低 MTTA(Mean Time to Acknowledge)和 MTTR(Mean Time to Resolve)。
如何评估你的告警质量
告警规则写好之后,要持续评估质量。关键指标:
告警精准率:告警触发了,是真的需要处理的比例。低于 80% 说明有太多误报。
告警召回率:出了真正的故障,是否有对应告警触发。100% 最好,低于 95% 说明有漏报。
MTTD(Mean Time to Detect):从故障发生到告警触发的平均时间。越短越好。
每个月做一次回顾:哪些告警从来没被处理过(可能是噪音)?哪些故障没有对应告警(告警盲区)?
深度解析:SLO、SLA 与 Error Budget 的实践
很多团队配置了大量告警,但仍然说不清楚"我们的服务有多可靠"。引入 SLO(Service Level Objective,服务水平目标)能把这个问题量化。
从指标到 SLO
SLO 是你对服务可靠性的内部目标,比如"支付接口的成功率 99.9%,P99 延迟低于 500ms"。注意 SLO 和 SLA(Service Level Agreement)的区别:SLA 是对外的承诺,通常比 SLO 宽松,因为中间要留缓冲。SLO 是内部执行标准,用来驱动工程决策。
SLO 需要基于正确的指标来度量。以成功率为例,关键是"怎么定义成功"。HTTP 200 一定是成功吗?如果业务上返回了 200 但 response body 里有 "code": "ERROR",这算成功还是失败?定义 SLO 要和业务方对齐,确保双方对"成功"有相同的理解。
Error Budget 让决策变简单
Error Budget 是 SLO 的另一面:如果 SLO 是 99.9%,那在任意 30 天窗口内,允许 0.1% 的请求失败。0.1% × 30天 × 24小时 × 3600秒 = 约 2592 秒,这就是每月的 error budget——43 分钟的故障时间(在 100% 故障率下)。
Error Budget 有一个非常实用的决策框架:当 Error Budget 充足时(用掉不到 50%),可以大胆做变更,部署新功能,因为即使出了小问题还有余量;当 Error Budget 快用完时(用掉超过 80%),需要降低变更频率,优先做稳定性工作,而不是继续推新功能。这个框架把工程决策从"感觉"变成了"数据驱动",也让业务方和工程团队有了共同的语言。
Prometheus 计算 SLO
用 Prometheus 计算 SLO 需要两类指标:成功的请求数和总的请求数。
# 5 分钟内的成功率(滑动窗口)
sum(rate(http_requests_total{status=~"2.."}[5m]))
/ sum(rate(http_requests_total[5m]))30 天的 Error Budget 消耗情况,需要更长的时间窗口:
# 30 天内的成功率
sum(rate(http_requests_total{status=~"2.."}[30d]))
/ sum(rate(http_requests_total[30d]))告警可以基于 Error Budget 的消耗速率来设计:如果当前速率持续,6 小时内会消耗掉 1 小时的 Error Budget,就触发告警。这种"burn rate"告警比固定阈值告警更智能——它考虑的是"按照这个速度,我还能撑多久",而不只是"当前状态是否超过了某个值"。
深度解析:告警文化与 On-Call 实践
告警规则的技术问题是可以解决的,但告警文化的问题更难,也更关键。
告警疲劳是最大的风险
"告警疲劳"是指:告警太多、太频繁,响应者逐渐麻木,开始忽略告警,或者形成"先看一眼,好像没事,等等再说"的心理。这是非常危险的状态,因为当真正的重大故障来临时,可能已经习惯性地被忽视了几分钟甚至更长时间。
我见过最严重的案例是:一个团队的 Alertmanager 每天发出 300+ 告警,响应者每天上午花一个小时"清告警"——实际上是把告警一条条标为"已知问题"或者直接静音,根本没有真正处理。这个团队的告警体系实际上已经失效了,只是表面上在运行。
避免告警疲劳,最核心的原则是:每一条告警必须对应一个需要人处理的行动。如果一条告警出来了,但值班工程师不知道该做什么,或者确认不需要做什么,这条告警就不应该存在。把这条原则作为告警配置的 review 标准,每增加一条告警都要问:"工程师收到这条告警,他会做什么?"
On-Call 轮班的合理设计
On-Call 是告警体系的"最后一公里"。On-Call 工程师要能在告警到来后快速响应,这需要两个保障:工程师熟悉告警涉及的系统,以及有清晰的 runbook(处理手册)可以参考。
对于新加入 On-Call 轮班的工程师,必须有"入职培训":把过去 3 个月内最常见的告警类型过一遍,每种告警的排查路径是什么,escalation 的条件是什么,谁是后备联系人。这个培训不用很正式,可以是 1-2 小时的结对,由有经验的工程师带着新人走一遍。跳过这个步骤,就是让新人在凌晨三点独自面对陌生的告警,既对系统不好,对工程师的心理压力也很大。
On-Call 之后的复盘也很重要。每次重要故障或者 On-Call 事件,花 30 分钟写一个简短的复盘:问题是什么、怎么发现的、怎么解决的、有没有改进点。把这些复盘积累起来,是团队的知识库,也是改进告警规则的依据。告警体系的成熟,需要工程师把 On-Call 经验持续反馈回告警规则的优化中,这是一个良性循环的起点。
总结
告警规则不是配置一次就完的东西,它需要持续迭代。
告警的终极目标是:睡觉时被叫醒,处理的是真正值得你睡不着的问题,而不是每次都是误报。
建立"告警即行动"的文化,让每一条告警都有对应的 runbook,让每一次响应都有记录,让告警规则在复盘中持续优化——这才是告警体系成熟的标志。
