第 5 部分导论 · 告警规则怎么工作

告警不是「查询结果为真就发通知」那么简单。这一章读 rules/alerting.go:Inactive→Pending→Firing 三态机、for 子句(holdDuration)为什么是防抖的关键、keep_firing_for 解决什么,以及那个你能直接查询的 ALERTS 指标。

作者 David更新于 第 31 篇(共 52 篇)

第 5 部分导论 · 告警规则怎么工作

PromQL 会写了。它最重要的用途之一是告警——也是这个专栏最实战的部分之一。

但告警远不止“查询结果为真就发通知”。这一章读 rules/alerting.go,看清它的机制;后面几章讲阈值怎么定、怎么不被淹没。

一条告警规则长什么样

groups:
- name: api-alerts
  rules:
  - alert: HighErrorRate
    expr: |
      sum by (service) (rate(http_requests_total{status=~"5.."}[5m]))
      / sum by (service) (rate(http_requests_total[5m])) > 0.01
    for: 5m                          # ← 关键
    keep_firing_for: 5m              # ← 较新的字段
    labels:
      severity: page
    annotations:
      summary: "{{ $labels.service }} 错误率 {{ $value | humanizePercentage }}"

这些字段在源码里一一对应(rules/alerting.go:116):

type AlertingRule struct {
    name         string
    vector       parser.Expr      // expr:要周期性求值的表达式
    // The duration for which a labelset needs to persist in the expression
    // output vector before an alert transitions from Pending to Firing state.
    holdDuration time.Duration    // ← 这就是 for
    // The amount of time that the alert should remain firing after the resolution.
    keepFiringFor time.Duration   // ← keep_firing_for
    labels        labels.Labels       // 附加标签
    annotations   labels.Labels       // 非标识性的说明文字
    active map[uint64]*Alert          // 当前活跃的告警,按标签集指纹索引
}

三态机:Inactive → Pending → Firing

源码里的状态定义(rules/alerting.go:54):

type AlertState int
const (
    // StateInactive is the state of an alert that is neither firing nor pending.
    StateInactive
    // StatePending is the state of an alert that has been active for less than
    // the configured threshold duration.
    StatePending
    // StateFiring is the state of an alert that has been active for longer than
    // the configured threshold duration.
    StateFiring
)
图表加载中…

Pending 这个中间态,是整个告警机制里最重要的设计。

没有它会怎样? 表达式每次求值(默认每 15s 一次)只要为真就发告警——于是:

  • 一次抓取失败 → 误报。
  • 一个瞬时的流量尖峰 → 误报。
  • GC 造成的一次 200ms 抖动 → 误报。

for: 5m 的意思是:“我要看到这个条件连续满足 5 分钟**,才认为它是真的。”** 中途只要有一次求值不满足,状态就打回 Inactive,计时重来。

这就是防抖(debounce)。 它把“瞬时抖动”和“真实的持续问题”区分开——是告警可用性的基石。没有 for,你的告警会淹没在噪音里,然后全员静音,然后真出事时没人看。

实战规则:几乎每条告警都该有 for。 唯一的例外是那些“一次就致命”的条件(比如证书已过期、磁盘已满 100%)——但即使那些,加个 for: 1m 也不亏(防的是采集本身的抖动)。

keep_firing_for:防止震荡

for 防的是“太快响”,keep_firing_for 防的是“太快停”。

源码注释:“The amount of time that the alert should remain firing after the resolution.”

场景:错误率在阈值附近反复横跳(1.1% → 0.9% → 1.2% → 0.8%……)。没有 keep_firing_for 的话:

告警 → 恢复 → 告警 → 恢复 → 告警 ...     ← 一晚上几十条通知,人会疯

这叫震荡(flapping)。keep_firing_for: 5m 让告警在条件不满足后仍保持 firing 5 分钟,于是短暂的“恢复”不会触发“已解决”通知,连续的抖动被合并成一条告警。

for + keep_firing_for 一起,构成了告警的迟滞(hysteresis)——进入和退出用不同的时机,这是所有防震荡控制的通用手法(空调的温控、施密特触发器都是这个原理)。

ALERTS:告警本身也是指标

一个精妙的设计(源码 rules/alerting.go:41):

