第 9 部分导论 · 基数爆炸,头号杀手
可观测性事故里,最常见的不是「没数据」,而是「数据太多把自己压垮了」。这一章把前面散落各处的基数线索收拢成一套完整的战术:怎么发现、怎么定位、怎么急救、怎么根治。
第 9 部分导论 · 基数爆炸,头号杀手
技术都讲完了。最后一部分回到最现实的问题——这一切要花多少钱、怎么不失控。
而失控的头号原因只有一个:基数爆炸。它在这个专栏里已经被提了十几次,这一章把所有线索收拢成一套完整的战术。
它为什么是头号杀手
可观测性的事故里,最常见的不是“没数据”,而是“数据太多把自己压垮了”。
典型的事故链:
某人加了个标签(user_id / 原始 URL / 错误消息)
↓
series 数从 50 万涨到 5000 万
↓
Prometheus 内存从 4GB 涨到 40GB → OOM
↓
重启 → WAL 重放几十分钟 → 期间完全没有监控
↓
重放完 → 又 OOM → 循环
↓
【整个组织在这段时间是瞎的】注意最后一步的严重性:基数爆炸不只是“账单变贵”,而是监控系统自己挂掉——恰恰在你最需要它的时候。 第 1 部分说“监控必须比被监控更可靠”,而基数是唯一一个能让监控系统自杀的东西。
为什么它总是发生
因为它有三个恶劣的性质:
- 加标签的人和付代价的人不是同一个 —— 业务开发加个标签很随手(“多个维度总是好的”),而爆炸后是 SRE 半夜起来救火。激励不对齐。
- 它是乘法,不是加法 —— 直觉会骗人:“就多一个标签而已”。但 series 数 = 各标签取值数的乘积,一个 1000 取值的标签让总量 ×1000。
- 它有延迟 —— 加标签的那天没事(
user_id只有几十个测试用户)。上线两周后,用户涨到 10 万,才爆。因果隔了两周,没人会联想到那次改动。
这三条加起来,注定了它会反复发生。 所以你需要的不是“提醒大家小心”,而是一套系统性的防线。
防线一:发现(监控你的监控)
第 1 部分和第 3 部分提过的那些指标,现在组成一套完整的预警:
# ① 最核心的指标:当前活跃 series 数
prometheus_tsdb_head_series
# ② 它在增长吗?(比绝对值更重要!)
deriv(prometheus_tsdb_head_series[1h]) > 0
# ③ 谁在制造?按 job 看每次抓回的样本数
topk(10, scrape_samples_scraped)
# ④ 最灵敏的前兆:持续有新 series 出现
topk(10, rate(scrape_series_added[10m]))scrape_series_added 持续 > 0 是最灵敏的前兆(第 1 部分强调过)——一个稳定的服务应该反复吐出同一批 series,新增该趋近 0。如果它一直在涨,说明每次抓取都在产生新的标签组合 = 标签里混进了会变的东西。
这条能在爆炸前几天就报警,而 head_series 那条往往报的时候已经晚了。
该配的告警:
- alert: CardinalityGrowing
expr: topk(5, rate(scrape_series_added[30m])) > 10
for: 1h
labels: { severity: ticket } # ← ticket 不是 page:它是趋势不是事故
annotations:
summary: "{{ $labels.job }} 持续产生新 series,可能有高基数标签"防线二:定位(揪出元凶)
爆了之后,怎么快速找到是哪个指标、哪个标签?
① Prometheus 自带的 TSDB 状态页 —— /tsdb-status,这是最快的手段。它直接列出:
- series 数最多的指标名 ← 元凶通常在这
- 取值最多的标签名
- 占用内存最多的标签对
② API 查询
# 每个指标名有多少 series
curl -s 'http://prom:9090/api/v1/status/tsdb' | jq '.data.seriesCountByMetricName'
# 每个标签有多少不同取值
curl -s 'http://prom:9090/api/v1/status/tsdb' | jq '.data.labelValueCountByLabelName'③ 在源头 curl —— 第 1 部分那条命令,不需要任何工具:
curl -s http://app:8080/metrics | grep -v '^#' | sed 's/{.*//' | sort | uniq -c | sort -rn | head④ PromQL(最后才用)
count by (__name__) ({__name__=~".+"})⚠️ 上面第 ④ 条本身很贵——它选中了所有 series。在基数已经爆了的实例上跑它,可能直接把它彻底推倒。
优先用 /tsdb-status:它读的是内部统计,不走查询引擎,再危急也能用。这个区别在救火时很要命——很多人第一反应是写个 PromQL 去查,结果把苟延残喘的实例送走了。
防线三:急救(改不了源头时)
第 1 部分的 metric_relabel_configs 是你的灭火器——尤其当元凶是第三方组件、你改不了它的代码时:
metric_relabel_configs:
# 丢掉整个高基数指标
- source_labels: [__name__]
regex: "some_exploding_metric.*"
action: drop
# 或者保留指标,删掉那个高基数标签
- regex: "user_id|request_id|path"
action: labeldrop配合 sample_limit 做硬防护:
scrape_configs:
- job_name: 'untrusted-app'
sample_limit: 10000 # ← 超了就整次丢弃,保护全局第 1 部分说过:sample_limit 的哲学是“一个失控的服务不该有能力打垮整个监控系统”。 每个 job 都该配一个——这是最便宜的保险。
注意 labeldrop 的陷阱:删标签后如果多条 series 塌缩成同一条,会报“重复样本”错误。删之前确认剩下的标签组合仍然唯一。
防线四:根治(在埋点那一刻)
前三道都是补救。唯一的根治在第 2 部分:
- 埋点前算那个乘法 —— 30 秒的事,能省一次事故。
- 黑名单 —— user_id / 原始 path / error_message / 任何“每次都不一样”的值,一律不进标签。
- 真需要高基数就别用指标 —— 用日志 / 追踪 / exemplar。
- Code review 时看一眼标签 —— 把它加进 review checklist。一个标签的代价可能是几十 GB 内存,这值得一次 review。
组织层面的一条建议:让“加标签”这件事带一点点摩擦。 不是设卡,而是让人意识到它有代价——比如 review 时问一句“这个标签有多少种取值?”。大多数基数事故,都是因为加的人从没被问过这个问题。
一个决策速查
小结与下一章
基数爆炸是头号杀手——它不只是账单变贵,而是让监控系统自己挂掉(OOM → 重启 → WAL 重放几十分钟没监控 → 又 OOM),恰恰在你最需要它的时候;它是唯一能让监控系统自杀的东西。它总是发生,因为三个恶劣性质:加标签的人和付代价的人不是同一个(激励不对齐)、它是乘法不是加法(直觉会骗人)、它有延迟(上线两周后才爆,没人联想到那次改动)。四道防线:①发现——scrape_series_added 持续 > 0 是最灵敏的前兆(提前几天预警,而 head_series 报的时候常已晚),配 ticket 级告警;②定位——优先用 /tsdb-status(读内部统计、不走查询引擎,再危急也能用);count by (__name__) 这类查询本身很贵,可能把已爆的实例彻底推倒;③急救——metric_relabel_configs 的 drop/labeldrop + 每个 job 都配 sample_limit;④根治——只在埋点那一刻:算乘法、守黑名单、真需要高基数就用日志/追踪,并让“加标签”带一点摩擦(review 时问“这标签有多少种取值”)。
基数控制住了,但账单还是会涨。下一章看成本。👉 成本与取舍。
参考资料
- [1] Prometheus 官方文档:
/tsdb-status、/api/v1/status/tsdb、sample_limit。 - [2] 关联基数乘法公式、relabel 急救、scrape 元指标。
- [3] Prometheus 源码
tsdb/head.go(prometheus_tsdb_head_series的来源)。