第 5 部分导论 · 告警规则怎么工作
告警不是「查询结果为真就发通知」那么简单。这一章读 rules/alerting.go:Inactive→Pending→Firing 三态机、for 子句(holdDuration)为什么是防抖的关键、keep_firing_for 解决什么,以及那个你能直接查询的 ALERTS 指标。
第 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”。
labels | annotations | |
|---|---|---|
| 作用 | 标识这条告警是谁 | 描述给人看的信息 |
| 参与去重/分组吗 | 是(Alertmanager 靠它分组) | 否 |
| 典型内容 | severity: page、team: api | summary、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。