适用场景

这篇文章适合已经使用 Prometheus 和 Alertmanager,但值班群里经常出现同一告警在几分钟内“触发—恢复—再次触发”的团队。典型场景包括:

  • 接口错误率在阈值附近上下波动;
  • 抓取偶发失败,表达式短暂返回空向量;
  • 应用滚动发布后计数器重置,瞬时比率失真;
  • 规则计算过慢或远端数据到达较晚,评估结果出现断点;
  • 告警规则只有触发阈值,没有持续时间、恢复缓冲和数据缺失告警。

治理目标不是让告警“安静”,而是让一次真实故障对应一个可追踪的告警事件,同时把采集故障与业务故障分开表达。

现象描述

以 API 5xx 错误率告警为例,初始规则可能如下:

groups:
  - name: api.rules
    rules:
      - alert: ApiHighErrorRate
        expr: |
          sum(rate(http_requests_total{job="api",status=~"5.."}[1m]))
          /
          sum(rate(http_requests_total{job="api"}[1m]))
          > 0.05
        labels:
          severity: page
        annotations:
          summary: "API 5xx 错误率超过 5%"

这条规则看起来直观,却存在三个问题:

  1. 1m 窗口容易被少量请求或短时尖峰放大;
  2. 没有 for,第一次越过阈值就进入 firing;
  3. 一旦表达式不再返回满足条件的序列,告警立即恢复。

如果评估周期为 30 秒,错误率依次为 4.8%、5.2%、4.9%、5.3%,值班人员可能连续收到触发和恢复通知。若其中一次抓取失败导致分子、分母都没有数据,告警还可能产生一次错误恢复。

先分清三类抖动

1. 业务值在阈值附近回摆

这是最常见的情况。流量较小、窗口过短或固定阈值不符合业务基线,都会导致表达式结果反复跨过边界。

先在 Prometheus 表格视图确认返回的标签集合,再用图形视图观察至少一个业务周期:

sum by (service) (
  rate(http_requests_total{job="api",status=~"5.."}[5m])
)
/
clamp_min(
  sum by (service) (rate(http_requests_total{job="api"}[5m])),
  0.001
)

clamp_min 只用于避免分母为零造成无穷值,不能替代最低请求量判断。低流量服务即使只有一个失败请求,也可能得到很高的百分比。

2. 指标短暂缺失

PromQL 的即时向量在当前评估点找不到可用样本时,通常返回空向量,而不是数值 0。对于告警规则,空向量表示没有活跃告警实例,这会被解释为条件消失。

同时检查抓取状态和样本是否连续:

up{job="api"}
count_over_time(http_requests_total{job="api"}[10m])
absent_over_time(http_requests_total{job="api"}[5m])

up == 0 表示 Prometheus 已尝试抓取但失败;absent_over_time 适合判断一段时间内完全没有目标序列。二者应建立独立告警,不要把“监控看不见了”误报成“业务恢复了”。

3. 规则评估本身不稳定

规则组执行时间超过评估间隔时,后续评估会被跳过。应检查 Prometheus 自监控指标:

increase(prometheus_rule_group_iterations_missed_total[15m]) > 0
prometheus_rule_group_last_duration_seconds
/
prometheus_rule_group_interval_seconds
> 0.8

第一条确认最近是否漏评估,第二条找出执行时间已经逼近周期的规则组。若使用远端写入接收或查询链路,还要考虑数据到达延迟,必要时为规则组设置 query_offset,不要用无限增大 for 掩盖计算能力不足。

一套可落地的稳定规则

下面的规则把请求量、错误率、持续触发和恢复缓冲拆开处理:

groups:
  - name: api-availability.rules
    interval: 30s
    rules:
      - record: service:http_requests:rate5m
        expr: |
          sum by (service) (
            rate(http_requests_total{job="api"}[5m])
          )

      - record: service:http_5xx:rate5m
        expr: |
          sum by (service) (
            rate(http_requests_total{job="api",status=~"5.."}[5m])
          )

      - record: service:http_5xx:ratio5m
        expr: |
          service:http_5xx:rate5m
          /
          service:http_requests:rate5m

      - alert: ApiHighErrorRate
        expr: |
          service:http_5xx:ratio5m > 0.05
          and on (service)
          service:http_requests:rate5m > 1
        for: 5m
        keep_firing_for: 10m
        labels:
          severity: page
          team: backend
        annotations:
          summary: "{{ $labels.service }} 的 API 5xx 错误率持续超过 5%"
          description: "当前五分钟错误率为 {{ $value | humanizePercentage }},且请求速率超过 1 req/s。"
          runbook_url: "https://runbooks.example.com/api-high-error-rate"

      - alert: ApiMetricsMissing
        expr: absent_over_time(http_requests_total{job="api"}[5m])
        for: 2m
        labels:
          severity: warning
          team: platform
        annotations:
          summary: "API 请求指标已连续缺失"
          description: "先检查服务发现、抓取目标、网络和指标端点,不能据此判断业务健康。"

关键字段的含义如下:

  • interval: 30s:该规则组每 30 秒评估一次;
  • for: 5m:条件必须在连续评估中保持活跃 5 分钟,告警才从 pending 进入 firing;
  • keep_firing_for: 10m:条件消失后继续保持 firing 10 分钟,减少短暂恢复或数据断点造成的恢复通知;
  • rate(...[5m]):用五分钟窗口降低短时噪声,计数器重启也由 rate 处理;
  • service:http_requests:rate5m > 1:只有流量达到最低门槛时才按错误率分页;
  • and on (service):只按 service 标签匹配两侧序列,避免标签集合不一致导致结果为空。