alertMetricName         = "ALERTS"
alertForStateMetricName = "ALERTS_FOR_STATE"

每条活跃的告警(Pending 或 Firing),都会被写回 TSDB 成一条真实的 series**,指标名就叫 ALERTS。**

# 查询当前所有 firing 的告警——这本身就是个普通的 PromQL 查询!
ALERTS{alertstate="firing"}

# 过去 24 小时,哪条告警响得最多?(告警质量分析!)
topk(10, count_over_time(ALERTS{alertstate="firing"}[24h]))

# 某条告警的历史:它什么时候响过?
ALERTS{alertname="HighErrorRate"}

这个设计的价值被严重低估了:

  • 告警历史可查询 —— 你能用 PromQL 分析告警本身:哪条最吵、哪条从没响过(可能是死规则)、告警和故障的相关性。
  • 可以对告警告警 —— 比如“如果 firing 的告警数突然 > 50,说明可能是监控系统自己出问题了”。
  • ALERTS_FOR_STATE 用于重启恢复 —— 它持久化了“这条告警从什么时候开始 pending 的”,这样 Prometheus 重启后不用把所有 for 计时器从零重来(否则重启一次,所有 for: 1h 的告警都要再等一小时——重启期间等于没有告警)。源码里那个 restored *atomic.Bool 就是干这个的。

“把系统自己的状态也变成指标”——这是可观测性理念的自我应用,和 up 指标、prometheus_tsdb_head_series 一脉相承。第 9 部分讲告警质量分析时,ALERTS 就是你的数据源。

labels vs annotations:一个关键区别

源码注释说得很准确:annotations 是 “Non-identifying key/value pairs”。

labelsannotations
作用标识这条告警是谁描述给人看的信息
参与去重/分组吗是(Alertmanager 靠它分组)否
典型内容severity: page、team: apisummary、description、runbook_url

关键后果:告警的“身份”由 labels 决定。 如果你把每次都变的东西(比如 {{ $value }} 当前值)放进 labels,那么每次值一变就成了一条“新告警”——Alertmanager 会把它当成不同的告警,去重和抑制全部失效,你会被通知淹没。

规则:变化的东西放 annotations,标识性的东西放 labels。 这和第 2 部分讲的“标签是标识、不是数据”是同一条原则在告警层的体现——又一次,基数思维。

规则组:顺序与并发

规则被组织进 group(rules/group.go),这不只是“分类”——它有语义:

  • 同一个 group 内,规则顺序执行** —— 所以 recording rule 可以依赖同组里前面的规则结果。
  • 不同 group 之间并发执行** —— 互不影响。
  • 每个 group 有自己的 interval —— 不写就用全局 evaluation_interval。

实战含义:如果规则 B 依赖规则 A 的结果,它们必须在同一个 group 且 A 在前。 放不同 group 的话,B 可能读到 A 上一轮的结果(甚至第一次求值时 A 还没产出)——这是“我的规则算出来是空”的常见原因。

小结与下一章

告警规则的核心是三态机 Inactive → Pending → Firing(rules/alerting.go:54)。for(源码 holdDuration)是防抖的基石——要求条件连续满足才转 Firing,中途一次不满足就打回重来;没有它,一次抓取失败就误报,最终全员静音。keep_firing_for 防震荡(条件不满足后仍保持 firing 一段时间,把反复横跳合并成一条)——两者合起来是迟滞,和空调温控同一原理。告警本身也是指标 ALERTS(alerting.go:41)——可以用 PromQL 分析告警质量(哪条最吵/哪条是死规则)、可以对告警告警;ALERTS_FOR_STATE 让重启后不用重头计时,这是可观测性理念的自我应用。labels 标识、annotations 描述——把每次都变的值放进 labels 会让每次变化都成为“新告警”,去重失效。规则组内顺序执行、组间并发——依赖前一条结果的规则必须同组且在后。

告警之外,规则引擎还有另一个用途——也是查询性能的利器。下一章看它。👉 recording rules。

参考资料

  • [1] Prometheus 源码 rules/alerting.go(AlertState:54、AlertingRule:116、holdDuration/keepFiringFor、ALERTS:41)、rules/group.go。
  • [2] Prometheus 官方文档:Alerting rules、Recording rules。
  • [3] 关联PromQL 求值、标签是标识。

本页目录