适用场景
本文适用于 Prometheus 根据错误率、延迟、资源使用率等指标触发告警,并通过 Alertmanager 发送通知的场景。典型问题是:指标在阈值附近波动,告警几分钟内反复触发和恢复;短暂采集缺口被误判为恢复;值班人员收到大量重复通知,却难以判断故障是否真正结束。
目标不是简单地“把告警调迟”,而是分别处理三个时间尺度:查询窗口负责平滑原始指标,for 负责确认异常持续存在,keep_firing_for 负责容忍短暂恢复或数据缺口。最后用 promtool 固化规则行为,避免改动规则后产生意外通知。
现象描述
下面这条规则在低流量或指标抖动时很容易制造噪声:
groups:
- name: checkout-alerts
rules:
- alert: CheckoutHighErrorRate
expr: job:http_5xx_ratio:rate1m{job="checkout"} > 0.05
labels:
severity: page
annotations:
summary: "结算服务 5xx 比例过高"
它存在几个问题:
- 一分钟窗口对少量错误过于敏感,一次突发就可能越过阈值;
- 没有最小请求量约束,低流量时一次失败就会形成很高的比例;
- 没有
for,表达式第一次为真就进入 firing; - 条件一次为假就恢复,短暂抓取失败或实例切换会造成“恢复—再次触发”;
- 规则没有自动化测试,调整窗口或持续时间时只能在线上观察。
先确认抖动发生在哪一层
不要看到重复通知就立即增加 Alertmanager 的 repeat_interval。通知重复可能来自不同层次:
| 层次 | 识别方法 | 常见原因 |
|---|---|---|
| 指标 | Prometheus 图上数值频繁穿越阈值 | 窗口过短、低样本量、分母接近零 |
| 告警规则 | Alerts 页面状态在 pending、firing、inactive 间切换 | 缺少 for,标签集合不稳定,短暂数据缺失 |
| Alertmanager | Prometheus 始终 firing,但通知反复发送 | 分组标签不合理、路由重复、重复间隔过短 |
| 高可用链路 | 同一事件来自不同 Prometheus 实例 | external_labels 或 Alertmanager 去重标签不一致 |
先在 Prometheus 的 Alerts 页面查看告警状态和标签,再在表达式浏览器执行原始 PromQL。若告警实例的标签集合不断变化,即使 alertname 相同,Prometheus 也会把它们视为不同告警实例;这时增加等待时间只能掩盖问题。
可先检查告警规则当前状态:
ALERTS{alertname="CheckoutHighErrorRate"}
alertstate 标签可区分 pending 与 firing。同时绘制规则表达式和请求速率,确认阈值穿越是否与低流量、发布或抓取缺口重合。
用稳定的窗口和流量门槛计算错误率
先用 recording rules 统一请求速率和错误速率,避免多个告警复制复杂表达式:
groups:
- name: checkout-recording
interval: 30s
rules:
- record: job:http_requests:rate5m
expr: sum by (job) (rate(http_requests_total{job="checkout"}[5m]))
- record: job:http_5xx:rate5m
expr: sum by (job) (rate(http_requests_total{job="checkout",status=~"5.."}[5m]))
- record: job:http_5xx_ratio:rate5m
expr: |
job:http_5xx:rate5m{job="checkout"}
/
clamp_min(job:http_requests:rate5m{job="checkout"}, 0.001)
rate(...[5m]) 使用五分钟范围向量估算每秒增长率,比一分钟窗口更能抵抗瞬时尖峰。sum by (job) 在计算比例前先聚合分子和分母,避免“先算每实例比例再平均”造成权重错误。clamp_min 只负责防止除零,不能代替流量门槛。
正式告警同时要求错误比例和总请求速率达到门槛:
- name: checkout-alerts
interval: 30s
rules:
- alert: CheckoutHighErrorRate
expr: |
job:http_5xx_ratio:rate5m{job="checkout"} > 0.05
and on (job)
job:http_requests:rate5m{job="checkout"} > 1
for: 10m
keep_firing_for: 5m
labels:
severity: page
team: payment
annotations:
summary: "结算服务 5xx 比例持续高于 5%"
description: "过去 5 分钟错误比例为 {{ $value | humanizePercentage }},且异常已持续 10 分钟。"
runbook_url: "https://runbook.example.com/checkout/high-error-rate"
这里的三个时间参数含义不同:
[5m]是每次计算所观察的数据范围,不代表告警必须持续五分钟;for: 10m要求同一标签集合的表达式持续为真十分钟,期间处于 pending;keep_firing_for: 5m在条件最后一次满足后继续保持 firing 五分钟,可吸收短暂恢复和数据缺口。
keep_firing_for 不是无限宽限。若真实恢复已经超过五分钟,告警仍应结束;若指标频繁消失,应优先修复采集链路,并另设 up == 0 或数据缺失告警。
如何选择时间参数
参数应来自业务影响速度和观测噪声,而不是统一抄一个模板:
- 查询窗口至少覆盖多个采样点。抓取间隔为 30 秒时,五分钟窗口约包含十个点;
for应长于常见无害波动,但短于业务允许的发现时间;keep_firing_for应覆盖常见的短暂恢复、实例切换或单次抓取缺口,但不能长到掩盖真实恢复;- page 级告警强调可行动性,ticket 级告警可以容忍更长确认时间;
- 每次只调整一个时间尺度,并用历史曲线回放影响。
例如,支付错误率通常需要快速响应,可从“5 分钟窗口、持续 10 分钟、恢复保持 5 分钟”开始;容量趋势告警则可能采用 30 分钟窗口和更长的 for。这些只是起点,最终应依据历史事件校准。
避免标签变化重置 for
Prometheus 按完整标签集合识别告警实例。如果表达式输出的标签变化,for 计时会重新开始。不要把当前值、时间戳、Pod UID 等动态内容放进 labels:
# 错误示例:每次求值都可能形成新的告警实例。
labels:
severity: page
current_value: "{{ $value }}"
动态值应放进 annotations,路由和去重所需的稳定维度才放进 labels。同时谨慎保留 pod、instance 等高基数标签:如果值班动作针对整个服务,先按 job 或业务服务聚合通常更合适;如果必须定位单实例,则保留实例标签并接受每个实例独立计时。
用 promtool 做语法检查
上线前先检查规则文件:
promtool check rules ./rules/checkout.rules.yml
命令退出码为 0 才能进入发布阶段。语法检查可以发现 YAML、PromQL 和规则结构错误,但不能证明告警时序符合预期,因此还需要单元测试。
为告警时序编写单元测试
为了让测试专注于 pending、firing 和保持恢复的行为,可以对记录后的错误比例编写规则测试。测试文件 checkout.rules.test.yml 示例:
rule_files:
- checkout.rules.yml
evaluation_interval: 1m
tests:
- name: 持续异常达到十分钟后触发
interval: 1m
input_series:
- series: 'job:http_5xx_ratio:rate5m{job="checkout"}'
values: '0.08x20'
- series: 'job:http_requests:rate5m{job="checkout"}'
values: '10x20'
alert_rule_test:
- eval_time: 9m
alertname: CheckoutHighErrorRate
exp_alerts: []
- eval_time: 10m
alertname: CheckoutHighErrorRate
exp_alerts:
- exp_labels:
job: checkout
severity: page
team: payment
exp_annotations:
summary: "结算服务 5xx 比例持续高于 5%"
description: "过去 5 分钟错误比例为 8%,且异常已持续 10 分钟。"
runbook_url: "https://runbook.example.com/checkout/high-error-rate"
- name: 低流量不触发
interval: 1m
input_series:
- series: 'job:http_5xx_ratio:rate5m{job="checkout"}'
values: '0.50x15'
- series: 'job:http_requests:rate5m{job="checkout"}'
values: '0.2x15'
alert_rule_test:
- eval_time: 15m
alertname: CheckoutHighErrorRate
exp_alerts: []
运行测试:
promtool test rules ./rules/checkout.rules.test.yml
模板渲染的空格和百分比格式可能随实际版本输出不同。首次编写测试时可用 --debug 查看求值过程,再把期望注解校准到当前生产版本。核心断言至少应覆盖:未达到 for 时不触发、持续异常后触发、低流量不触发、短暂恢复期间仍保持 firing、超过保持窗口后恢复。
发布与回滚
推荐按以下顺序上线:
- 在代码仓库中执行
promtool check rules和promtool test rules; - 在预发布 Prometheus 上加载规则,观察表达式、pending 时长和标签集合;
- 生产环境先仅路由到低打扰接收器,和旧规则并行比较一段时间;
- 确认新规则没有漏掉真实事件后,再切换正式 page 路由;
- 保留旧规则文件或提交版本,异常时可快速回滚。
Prometheus 支持在规则文件格式正确时通过配置重载应用变更。重载后应检查规则加载状态和 Prometheus 自身日志;不能只看到命令成功就认为所有规则都已生效。
预防措施
- 为每条 page 告警维护负责人、处置动作、运行手册和业务影响说明;
- 监控告警自身的触发、恢复和通知数量,定期找出抖动最多的规则;
- 比例类告警同时设置最小样本量或请求速率门槛;
- 把动态观测值放入 annotations,保持用于身份和路由的 labels 稳定;
- 将规则文件和测试一起评审,阈值、窗口、
for或标签变化都必须更新测试; - 将采集失败与业务恢复分开建模,不要用
keep_firing_for长期掩盖指标缺失; - 对高可用 Prometheus 统一外部标签,确保 Alertmanager 能正确去重。
总结
告警抖动不是单纯的通知频率问题。有效治理需要从数据到通知逐层定位:先用合理查询窗口和流量门槛降低指标噪声,再用 for 确认异常持续存在,用 keep_firing_for 容忍短暂恢复,同时保持告警标签稳定。
最后用 promtool check rules 和 promtool test rules 把时序行为变成可重复验证的规则。这样减少的是无效通知,而不是通过粗暴延迟牺牲真实故障的发现能力。
Discussion
评论