for 和 keep_firing_for 解决的是不同问题:前者过滤短暂异常,后者过滤短暂恢复。两者都不应随意设成很长时间,否则会延迟真实告警或掩盖恢复状态。

如何选择时间窗口

时间参数应从 SLO、采集周期和故障响应目标推导,而不是照搬示例。

假设抓取周期为 30 秒,目标是在持续异常 8 分钟内通知:

  1. 查询窗口可先用 5 分钟,至少包含 10 个采样点;
  2. for 可从 3 分钟开始,使总检测延迟约为 3 至 8 分钟;
  3. keep_firing_for 应覆盖常见的短暂数据缺口和自动恢复抖动,例如 5 至 10 分钟;
  4. 对低流量服务增加请求量门槛,或改用固定时间窗内的失败请求数;
  5. 对高优先级故障保留更短窗口,但必须通过历史回放验证误报率。

不要把不同用途的时间混为一谈:查询窗口决定统计平滑程度,for 决定触发确认时间,keep_firing_for 决定恢复确认时间,Alertmanager 的 group_wait 和 repeat_interval 则控制通知聚合与重复发送。

用 promtool 做静态检查和规则测试

上线前先检查语法:

promtool check rules ./rules/api-availability.rules.yml

再用单元测试验证“短尖峰不触发、持续异常会触发”。测试文件示例:

rule_files:
  - api-availability.rules.yml

evaluation_interval: 1m

tests:
  - interval: 1m
    input_series:
      - series: 'service:http_requests:rate5m{service="checkout"}'
        values: '10x20'
      - series: 'service:http_5xx:ratio5m{service="checkout"}'
        values: '0.01x2 0.08x3 0.01x15'
    alert_rule_test:
      - eval_time: 5m
        alertname: ApiHighErrorRate
        exp_alerts: []

  - interval: 1m
    input_series:
      - series: 'service:http_requests:rate5m{service="checkout"}'
        values: '10x20'
      - series: 'service:http_5xx:ratio5m{service="checkout"}'
        values: '0.01x2 0.08x10 0.01x8'
    alert_rule_test:
      - eval_time: 8m
        alertname: ApiHighErrorRate
        exp_alerts:
          - exp_labels:
              alertname: ApiHighErrorRate
              service: checkout
              severity: page
              team: backend
            exp_annotations:
              summary: "checkout 的 API 5xx 错误率持续超过 5%"
              description: "当前五分钟错误率为 8%,且请求速率超过 1 req/s。"
              runbook_url: "https://runbooks.example.com/api-high-error-rate"

执行测试:

promtool test rules ./rules/api-availability.test.yml

eval_time 是相对测试起点的时间。第一组只有三分钟高错误率,不满足五分钟 for;第二组持续时间足够,应产生告警。实际项目还应补充指标缺失、最低流量边界、标签匹配和恢复缓冲测试。

灰度发布与现场验证

不要直接覆盖所有生产规则。推荐按以下顺序操作:

  1. 在测试环境运行 promtool check rules 和 promtool test rules;
  2. 在生产以新告警名并行观察,暂不接入分页接收器;
  3. 对比新旧规则一周内的触发次数、持续时间和人工确认结果;
  4. 确认新规则稳定后再切换 Alertmanager 路由;
  5. 保留旧规则配置和回滚步骤,观察期结束后再删除。

运行时可查询 Prometheus 自动生成的告警序列:

ALERTS{alertname="ApiHighErrorRate",alertstate=~"pending|firing"}

同时记录以下指标,才能判断治理是否有效:

  • 每周告警事件数和恢复事件数;
  • 同一标签集合在 30 分钟内重复触发次数;
  • 告警从 pending 到 firing 的耗时;
  • 指标缺失告警和业务告警的重叠情况;
  • 被值班人员标记为无须处理的比例。

常见错误

用 or vector(0) 粗暴补零

or vector(0) 生成的零值通常没有原业务标签,可能让缺失序列看起来像健康,还可能改变向量匹配结果。数据缺失应由单独规则表达;确需补零时,必须明确标签来源和匹配关系。

只调整 Alertmanager 的重复通知间隔

repeat_interval 只能减少同一 firing 告警的重复通知,不能阻止 Prometheus 先恢复、再创建一个新的 firing 事件。告警状态抖动应先在 PromQL、for 和 keep_firing_for 层治理。

按百分比告警却没有流量门槛

低流量时百分比极不稳定。分页告警应同时满足错误率和最低请求量;低流量服务可以改用更长窗口或失败请求数。

用超长 for 掩盖坏指标

如果根因是抓取失败、规则漏评估或标签高基数,延长 for 只会推迟发现问题。先修复采集与规则性能,再设定业务可接受的确认窗口。

预防措施

  • 每条业务告警都明确“异常持续多久才需要人工介入”;
  • 业务指标缺失、抓取失败和规则漏评估分别告警;
  • 为比例告警设置最低样本量或最低流量门槛;
  • 用记录规则固定复杂表达式,减少重复计算和标签错配;
  • 把 promtool check rules 与 promtool test rules 加入持续集成;
  • 监控规则执行时长、漏评估次数和每条规则产生的序列数量;
  • 变更告警时保留静默观察期,用历史数据和真实事件复盘参数;
  • 在告警注解中提供当前值、影响对象和运行手册地址。

总结

Prometheus 告警抖动通常不是单一参数问题,而是统计窗口、触发确认、恢复确认、指标完整性和规则执行能力共同作用的结果。可靠的治理顺序是:先确认返回的标签集合和数据连续性,再区分业务回摆、指标缺失与规则漏评估,最后用合理的查询窗口、for、keep_firing_for 和最低流量门槛稳定状态。

告警的价值不在于数量多,而在于每次触发都能清楚回答三个问题:发生了什么、影响谁、现在该做什么。

参考资料