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

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

- 作者：David（道雾轩）
- 专栏：指标采集与观测（https://daiw.org/manual/observability.md）
- 最后更新：2026-07-15
- 原文：https://daiw.org/manual/observability/cardinality
- 转载与引用：请注明出处并附原文链接（https://daiw.org/about/copyright）

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

技术都讲完了。最后一部分回到最现实的问题——**这一切要花多少钱、怎么不失控**。

而失控的头号原因只有一个：**基数爆炸**。它在这个专栏里已经被提了十几次，这一章把所有线索**收拢成一套完整的战术**。

## 它为什么是头号杀手

<Callout type="warn">
  **可观测性的事故里，最常见的不是“没数据”，而是“**数据太多把自己压垮了**”。**

  典型的事故链：

  ```
  某人加了个标签(user_id / 原始 URL / 错误消息)
       ↓
  series 数从 50 万涨到 5000 万
       ↓
  Prometheus 内存从 4GB 涨到 40GB → OOM
       ↓
  重启 → WAL 重放几十分钟 → 期间完全没有监控
       ↓
  重放完 → 又 OOM → 循环
       ↓
  【整个组织在这段时间是瞎的】
  ```

  **注意最后一步的严重性：基数爆炸不只是“账单变贵”，而是[**监控系统自己挂掉**](https://daiw.org/manual/observability/head-and-wal)——恰恰在你最需要它的时候。** [第 1 部分说“监控必须比被监控更可靠”](https://daiw.org/manual/observability/prometheus-architecture)，而基数是**唯一一个能让监控系统自杀的东西**。
</Callout>

## 为什么它总是发生

<Callout type="info">
  **因为它有三个恶劣的性质：**

  - **加标签的人和付代价的人不是同一个** —— 业务开发加个标签很随手（“多个维度总是好的”），而爆炸后是 SRE 半夜起来救火。**激励不对齐。**
  - **它是[乘法](https://daiw.org/manual/observability/defining-metrics)，不是加法** —— 直觉会骗人：“就多一个标签而已”。但 [series 数 = 各标签取值数的**乘积**](https://daiw.org/manual/observability/defining-metrics)，一个 1000 取值的标签让总量 **×1000**。
  - **它有延迟** —— 加标签的那天没事（`user_id` 只有几十个测试用户）。**上线两周后，用户涨到 10 万，才爆**。**因果隔了两周，没人会联想到那次改动。**

  **这三条加起来，注定了它会反复发生。** 所以你需要的不是“提醒大家小心”，而是**一套系统性的防线**。
</Callout>

## 防线一：发现（监控你的监控）

<Callout type="info">
  **[第 1 部分](https://daiw.org/manual/observability/scrape-loop)和[第 3 部分](https://daiw.org/manual/observability/blocks-compaction)提过的那些指标，现在组成一套完整的预警：**

  ```text
  # ① 最核心的指标:当前活跃 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 部分强调过](https://daiw.org/manual/observability/scrape-loop)）——**一个稳定的服务应该反复吐出同一批 series，新增该趋近 0**。如果它一直在涨，说明**每次抓取都在产生新的标签组合** = 标签里混进了[会变的东西](https://daiw.org/manual/observability/defining-metrics)。

  **这条能在爆炸前几天就报警，而 `head_series` 那条往往报的时候已经晚了。**

  **该配的告警：**
  ```yaml
  - alert: CardinalityGrowing
    expr: topk(5, rate(scrape_series_added[30m])) > 10
    for: 1h
    labels: { severity: ticket }     # ← ticket 不是 page:它是趋势不是事故
    annotations:
      summary: "{{ $labels.job }} 持续产生新 series,可能有高基数标签"
  ```
</Callout>

## 防线二：定位（揪出元凶）

<Callout type="info">
  **爆了之后，怎么快速找到是哪个指标、哪个标签？**

  **① Prometheus 自带的 TSDB 状态页** —— `/tsdb-status`，**这是最快的手段**。它直接列出：
  - **series 数最多的指标名** ← 元凶通常在这
  - **取值最多的标签名**
  - **占用内存最多的标签对**

  **② API 查询**
  ```bash
  # 每个指标名有多少 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 部分那条命令](https://daiw.org/manual/observability/exposition-format)，不需要任何工具：
  ```bash
  curl -s http://app:8080/metrics | grep -v '^#' | sed 's/{.*//' | sort | uniq -c | sort -rn | head
  ```

  **④ PromQL（最后才用）**
  ```text
  count by (__name__) ({__name__=~".+"})
  ```
</Callout>

<Callout type="warn">
  **⚠️ 上面第 ④ 条本身很贵**——[它选中了所有 series](https://daiw.org/manual/observability/promql-performance)。**在基数已经爆了的实例上跑它，可能直接把它彻底推倒。**

  **优先用 `/tsdb-status`**：它读的是**内部统计**，不走查询引擎，**再危急也能用**。**这个区别在救火时很要命**——很多人第一反应是写个 PromQL 去查，结果把苟延残喘的实例送走了。
</Callout>

## 防线三：急救（改不了源头时）

<Callout type="warn">
  **[第 1 部分的 `metric_relabel_configs`](https://daiw.org/manual/observability/relabeling) 是你的灭火器——尤其当元凶是第三方组件、你改不了它的代码时：**

  ```yaml
  metric_relabel_configs:
    # 丢掉整个高基数指标
    - source_labels: [__name__]
      regex: "some_exploding_metric.*"
      action: drop
    # 或者保留指标,删掉那个高基数标签
    - regex: "user_id|request_id|path"
      action: labeldrop
  ```

  **配合 [`sample_limit`](https://daiw.org/manual/observability/scrape-loop) 做硬防护：**
  ```yaml
  scrape_configs:
    - job_name: 'untrusted-app'
      sample_limit: 10000      # ← 超了就整次丢弃,保护全局
  ```

  **[第 1 部分说过](https://daiw.org/manual/observability/scrape-loop)：`sample_limit` 的哲学是“一个失控的服务不该有能力打垮整个监控系统”。** **每个 job 都该配一个**——这是最便宜的保险。

  **注意 [`labeldrop` 的陷阱](https://daiw.org/manual/observability/relabeling)**：删标签后如果多条 series 塌缩成同一条，会报“重复样本”错误。**删之前确认剩下的标签组合仍然唯一。**
</Callout>

## 防线四：根治（在埋点那一刻）

<Callout type="info">
  **前三道都是补救。唯一的根治在[第 2 部分](https://daiw.org/manual/observability/defining-metrics)：**

  - **[埋点前算那个乘法](https://daiw.org/manual/observability/defining-metrics)** —— 30 秒的事，能省一次事故。
  - **[黑名单](https://daiw.org/manual/observability/defining-metrics)** —— user_id / 原始 path / error_message / 任何“每次都不一样”的值，**一律不进标签**。
  - **[真需要高基数就别用指标](https://daiw.org/manual/observability/defining-metrics)** —— 用[日志 / 追踪 / exemplar](https://daiw.org/manual/observability/exemplars)。
  - **Code review 时看一眼标签** —— **把它加进 review checklist**。一个标签的代价可能是几十 GB 内存，**这值得一次 review**。

  **组织层面的一条建议：让“加标签”这件事带一点点摩擦。** 不是设卡，而是让人**意识到它有代价**——比如 review 时问一句“**这个标签有多少种取值？**”。**大多数基数事故，都是因为加的人从没被问过这个问题。**
</Callout>

## 一个决策速查

```mermaid
flowchart TD
    A["我想加个标签"] --> B{"能数出取值数吗?"}
    B -->|"不能"| B1["❌ 别加<br/>→ 放日志/追踪"]
    B -->|"能"| C{"算过乘法了吗?<br/>(× 其他标签 × 实例数)"}
    C -->|"结果 > 10 万"| C1["⚠️ 三思<br/>→ 能不能聚合掉一个维度?"]
    C -->|"可接受"| D{"我能说出一个<br/>会用它的具体查询吗?"}
    D -->|"不能"| B1
    D -->|"能"| E["✅ 加"]
    style B1 fill:#ef4444,color:#fff
    style E fill:#22c55e,color:#fff
```

## 小结与下一章

**基数爆炸是头号杀手**——它不只是账单变贵，而是**让监控系统自己挂掉**（[OOM → 重启 → WAL 重放几十分钟没监控 → 又 OOM](https://daiw.org/manual/observability/head-and-wal)），恰恰在你最需要它的时候；[它是唯一能让监控系统自杀的东西](https://daiw.org/manual/observability/prometheus-architecture)。**它总是发生，因为三个恶劣性质**：[加标签的人和付代价的人不是同一个（激励不对齐）、它是**乘法**不是加法（直觉会骗人）、**它有延迟**（上线两周后才爆，没人联想到那次改动）](https://daiw.org/manual/observability/defining-metrics)。**四道防线**：①**发现**——[`scrape_series_added` 持续 > 0 是最灵敏的前兆](https://daiw.org/manual/observability/scrape-loop)（提前几天预警，而 `head_series` 报的时候常已晚），配 **ticket 级**告警；②**定位**——[**优先用 `/tsdb-status`**（读内部统计、不走查询引擎，再危急也能用）；`count by (__name__)` 这类查询本身很贵，**可能把已爆的实例彻底推倒**](https://daiw.org/manual/observability/promql-performance)；③**急救**——[`metric_relabel_configs` 的 `drop`/`labeldrop` + **每个 job 都配 `sample_limit`**](https://daiw.org/manual/observability/relabeling)；④**根治**——[只在埋点那一刻：算乘法、守黑名单、真需要高基数就用日志/追踪](https://daiw.org/manual/observability/defining-metrics)，**并让“加标签”带一点摩擦**（review 时问“这标签有多少种取值”）。

基数控制住了，但账单还是会涨。下一章看成本。👉 [成本与取舍](https://daiw.org/manual/observability/cost-control)。

## 参考资料

- [1] Prometheus 官方文档：`/tsdb-status`、`/api/v1/status/tsdb`、`sample_limit`。
- [2] 关联[基数乘法公式](https://daiw.org/manual/observability/defining-metrics)、[relabel 急救](https://daiw.org/manual/observability/relabeling)、[scrape 元指标](https://daiw.org/manual/observability/scrape-loop)。
- [3] Prometheus 源码 `tsdb/head.go`（`prometheus_tsdb_head_series` 的来源）。
