告警不是变绿,是「不再是 active」——rules/alerting.go 里 0 次 IsStaleNaN

它连「已恢复」这一步都没走到。

告警不是变绿,也不是变红,而是不再是 active 。你在面板上看到的是「已恢复」, 而代码里发生的事情是:这个指纹从结果集里消失了。

这两件事在界面上长得一模一样,在代码里是两回事。

⚠️ 口径先钉住:本篇的每个行号、每个位模式、每段官方文档原文, 都是我今天从 prometheus/prometheus 的 main 分支当场取回全文逐条核对的, 不是凭印象。跨版本的部分我另外抓了 5 个 ref 的全文重跑,结论在文末。

一、stale 不是「值为空」,是一种专门造出来的 NaN

先看它到底是什么。model/value/value.go:

go 复制代码
// StaleNaN is a signaling NaN, due to the MSB of the mantissa being 0.
StaleNaN uint64 = 0x7ff0000000000002

// IsStaleNaN returns true when the provided NaN value is a stale marker.
func IsStaleNaN(v float64) bool {
	return math.Float64bits(v) == StaleNaN
}

它是一个位模式,不是「没有值」。

IEEE 754 的 NaN 有一大堆(正负无穷相减、各种 payload)。 Prometheus 挑了 0x7ff0000000000002 这一个,注释说得很清楚: signaling NaN,因为尾数的最高位是 0。

为什么要专门造一个?因为「这条序列没了」这件事, 必须能被当成一个数据点写进时间序列里。 如果只写「不写」,那查询和存储就分不清「这一时刻没采到」和「这一时刻它没了」。

所以 stale 是一个值。它有确切的位置,确切的时间戳,确切的序列。

而判定它的函数只有一行:比对位模式。不解析数值、不做大小比较。

二、这条链一共六步,每一步都能钉到行

go 复制代码
① target 从服务发现里消失
   ↓  scrape/manager.go      sp.stop()
② 抓取侧决定标记它
   ↓  scrape/scrape.go       空抓取 → updateStaleMarkers
③ 写入一个 stale 位模式
   ↓  scrape/scrape.go       app.Append(..., math.Float64frombits(value.StaleNaN))
④ 序列上多了一个点
   ↓
⑤ 查询时这个点被丢弃
   ↓  promql/engine.go:2842  if value.IsStaleNaN(v) || ... { return ..., false }
