它连「已恢复」这一步都没走到。
告警不是变绿,也不是变红,而是不再是 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 那一侧无论怎么改都救不回来------ 因为它拿到的输入已经是「这条序列不存在」了。 要救,得回到第 ⑤ 跳。
五、这个机制不成立的地方
写清楚免得被过度引用:
- 它只在「这条序列真的没了」时触发。
如果 target 只是抓不到值但仍在服务发现里,走的是另一条路(up == 0),不是 stale。
stale可以被查询显式看到。 直接查原始序列时那个位模式还在。
看不见它的是 PromQL 的向量选择器,不是存储。
- 它不会让告警变红。 变红需要
for子句里的条件持续为真;
stale 让的是「不再 active」,方向相反。
- 本文没有验证
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), 所以本文的论点全部挂在计数、函数名和逐字相同的代码上,不挂行号。
本篇不成立的结论(免得被过度引用)
- 不主张「stale 是 bug」。它是一个设计选择,代价写在官方文档里。
- 不主张 「告警规则应该检查 stale」。本文只指出检查不到,
以及为什么在这个接口上补检查是补不上的。
- 本文没有实测 任何一条真实告警从 stale 到 inactive 的完整时间线。
全部结论来自源码与官方文档,不是运行时观测。
- 本文没有取到
for子句、keep_firing_for与 stale 的交互行为,不作断言。