替换 Prometheus Federation:一个多租户监控平台的重建

英文版:English

Why

Federation 拓扑顶端的 global Prometheus 每周 OOM 两到三次。它的 head block 维护着 1.2M series,占用 5 到 10 GB 内存;每次崩溃重启都会在数据上留下空洞,而这些空洞会以"不存在的故障"的形式把 oncall 从床上叫醒。这是可靠性的一半。另一半是延迟,而且是结构性的:Federation 叠加了两层 scrape 周期,集群本地 15s 加全局层 30s,全局视图最多滞后现实 45 秒。P0 发生时,被 page 的人看到的是过去。

规模让两个问题都无解。50 个集群的负载下 /federate 接口频繁 scrape timeout,Grafana 上出现数据断点;每个集群各自维护一套 federation rules,新集群接入要手工修改 global Prometheus 的 scrape 配置。同时平台没有可用的多租户维度:指标跨租户聚合,单个租户的局部故障被平均值掩盖。

我在一个 3 到 4 人的 SRE 团队里独立 own observability 这个领域:设计、选型、落地。引擎选型上我做了 VictoriaMetrics 与 Thanos 的 POC 对比(写入吞吐、压缩率、运维复杂度),结论由团队共识确认。

How

替代方案是推送式的中心化架构:每个 workload 集群部署一个 vmagent 就地采集,在 relabel 阶段注入 cluster 和 cluster group 标签,然后 remote_write 到中心 VictoriaMetrics 集群(vminsert ×2 挂 LB,vmstorage ×3 副本因子 2,vmselect ×2)。vmalert 在同一存储上评估 recording rules 和 alert rules;Alertmanager 以 HA 双实例做通知路由;Grafana 加 Loki 构成读取与验证面。

容量按实测输入设计:50 集群 × 约 12 节点 ≈ 600 节点,每节点约 2,000 series,合计约 1.2M active series、约 80,000 samples/s(15s 采集间隔)。热存储 SSD 上 3 个月约 250 GB,压缩比约 4 倍:同样的窗口在 Federation 下要 930 GB。冷存储走 S3,5 分钟降采样,180 天约 25 GB。

告警是平台的组成部分,不是附属品。recording rules 先物化一族 SLI:QPS、error ratio、P95/P99 latency、saturation、分租户 SLI,全部按 {tenant, cluster group} 键控;dashboard 和告警规则都消费预计算结果,不在现场跑重查询。告警分三层:PAGER 走 PagerDuty,只留给确认的用户影响;HIGH 进 Slack;MEDIUM 归工单。PAGER 必须满足 multi-window burn-rate:5m 和 30m 两个窗口同时超阈值才触发,既滤掉瞬时抖动,又不放过持续恶化。inhibition 只沿因果明确的链条做抑制(NodeNotReady 抑制同节点的 PodUnableToStart);跨服务的推测性抑制被刻意排除,因为配错方向的 inhibition 会掩盖真实故障。

租户身份有两条注入路径:应用在自己的 metrics 上打 tenant label,因为只有应用知道在处理谁的请求;vmagent 在 scrape 时通过 relabel 注入集群与 cluster group 上下文。项目之前各集群的 relabel 配置各自为政:cluster 命名不统一,部分团队用 client 而不是 tenant,这会悄无声息地破坏告警路由、drill-down 和 inhibition 的 label 相等匹配。我写了统一的 relabel 模板分发到所有集群,并推动应用侧收敛到 tenant

旧架构:federation。

flowchart TB subgraph FLEET["50 个集群"] P["每集群 Prometheus<br/>15s 抓取"] end P -->|"/federate 拉取 · 30s"| G["全局 Prometheus<br/>120 万 series head block"] G --> GR["Grafana"] G -.->|"每周 OOM · 延迟最高 45s"| GAP["数据缺口 → 假告警"]

新架构:VictoriaMetrics 平台。

flowchart TB subgraph FLEET2["50 个集群"] A["vmagent<br/>remote_write + 持久化队列"] end A --> VI["vminsert ×2"] VI --> VS[("vmstorage ×3 · 双副本")] VS --> VQ["vmselect ×2"] VQ --> VA["vmalert<br/>recording + 告警规则"] VQ --> GF["Grafana / Loki"] VA --> AM["Alertmanager<br/>三层分级 + inhibition"]

Hard parts

切换全程不丢一条告警。 VictoriaMetrics 用 MetricsQL,与 PromQL 存在边界差异,“规则照常触发"不能靠假设。我们以双写配置并行运行新旧两套栈两周,每条告警规则在两侧同时评估,diff 触发行为,修完所有分歧才切换通知路由。平台还必须"响亮地失败”:vmalert 持续输出心跳,deadman’s switch 加 out-of-band 探针把"监控失联"在分钟级转化为一条明确的 page,而不是一个安静的盲区。

数据延迟从 45s 到 5s 以内。 改善来自架构而非调参。Federation 的滞后是两层 scrape 周期的结构性叠加;remote_write 在采集的同时推送样本,延迟直接降到 5 秒以内。真正需要工程投入的不是快,而是不丢:推送意味着网络抖动可能丢数据,所以每个 vmagent 都在本地磁盘上跑 persistent queue,remote 断连期间缓冲,恢复后回放。

多租户 series 基数治理。 加租户维度会成倍放大 series,label 铺开后增长很快。治理手段:tenant 级 recording rules 只覆盖核心 SLI;基础设施指标一律不带 tenant label;retention 分层(tenant SLA 序列 90 天,排障序列 30 天,基础设施 15 天);定期 cardinality 审查清理意外的高基数 label。诚实的复盘结论:cardinality budget 应该在第一天就存在,而不是等增长曲线逼出来。

让 severity 有语义。 对 30 天告警历史做审计,约 80% 的 page 是 resource-centric 的:CPU 或内存抖动,值班人看一眼就关掉。解法是给 PAGER 层做准入控制:一条 page 必须指示可观测的用户影响(SLO burn rate、SLA latency 击穿、关键链路 QPS 断崖),其余全部降级到 HIGH 或 MEDIUM;静态阈值全部换成 multi-window burn-rate。上线后用两个反馈信号持续调参:每条 PAGER 的 action rate,以及用户报了问题但没有对应 page 的漏报。

Production

切换的门禁是那两周的双写窗口:告警规则在两套栈上触发行为等价、dashboard 查询在 MetricsQL 下结果等价,路由才允许迁移;门禁通过之前 Federation 栈一直作为 fallback 在线。inhibition 规则在 staging 环境人工触发上游故障做验证,确认下游告警被正确抑制,并保留 firing 历史供 postmortem 回溯。切换后,新集群接入从"写 federation rules 加改全局 scrape 配置"收缩为"部署 vmagent,配一个 remote_write 地址"。日常验证靠三个信号:deadman 心跳、PAGER action rate、漏报追踪。

Takeaways

  • Federation 的 45s 延迟和每周 OOM 是架构属性而不是配置属性,任何调参预算都修不好。
  • 告警质量是准入控制问题:定义什么配得上一条 page,其余全部降级,用每条 page 的 action rate 做回归测试。
  • label 一致性本身就是平台基础设施:路由、drill-down、inhibition 全部依赖 label 相等匹配,必须在 relabel 层一次性强制,不能靠各团队各自约定。