⑥ 结果集里没有这个指纹
   ↓  rules/alerting.go:483  if _, ok := resultFPs[fp]; !ok {
⑦ 置为 inactive
      rules/alerting.go       a.State = StateInactive; a.ResolvedAt = ts

注意 ② 和 ③ 之间的一个细节:抓取侧不是立刻写 stale 的, 官方文档写的是 target 被移除后「soon after removal」------ 中间要等大约两个抓取间隔,为的是不把一次抖动的抓取误判成下线。

另外两条写入路径我也在 rules/group.go 里核到了:

arduino 复制代码
// Series no longer exposed, mark it stale.
// Rule that produced series no longer configured, mark it stale.

第二条特别值得注意:规则本身不再产出某条序列时,也会把它标 stale。 所以「服务还在跑」和「这条告警还在被算」是两件事------ 规则配置改掉、或者规则不再输出那条序列,告警照样会「不再是 active」。

三、真正的问题在第 ⑤ 跳

promql/engine.go 里,vectorSelectorSingle 遇到 stale 的处理是直接 return:

go 复制代码
if value.IsStaleNaN(v) || (h != nil && value.IsStaleNaN(h.Sum)) {
	return 0, 0, 0, nil, false
}

这个 false 的语义是「这一刻这个时间序列不存在」, 而不是「这个值坏了」或者「数据缺失,重试一下」。

官方文档把同一件事写得很直白:

If a query is evaluated at a sampling timestamp after a time series is marked as stale, then no value is returned for that time series.

「没有返回值」和「值坏了」在下游是完全不同的两件事, 但在这个接口上它们长得一模一样------都是「查不到」。

于是求值器把这条序列当成了不存在。判据没有报错,没有异常,没有任何日志。 它只是诚实地报告「这一刻我什么都没查到」。

这一跳是整个机制的要害:stale 是一个「有值的坏消息」, 而引擎把它翻译成了「没有消息」。 信息在翻译过程中丢失了,不是被丢弃,是被归一化成了沉默。

四、所以告警不是变绿,是「不再是 active」

rules/alerting.go 里这一段是落点:

css 复制代码
for fp, a := range r.active {
	if _, ok := resultFPs[fp]; !ok {

结果集里没有这个指纹 → 这条告警不再 active。

这里有一个我必须专门拿出来讲的事实:

rules/alerting.go 里 IsStaleNaN 出现 0 次。 我在 5 个 ref 上逐个数过,五版全部是 0。

也就是说------告警规则那一侧完全不知道 stale 这件事。 它不是「检查了发现值坏了于是不报」,而是根本没往那边想。 它的判据是「结果集里有没有这个指纹」,而 stale 已经被上游翻译成了「没有指纹」。

这两句的区别不是抠字眼:

如果引擎说 / 告警侧看到 / 会发生什么

  • 「值坏了」 ------ 一个异常值 ------ 可能报错、可能标记数据质量
  • 「序列不存在」 ------ 结果集少一条 ------ 判为 resolved,一切正常

alerting.go 那一侧无论怎么改都救不回来------ 因为它拿到的输入已经是「这条序列不存在」了。 要救,得回到第 ⑤ 跳。

五、这个机制不成立的地方

写清楚免得被过度引用:

  1. 它只在「这条序列真的没了」时触发。

如果 target 只是抓不到值但仍在服务发现里,走的是另一条路(up == 0),不是 stale。

  1. stale 可以被查询显式看到。 直接查原始序列时那个位模式还在。

看不见它的是 PromQL 的向量选择器,不是存储。

  1. 它不会让告警变红。 变红需要 for 子句里的条件持续为真;

stale 让的是「不再 active」,方向相反。

  1. 本文没有验证 alerting.go 在别的分支或别的项目里的行为。

全部结论只对 main 分支当时的状态负责。

六、看到一个「不再是 active」的告警,该先问什么

按这个顺序,能少走很多弯路:

一、这条序列还在被抓吗? up 和该序列的 absent()/absent_over_time() 一起看。 up == 1 但序列没有 → 那多半是规则不再产出它,不是 target 掉了。

二、是不是配置改掉了? rules/group.go 那条注释说得很清楚:规则不再产出这条序列,它也会被标 stale。 up 一切正常而告警「已恢复」,先查规则配置改没改。

三、别把 stale 当成「没数据所以不算」。 它是一个被明确写下来的结论------「这条序列在此时刻终止了」。 和「这一轮没采到」不是一回事。

四、如果你的判据依赖 absent() 或 count() 这类聚合, 把 stale 算不算进去,结果可能和你想的相反。 具体行为要在你的查询里实测, 本文不下结论。

附:本文引用

一手(prometheus/prometheus main 分支,本轮我亲自取全文核对)

结论 / 出处

  • StaleNaN 是 signaling NaN,尾数最高位为 0 ------ model/value/value.go:24-28
  • 位模式 0x7ff0000000000002 ------ model/value/value.go:28
  • IsStaleNaN 只做位模式比对 ------ model/value/value.go:32-33
  • 查询时 stale 点被直接丢弃,返回 false ------ promql/engine.go:2842-2844(vectorSelectorSingle 首处)
  • rules/alerting.go 里 IsStaleNaN 出现 0 次 ------ rules/alerting.go 全文;5 个 ref 逐个数过
  • 结果集没有指纹 → 不再 active ------ rules/alerting.go:482-483
  • 置 inactive 并写 ResolvedAt ------ rules/alerting.go 的 a.State = StateInactive 分支
  • 规则不再产出序列时也标 stale ------ rules/group.go:639 与 :729(两处 mark it stale)
  • 「no value is returned for that time series」 ------ docs/querying/basics.md
  • target 移除后「soon after removal」才标 stale ------ docs/querying/basics.md

跨版本复核(本轮实跑,抓 5 个 ref 的全文)

v2.55.0 / v3.0.0 / v3.7.0 / v3.15.0 / main:

量 / 结果

  • promql/engine.go 里 IsStaleNaN 的出现次数 ------ 各 6 次,分布在 5 行上(首行有 2 处)
  • rules/alerting.go 里 IsStaleNaN ------ 各 0 次
  • 首处那一行的内容 ------ 五版逐字相同 ,去空白后 md5 全部 09b00ebf7b545aa8
  • 首处所属函数 ------ 五版都是 vectorSelectorSingle
  • 紧随其后的丢弃返回 ------ v2.55.0/v3.0.0/v3.7.0 为 return 0, 0, nil, false;v3.15.0/main 为 return 0, 0, 0, nil, false

行号在两版之间无一相同 (v3.7.0 的 2406/2641/2652/2689/2700 vs main 的 2842/3108/3123/3164/3178), 所以本文的论点全部挂在计数、函数名和逐字相同的代码上,不挂行号。

本篇不成立的结论(免得被过度引用)

  1. 不主张「stale 是 bug」。它是一个设计选择,代价写在官方文档里。
  2. 不主张 「告警规则应该检查 stale」。本文只指出检查不到,

以及为什么在这个接口上补检查是补不上的。

  1. 本文没有实测 任何一条真实告警从 stale 到 inactive 的完整时间线。

全部结论来自源码与官方文档,不是运行时观测。

  1. 本文没有取到 for 子句、keep_firing_for 与 stale 的交互行为,不作断言。
相关推荐
迪康软件zz1 天前
终端审批体系怎么搭?从模板配置到业务连续性
运维·安全·自动化运维
东莞市云毅网络有限公司1 天前
结构化标注缺失怎么查:JSON-LD抽取与每日巡检脚本实现
python·sqlite·自动化运维·geo·数据监测
Vex2une3 天前
Windows PowerShell 发展史:从命令行工具到自动化核心
windows·自动化运维·命令行·powershell·cmd·脚本语言·对象管道
Ikalus19884 天前
让 Agent 去修 flaky,它删掉了让测试不确定的变量
自动化运维
东莞市云毅网络有限公司5 天前
企业资料时效性判定:Last-Modified、正文日期与版本字段三路交叉
python·sqlite·自动化运维·geo·数据监测
Ikalus19885 天前
1,136 条「通过」的轨迹里,122 条是运气
自动化运维
Ikalus19885 天前
3254 个测试全绿,CI 红在一个根本没跑的检查上
自动化运维
Ikalus19885 天前
我批准部署的那一刻,抓「部署没发生」的门禁变绿了
自动化运维
Zelman6 天前
测试过程模型与左移右移
测试·自动化运维·devops