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

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

- 作者：David（道雾轩）
- 专栏：指标采集与观测（https://daiw.org/manual/observability.md）
- 最后更新：2026-10-01
- 原文：https://daiw.org/manual/observability/alerting-rules
- 转载与引用：请注明出处并附原文链接（https://daiw.org/about/copyright）

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

[PromQL 会写了](https://daiw.org/manual/observability/promql-performance)。它最重要的用途之一是**告警**——也是这个专栏最实战的部分之一。

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

## 一条告警规则长什么样

```yaml
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`）：

```go
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`）：

```go
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
)
```

```mermaid
stateDiagram-v2
    [*] --> Inactive
    Inactive --> Pending: 表达式开始返回结果
    Pending --> Firing: 持续满足超过 for 时长
    Pending --> Inactive: 中途不满足了(防抖成功!)
    Firing --> Inactive: 不再满足(+ keep_firing_for)
    note right of Pending
        这个中间态就是防抖的全部意义
    end note
```

<Callout type="info">
  **`Pending` 这个中间态，是整个告警机制里**最重要**的设计。**

  **没有它会怎样？** 表达式每次[求值](https://daiw.org/manual/observability/promql-engine)（默认每 15s 一次）只要为真就发告警——于是：
  - 一次[抓取失败](https://daiw.org/manual/observability/scrape-loop) → 误报。
  - 一个瞬时的流量尖峰 → 误报。
  - GC 造成的一次 200ms 抖动 → 误报。

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

  **这就是防抖（debounce）。** 它把“[瞬时抖动](https://daiw.org/manual/observability/promql-rate)”和“[真实的持续问题](https://daiw.org/manual/observability/alerting-philosophy)”区分开——**是告警可用性的基石**。没有 `for`，你的告警会淹没在噪音里，然后全员静音，然后真出事时没人看。

  **实战规则：几乎每条告警都该有 `for`。** 唯一的例外是那些“[一次就致命](https://daiw.org/manual/observability/alert-thresholds)”的条件（比如证书已过期、磁盘已满 100%）——但即使那些，加个 `for: 1m` 也不亏（防的是采集本身的抖动）。
</Callout>

## keep_firing_for：防止震荡

<Callout type="info">
  **`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）**——进入和退出用不同的时机，这是所有防震荡控制的通用手法**（空调的温控、施密特触发器都是这个原理）。
</Callout>

## ALERTS：告警本身也是指标

一个精妙的设计（源码 `rules/alerting.go:41`）：

```go
alertMetricName         = "ALERTS"
alertForStateMetricName = "ALERTS_FOR_STATE"
```

<Callout type="info">
  **每条活跃的告警（Pending 或 Firing），都会被**写回 [TSDB](https://daiw.org/manual/observability/tsdb-overview) 成一条真实的 series**，指标名就叫 `ALERTS`。**

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

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

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

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

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

  **“把系统自己的状态也变成指标”——这是[可观测性理念的自我应用](https://daiw.org/manual/observability/why-observability)**，和 [`up` 指标](https://daiw.org/manual/observability/scrape-loop)、[`prometheus_tsdb_head_series`](https://daiw.org/manual/observability/blocks-compaction) 一脉相承。[第 9 部分讲告警质量分析](https://daiw.org/manual/observability/alerting-philosophy)时，`ALERTS` 就是你的数据源。
  </Callout>

## labels vs annotations：一个关键区别

<Callout type="warn">
  **源码注释说得很准确：`annotations` 是 “**Non-identifying** key/value pairs”。**

  | | `labels` | `annotations` |
  | --- | --- | --- |
  | **作用** | **标识**这条告警是谁 | **描述**给人看的信息 |
  | **参与去重/分组吗** | **是**（[Alertmanager 靠它分组](https://daiw.org/manual/observability/alertmanager)） | **否** |
  | **典型内容** | `severity: page`、`team: api` | `summary`、`description`、`runbook_url` |

  **关键后果：告警的“身份”由 labels 决定。** 如果你把**每次都变的东西**（比如 `{{ $value }}` 当前值）放进 `labels`，那么**每次值一变就成了一条“新告警”**——[Alertmanager 会把它当成不同的告警](https://daiw.org/manual/observability/alertmanager)，去重和抑制全部失效，你会被通知淹没。

  **规则：变化的东西放 annotations，标识性的东西放 labels。** 这和[第 2 部分讲的“标签是标识、不是数据”](https://daiw.org/manual/observability/defining-metrics)是同一条原则在告警层的体现——**又一次，基数思维**。
</Callout>

## 规则组：顺序与并发

<Callout type="info">
  **规则被组织进 **group**（`rules/group.go`），这不只是“分类”——它有语义：**

  - **同一个 group 内，规则**顺序执行** —— 所以 [recording rule](https://daiw.org/manual/observability/recording-rules) 可以依赖同组里**前面**的规则结果。
  - **不同 group 之间**并发执行** —— 互不影响。
  - **每个 group 有自己的 `interval`** —— 不写就用全局 `evaluation_interval`。

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

## 小结与下一章

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

告警之外，规则引擎还有另一个用途——也是[查询性能的利器](https://daiw.org/manual/observability/promql-performance)。下一章看它。👉 [recording rules](https://daiw.org/manual/observability/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 求值](https://daiw.org/manual/observability/promql-engine)、[标签是标识](https://daiw.org/manual/observability/defining-metrics)。
