> 一个从未被写入过的 Gauge,导出的不是"无数据",是"一切正常"
一个分布式存储项目有 53 KB 的运维手册,其中 §4.1「Prometheus 指标大全」 列了四十来个指标,§4.2 给它们配了告警规则,§5.1 的故障排查表里每一行的 "排查步骤"都是"查看某个指标"。
结论先行¶
一个分布式存储项目有 53 KB 的运维手册,其中 §4.1「Prometheus 指标大全」 列了四十来个指标,§4.2 给它们配了告警规则,§5.1 的故障排查表里每一行的 "排查步骤"都是"查看某个指标"。
指标全都声明了,在两个文件里,用 promauto 规规矩矩注册:
ClusterMode = promauto.NewGauge(prometheus.GaugeOpts{
Name: "cluster_mode",
Help: "Cluster mode: 0=read-write, 1=read-only",
})
数一遍谁在给它们赋值:
pkg/metrics/metrics.go 19 个声明,被写入 1 个(ErrorsTotal)
pkg/metrics/kvcache_metrics.go 21 个声明,被写入 0 个
40 个指标里 39 个在整个仓库中只出现在它自己的声明处。
真实数字是存在的 —— 它们从 internal/api/handlers/demo_handler.go
以 JSON 形式吐出来,名字还很像(kvcache_hits 对 kvcache_hits_total,
kvcache_mem_used 对 kvcache_memory_used_bytes),只是没有走 Prometheus。
最要命的一条是脑裂告警:
- expr: cluster_mode == 1 # 手册 §4.2
ClusterMode 是普通 Gauge,不是 Vec。promauto 注册之后,哪怕
一次都没 Set(),/metrics 里也会有 cluster_mode 0 ——
而它自己的 Help 写着 0=read-write。
它不是缺数据,它是在肯定地报平安。 真脑裂的时候,告警不响,
而手册让你"查看 cluster_mode 指标是否为 1",你查到的是 0。
为什么¶
Prometheus 客户端库里,没被写过的指标有两种导出行为,两种都不会让告警响:
| 类型 | 从未写入时 /metrics 里 |
x == 1 |
rate(x[5m]) > 0 |
|---|---|---|---|
NewGauge / NewCounter |
x 0 |
false(看起来健康) | false |
NewGaugeVec / NewCounterVec |
一条 series 都没有 | no data(不触发) | no data(不触发) |
无标签的 Vec 在任何标签组合被用到之前不产生任何 series,而 Prometheus 的
告警规则在表达式求值为空时默认不触发(要靠 absent() 才能捕捉)。
所以 Vec 那半边静悄悄,plain 那半边直接报健康。
这不是 Prometheus 的问题,是声明和写入在两个不同的改动里。
声明指标是写监控时的第一步,接到业务路径上是第二步,而第一步就足以让
/metrics 看起来是满的、让运维手册写得出来、让 Grafana 画得出面板。
中间那一步漏掉之后,系统的每一个外部观测面都变得更自信,而不是更沉默。
同一个仓库里还有一条印证:internal/storage/replication.go:350,
副本写失败的处理是
logger.Error("Replication failed", ...)
// TODO: Add to retry queue
既不重试,也不计数。那个本该记它的
s3_replication_operations_total{status="failure"} 就在上面那 39 个里。
于是一条副本永久缺失,唯一痕迹是一行日志。
怎么用¶
上线监控前,先数一遍"声明了但没人写"的指标。 一行 shell 就够:
for v in $(grep -oE '^\s*[A-Z][A-Za-z]+ = promauto\.New[A-Za-z]+' pkg/metrics/*.go \
| sed 's/.*://;s/ =.*//'); do
printf '%-34s refs=%s\n' "$v" "$(grep -rn "metrics\.$v" --include='*.go' . | wc -l)"
done
refs=1(只有声明本身)就是一个永远绿的指标。这是静态的、零成本的、
可以进 CI 的 —— 我用它在这个仓库上一次抓到 39 个。
真正的验收不是"指标存在",是"能让它变成非零"。 每个告警规则都该有一个配套的测试或手动步骤,去造出它该响的那个状态, 看它响。做不到这一步的告警规则,和注释没有区别 —— 同 断言"它拒绝了"不能用非零退出码——任何东西都会非零退出: 断言"门禁是绿的"和断言"门禁在该红的时候红"完全是两件事。
普通 Gauge 的默认值要选在"异常"那一侧。 0=read-write, 1=read-only
把健康态放在了零值上,于是漏接线和健康无法区分。反过来定义
(0=unknown, 1=read-write, 2=read-only,告警 cluster_mode != 1)
就让"没人写过它"变成一个会响的状态。能让默认值报警的编码,
比能让默认值报平安的编码值钱。
运维手册不是证据。 这份手册写得很认真,它只是对着代码里的
声明写的,而不是对着 /metrics 的实际输出写的。
写运行手册时,每个指标名都该从活实例的 curl /metrics 里复制 ——
见 被引用的产物要有一个"任何一次运行都生成不出来"的文件名。
出处¶
~/Engineering/s3-like-storage:pkg/metrics/metrics.go、
pkg/metrics/kvcache_metrics.go:104-107(ClusterMode 的声明与 Help),
internal/api/handlers/demo_handler.go:59-66(真实数值走的那条 JSON 路径),
internal/storage/replication.go:343-351(失败只记日志的那段),
docs/OPERATIONS_GUIDE.md §4.1 / §4.2(896 行的 cluster_mode == 1)/ §5.1
(脑裂行的排查步骤)。引用计数是我 2026-10-11 自己跑的,只读,未改仓库。