# 告警为什么反复触发或从不恢复？

告警反复触发或从不恢复，多半是阈值贴着数据抖动、数据断流被当成恢复、标签集在两次判定间变了，或没设恢复条件。

最后校对于 2026-10-10

同一条规则一晚上发了几十条，触发—恢复—触发—恢复；或者反过来，问题早修好了， 事件还挂在那里不走。这两种是同一套机制的两个方向，放在一页里讲。

先做个判断：**是事件在抖，还是只有通知在抖？** 打开告警事件的历史， 如果历史里真的是一串「触发 / 恢复」交替的记录，那是事件在抖； 如果历史里只有一条一直没恢复的事件，但你收到了很多条消息，那是通知重复， 两者的排查方向完全不同。

## 三个时间参数，各管一件事[​](#三个时间参数各管一件事 "三个时间参数，各管一件事的直接链接")

| 参数     | 默认值        | 拦的是什么                                     |
| -------- | ------------- | ---------------------------------------------- |
| 执行频率 | `@every 60s`  | 多久查一次                                     |
| 持续时长 | 60 秒         | 条件成立多久才产生事件 —— 拦**上升沿**的毛刺   |
| 留观时长 | 0（立即恢复） | 条件不成立多久才判定恢复 —— 拦**下降沿**的毛刺 |

**留观时长拦的是恢复事件本身，不只是恢复通知。** 在留观窗口内，事件仍然是「触发中」， 页面上看得到，只是还没被判恢复。这是抖动最直接的解药，也是开源版 Prometheus 规则 **唯一**的防抖手段——开源版这类规则没有独立的恢复条件开关，恢复条件永远是触发条件取反， 做不出「90% 报警、跌到 70% 才算恢复」这种非对称阈值。

用触发条件那种表单的类型（Elasticsearch、Loki、ClickHouse 等）才有单独的恢复条件可配。

## 最常见的一类：数据断了被当成恢复[​](#最常见的一类数据断了被当成恢复 "最常见的一类：数据断了被当成恢复的直接链接")

这是抖动的头号原因，而且很难一眼看出来。

引擎拿不准「查询返回空」到底是「值降下来了」还是「这段时间的点丢了」， 处理方式是**一律当成恢复**。所以只要采集断一个周期、时序库丢一批点、 或者查询超时返回空，就会走一次恢复，下个周期数据回来又触发一次。

怎么确认是它：

1. 打开这条规则的**判定记录**，找恢复的那个周期，看 `anomaly_total` 是不是 0， 同时该周期的查询结果 `series_total` 也是 0 —— **是查不到序列，不是值降下去了**；
2. 对着同一时间去即时查询做区间查询，看曲线上是不是有断点；
3. 服务端 `/metrics` 里看 `prometheus_tsdb_too_old_samples_total` 有没有在涨 （乱序/过期样本被丢，会造成断点）。

确认之后，从下面这几个方向治：

