Prometheus 指标基数爆炸:TSDB 内存与磁盘双满

自研业务埋点上线后 Prometheus 频繁 OOM 重启、TSDB 写放大拖满磁盘。根因是新指标携带用户 ID 等高基数标签,series 数失控。治理标签并设基数上限后恢复。

问题背景

公司的监控体系以 Prometheus 为核心,一台 8C16G 的专用实例采集全部基础设施与应用指标,运行两年一直平稳,日均 active series 稳定在 180 万左右。9 月中旬,业务团队上线了一套自研的用户行为埋点 SDK,按需求"每个用户的请求都要可追溯",把 user_id、order_id、session_id 直接作为标签打进了自定义指标。

上线第一周相安无事。第九天凌晨两点,Prometheus 因 OOM 被内核杀掉,systemd 拉起后跑了四十分钟再次 OOM;白天 Grafana 面板开始间歇性报 query timeout,磁盘容量告警也在同一天触发——一个埋点需求,把整个监控平台拖进了不可用边缘。

故障现象

  • Prometheus 容器反复重启,dmesg 里是标准的 OOM killer 记录,重启后内存爬升速度快于正常水平。
  • 磁盘:/data 分区日增量从平时的 2GB 涨到 9GB,按 30 天保留策略算,剩余空间只够撑四天。
  • 查询侧症状更早出现:面板查询超时,/api/v1/query 的 P99 延迟从 200ms 涨到 30s 以上,部分大范围查询直接失败。
  • prometheus_tsdb_head_series 指标本身也读得出来——head 里的 active series 从 180 万涨到了 1100 万,六天翻了六倍。
  • 业务接口无感知,故障完全局限在监控平台自身;但监控不可用意味着这期间所有告警都是盲的,风险敞口比故障本身更大。

排查过程

第一步确认"涨的是什么"。Prometheus 自带的 TSDB 状态页(/tsdb-status)按指标名列出 series 增量排行,前三名全是业务埋点自定义指标:app_request_duration_seconds、app_user_action_total、app_session_track_total,三者合计贡献了 900 万新 series。点开标签明细,每个 series 的标签组合里都带着 user_id="..." 或 order_id="..."——每来一个新用户、每一笔新订单,就生成一批全新的时间序列,这就是基数(cardinality)爆炸的标准形态。

第二步算清账。按当日活跃用户数 3 万、人均 6 类行为、每类行为 5 个额外标签维度估算,这三组指标一天的 series 增量就是百万级;而基础设施指标(节点、容器、网络)的 series 总集两年才积累到 180 万。增量与存量差了两个数量级,OOM 与写放大只是数学上的必然。

第三步回溯为什么上线前没拦住。查变更记录,埋点 SDK 是业务团队自行发布的,指标通过 Pushgateway 汇入,没有经过监控平台的评审流程;Prometheus 侧也没有配置任何基数防御——sample_limit、label_limit、keep_dropped 相关的 per-target 限制全部是默认关闭状态。也就是说,防线在流程和配置两层同时缺位。

第四步验证恢复路径。直接回滚埋点版本会导致业务侧追溯需求落空,于是采用"收缩而非删除"的方案:与业务团队确认真实需求是"按渠道和地域聚合分析用户行为",而不是"按单个用户 ID 精确追溯"——追溯场景改走日志平台(ELK),指标只保留聚合维度。确定边界后开始整改。

解决方案

  1. 标签治理(治本):三个埋点指标删除 user_id、order_id、session_id 标签,替换为 channel、region、user_tier 等低基数维度;改造后预估 series 总量从 1100 万回落到 210 万,与旧基线同量级。指标语义变化同步更新了 Grafana 面板与告警规则的标签匹配。
  2. 配置防线(防复发):Pushgateway 与各 scrape target 加上 sample_limit: 200000、label_limit: 30、label_value_length_limit: 200;超过限制的 series 被拒收并记录 prometheus_target_scrapes_exceeded_sample_limit_total,拒收即告警——让下一次失控在第一分钟暴露,而不是第九天凌晨。
  3. 资源兜底:Prometheus 实例内存上调至 24G、--storage.tsdb.retention.size=400GB 硬顶保留策略(防止磁盘再被打穿);磁盘告警阈值从 80% 提前到 70%。
  4. 流程补位:自定义指标上线纳入监控平台评审清单,新指标必须先在测试实例跑 24 小时观察基数曲线;把"是否含 ID 类高基数标签"写成评审必答题。

根因分析

直接根因是业务埋点把 user_id 等无限增长值作为标签,series 数随用户与订单量线性爆炸,head 内存与 TSDB 写入双双超出实例承受能力。深层根因是双防线缺位:配置层没有 sample/label 上限,Prometheus 对失控指标来者不拒;流程层没有指标评审,业务团队不知道"标签的每个取值组合都是一条独立时序"这条 Prometheus 的基本成本模型。

预防措施

  • 所有 scrape target 强制 sample_limit/label_limit,拒收指标告警化;
  • 新指标上线走评审流程,测试实例预跑 24 小时观察基数;
  • TSDB 状态页的 series Top10 加入周巡检,环比涨幅超 30% 即预警;
  • 用户级追溯需求一律路由到日志平台,监控指标只做聚合维度——这条边界写进埋点开发规范。

总结

这次故障最值得记下的不是"高基数标签会炸"这个知识点,而是它揭示的监控平台脆弱性模型:Prometheus 的成本不是随"监控对象数"线性增长,而是随"标签取值组合数"增长,一个看起来无害的埋点字段就能把成本模型改写。监控平台自身被拖垮时,全站告警同时失明——这九天的盲飞窗口提醒我们,监控系统的容量与防线,值得用和生产服务同等级别的方式去管理。

延伸阅读

使用 Hugo 构建
主题 Stack 由 Jimmy 设计