Prometheus 停止写入磁盘时,一切看起来都正常。状态页在跳数字,健康检查全绿,Grafana 还在画图。直到有人翻开容器日志,才发现数据库已经几个小时没有持久化任何数据。

这个项目叫 agentic-os,是作者部署在 Railway 上的自托管可观测性栈:Prometheus、Grafana、OpenTelemetry Collector、cloudflared 隧道,外加一个公开状态 API,发布关于 Claude Code 使用情况的三个数字——请求数、令牌数、成本。它是个个人项目,但它是真实部署、有真实运行时间,存在的全部意义就是"出事时告诉我"。

打开网易新闻 查看精彩图片

8月13日,Prometheus 停止向磁盘写入块。TSDB 压缩开始报"设备无剩余空间",每分钟一次,从不间断。每小时六十次失败——这不是性能下降,是每一次尝试都在失败。

有意思的是,从外面看什么都看不出来。Prometheus 用内存中的 head block 回答查询,所以一个几小时没持久化任何东西的数据库,和健康的数据库在外观上毫无区别。这个故障对指向它的每一个监控工具都是隐形的。

公平地说,Prometheus 重启并不会立即丢数据,因为 head 会从预写日志(WAL)重建。但 WAL 就在那个已经没有空间的卷上——"重启后能恢复"这个赌注,押的恰恰是已经耗尽的那个资源。作者不想赌。

发现方式很原始:手动翻容器日志。这是当时唯一可行的办法,原因事后回想很尴尬——prometheus_tsdb_* 指标没有被任何东西采集。Prometheus 是整个栈里唯一没人监控的服务。没有序列,就没有办法写告警规则,因为根本没人采集这些序列。

修复方案:用 Prometheus 监控 Prometheus,按大小而不是仅按时间限制保留期,加一个看门狗把状况上报给 Sentry。

然后真正的 bug 出现了。

Sentry 收到一条事件。然后沉默了六天。8月19日作者查看 issue,显示"最后出现于五天前"。这看起来完全像一个已经清除的故障。他差点就关闭了它。

看门狗的去重逻辑是一个模块级集合——每个进程生命周期只通知一次。看门狗触发一次,加入键,然后在这个进程存活期间再也不说话。收到的四条事件不是四次检测,而是四次重启。在它们之间,压缩每小时失败六十次,持续不断,而告警路径已经无话可说。

告警系统没有沉默。沉默反而是诚实的。它在主动报告一种形态——一条事件,然后什么都没有——这种形态对每个读 issue 追踪器的人来说,都意味着"暂时性,已解决"。

精确地说,这个 bug 不在于做了去重。去重本身没错——错的是去重发生在进程级别,而进程生命周期恰好掩盖了故障的持续性。一个设计上"只报一次"的机制,在故障持续存在时,变成了"报一次就永远闭嘴"。

监控系统的元问题:谁来监控监控者?

这次事故暴露了一个经典盲区:Prometheus 是栈里唯一没有被采集指标的服务。它监控一切,唯独没人监控它。当监控者自己出故障时,所有下游的告警都建立在流沙之上。

作者最后总结的教训很直白:告警系统最危险的状态不是沉默,而是"看起来正常"。一条事件后归于寂静,在人类读 issue 的直觉里等于"问题已解决"——但实际可能是告警通道本身已经死了,只是死得很有礼貌。