* **把留观时长设成大于一个采集断点的长度**，比如采集间隔 15 秒就设 60\~120 秒；
* 表达式里避免用会因为一个点缺失就整体变空的写法；
* 断点本身是采集或写入的问题，去 [数据源连上了，为什么查不到数据？](https://n9e.github.io/zh/docs/troubleshooting/no-data.md) 治根。

顺带说清楚一个反直觉的点：**查询报错和查询返回空的行为完全不同。** 报错时这一轮的判定和恢复处理**整个不做**，事件既不刷新也不恢复； 只有正常返回空才会触发恢复逻辑。所以数据源间歇性超时反而不会造成抖动， 它造成的是「状态卡住」。

## 标签变了，事件就换了一条[​](#标签变了事件就换了一条 "标签变了，事件就换了一条的直接链接")

事件是按标签集算哈希来标识的。**只要标签集有任何一个键或值变了，就是另一条事件**—— 旧的那条因为「不再出现」被判恢复，新的那条作为新事件触发。表现出来就是不停地 触发+恢复，但对象其实一直是同一个。

典型来源：

* 表达式里带了会变的标签，比如容器 ID、Pod 名、端口、`instance` 里的临时端口；
* 规则上挂的工作流里有 `relabel` 处理器，把标签改来改去；
* 设备列表里这台机器的标签被人改了，而 `Pushgw.LabelRewrite` 是开着的（默认开）， 改完之后写进去的标签就变了。

怎么确认是它：翻历史事件，把相邻两条「恢复」和「触发」的标签**逐个键对比**， 只要能找出一个键在变，就是这个原因。治法是在表达式里用聚合把易变标签去掉 （比如 `sum without(instance) (...)`），或者在工作流里把它 drop 掉。

## 反过来：从不恢复[​](#反过来从不恢复 "反过来：从不恢复的直接链接")

事件挂着不走，按这个顺序看：

1. **规则还在评估吗。** 判定记录停在某个时间点之后就没有了，说明引擎不再跑这条规则， 而**没有评估就永远不会恢复**。走 [判定记录为什么缺失或被丢弃？](https://n9e.github.io/zh/docs/troubleshooting/evaluation-missing.md)。
2. **查询一直在报错吗。** 报错时恢复处理整个跳过。看 `n9e_alert_rule_eval_error_total{stage="query_data"}` 有没有在涨， 判定记录里 `n9e_alert_eval_query_series_count` 是不是负数。
3. **留观时长设得太长了吗。** 窗口没走完，事件就还是触发中。
4. **对象消失了吗。** 机器从设备列表里删了之后，带这个 `ident` 的事件会被当成屏蔽处理 （判定记录里 detail 是 `ident not exists, was muted`），它不会自己恢复。
5. **恢复通知被关了。** 事件在页面上其实已经恢复，只是你没收到消息—— 规则上的「恢复后是否通知」是个独立开关，关掉之后恢复事件照样记录，只是不发。

## 通知在抖，事件没抖[​](#通知在抖事件没抖 "通知在抖，事件没抖的直接链接")

如果历史里只有一条事件，但消息发了很多条，那是重复通知，不是抖动。控制它的是两个参数：

* **重复通知间隔**（`notify_repeat_step`，单位分钟）：多久重发一次。设成 0 表示 **整个告警周期只通知一次**。
* **最大通知次数**（`notify_max_number`）：0 表示不限次。

日志里能直接读出每一轮的决策，前缀是 `alert_eval_<规则id> datasource_<数据源id> event-hash-<哈希>`：

```
fired, notify_repeat_step_matched(1788440612 >= 1788438872 + 60 * 60) notify_max_number_ignore(#2 / 0)

stalled, notify_repeat_step_not_matched(1788440612 < 1788438872 + 60 * 60)

stalled, notify_repeat_step_matched(...) notify_max_number_not_matched(#5 / 5)
```

`stalled` 的意思是：**事件还在告警中，但这一轮不发通知，也不写新的历史记录**。 从用户视角看就是「安静下来了」。这几行同时也会出现在判定记录的事件 `stage` 里。

一条规则挂多个级别时，**抑制**开关会让同一对象只保留最严重的那一条， 关掉它就会同时收到三条——判定记录里 `inhibited` 计数就是被它挡掉的数量。

## 收集这些再去提问[​](#收集这些再去提问 "收集这些再去提问的直接链接")

1. 规则 ID，以及规则上的执行频率 / 持续时长 / 留观时长 / 重复通知间隔四个值；
2. 抖动时间段内的历史事件列表，**保留标签列**（这是判断标签漂移的关键）；
3. 同一时间段判定记录里的 `anomaly_total`、`series_total`、事件的 `stage`；
4. 日志里 `event-hash-<哈希>` 的所有行。

**脱敏**：标签值里的主机名、业务名、容器 ID 都替换掉，但**要保留「哪几个键在变」这个结构**， 否则问题就没法看了。

## 下一步[​](#下一步 "下一步的直接链接")

* 判定与恢复的完整模型：[判定、恢复与状态变化](https://n9e.github.io/zh/docs/concepts/evaluation.md)
* 判定周期和恢复参数怎么调：[判定周期与恢复](https://n9e.github.io/zh/docs/alerting/evaluation.md)
* 数据断点的根因：[数据源连上了，为什么查不到数据？](https://n9e.github.io/zh/docs/troubleshooting/no-data.md)
* 还有哪些降噪手段：[常见噪音与对策](https://n9e.github.io/zh/docs/events/noise-patterns.md)
