适用场景

本文适用于 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 标签可区分 pendingfiring。同时绘制规则表达式和请求速率,确认阈值穿越是否与低流量、发布或抓取缺口重合。

用稳定的窗口和流量门槛计算错误率

先用 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 或数据缺失告警。

如何选择时间参数

参数应来自业务影响速度和观测噪声,而不是统一抄一个模板:

  1. 查询窗口至少覆盖多个采样点。抓取间隔为 30 秒时,五分钟窗口约包含十个点;
  2. for 应长于常见无害波动,但短于业务允许的发现时间;
  3. keep_firing_for 应覆盖常见的短暂恢复、实例切换或单次抓取缺口,但不能长到掩盖真实恢复;
  4. page 级告警强调可行动性,ticket 级告警可以容忍更长确认时间;
  5. 每次只调整一个时间尺度,并用历史曲线回放影响。

例如,支付错误率通常需要快速响应,可从“5 分钟窗口、持续 10 分钟、恢复保持 5 分钟”开始;容量趋势告警则可能采用 30 分钟窗口和更长的 for。这些只是起点,最终应依据历史事件校准。

避免标签变化重置 for

Prometheus 按完整标签集合识别告警实例。如果表达式输出的标签变化,for 计时会重新开始。不要把当前值、时间戳、Pod UID 等动态内容放进 labels

# 错误示例:每次求值都可能形成新的告警实例。
labels:
  severity: page
  current_value: "{{ $value }}"

动态值应放进 annotations,路由和去重所需的稳定维度才放进 labels。同时谨慎保留 podinstance 等高基数标签:如果值班动作针对整个服务,先按 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、超过保持窗口后恢复。

发布与回滚

推荐按以下顺序上线:

  1. 在代码仓库中执行 promtool check rulespromtool test rules
  2. 在预发布 Prometheus 上加载规则,观察表达式、pending 时长和标签集合;
  3. 生产环境先仅路由到低打扰接收器,和旧规则并行比较一段时间;
  4. 确认新规则没有漏掉真实事件后,再切换正式 page 路由;
  5. 保留旧规则文件或提交版本,异常时可快速回滚。

Prometheus 支持在规则文件格式正确时通过配置重载应用变更。重载后应检查规则加载状态和 Prometheus 自身日志;不能只看到命令成功就认为所有规则都已生效。

预防措施

  • 为每条 page 告警维护负责人、处置动作、运行手册和业务影响说明;
  • 监控告警自身的触发、恢复和通知数量,定期找出抖动最多的规则;
  • 比例类告警同时设置最小样本量或请求速率门槛;
  • 把动态观测值放入 annotations,保持用于身份和路由的 labels 稳定;
  • 将规则文件和测试一起评审,阈值、窗口、for 或标签变化都必须更新测试;
  • 将采集失败与业务恢复分开建模,不要用 keep_firing_for 长期掩盖指标缺失;
  • 对高可用 Prometheus 统一外部标签,确保 Alertmanager 能正确去重。

总结

告警抖动不是单纯的通知频率问题。有效治理需要从数据到通知逐层定位:先用合理查询窗口和流量门槛降低指标噪声,再用 for 确认异常持续存在,用 keep_firing_for 容忍短暂恢复,同时保持告警标签稳定。

最后用 promtool check rulespromtool test rules 把时序行为变成可重复验证的规则。这样减少的是无效通知,而不是通过粗暴延迟牺牲真实故障的发现能力。

参考资料