第 9 部分导论 · 基数爆炸,头号杀手

可观测性事故里,最常见的不是「没数据」,而是「数据太多把自己压垮了」。这一章把前面散落各处的基数线索收拢成一套完整的战术:怎么发现、怎么定位、怎么急救、怎么根治。

作者 David更新于 第 50 篇(共 52 篇)

第 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 部分:

组织层面的一条建议:让“加标签”这件事带一点点摩擦。 不是设卡,而是让人意识到它有代价——比如 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 时问“这标签有多少种取值”)。

基数控制住了,但账单还是会涨。下一章看成本。👉 成本与取舍。

参考资料

本页目录