# 怎么定义指标 · 命名、标签与基数红线

> 知道该测什么之后，怎么把它定义好？这一章给可执行的规则：命名规范（单位后缀是硬要求）、标签设计的取舍，以及那条必须守住的基数红线——含一个能让你立刻算出「这个指标要花多少钱」的公式。

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

# 怎么定义指标 · 命名、标签与基数红线

[方法论](https://daiw.org/manual/observability/metrics-methodology)告诉你**该测什么**，这一章讲**怎么把它定义好**。这是最“手艺活”的一章：规则不多，但每条都是血泪换来的——**定义指标是个几乎不可逆的决定**，一旦上线、被面板和告警依赖，改名字就是一场迁移。

## 命名规范

Prometheus 的命名约定不是风格问题，**有些是硬要求**：

<Callout type="info">
  **格式：`<命名空间>_<名词>_<单位>_<后缀>`**

  ```
  http_requests_total                    ← counter 必须 _total
  http_request_duration_seconds          ← 单位必须写全:seconds 不是 ms
  node_memory_usage_bytes                ← bytes 不是 MB
  process_cpu_seconds_total              ← 累计 CPU 秒数
  queue_depth                            ← gauge 不加 _total
  ```

  **规则清单：**

  | 规则 | 为什么 |
  | --- | --- |
  | **counter 以 `_total` 结尾** | [OpenMetrics 硬性规定](https://daiw.org/manual/observability/exposition-format)，工具链依赖它 |
  | **单位写在名字里，且用基本单位** | `seconds` 不用 `milliseconds`，`bytes` 不用 `MB` |
  | **单位用复数** | `_seconds`、`_bytes`（约定） |
  | **同名指标单位必须一致** | 否则聚合出来是垃圾 |
  | **用 `_` 不用驼峰** | 全生态约定 |
  | **前缀用命名空间** | `http_`、`node_`、`myapp_`——避免撞名 |

  **“用基本单位”这条被低估了。** 为什么不用毫秒？因为**你会忘**。三个月后你看到 `http_request_duration` = 250，这是 250 毫秒还是 250 秒？**名字里带单位，读的人永远不用猜**。而统一成秒（而非各处混用 ms/s/μs），让**跨指标的运算永远正确**——你可以直接把两个 `_seconds` 相加，不用转换。Grafana 会负责把 0.25 显示成 “250ms”，**展示是展示的事，存储永远用基本单位**。
</Callout>

<Callout type="warn">
  **一个真实的坑：改指标名的代价极高。** 指标名一旦上线，就被写进了：面板、告警规则、[recording rules](https://daiw.org/manual/observability/three-pillars)、别人的看板、别人的告警、事故复盘文档、甚至某些自动化脚本。改名意味着**所有这些都要同步改**，而且**历史数据不会跟着改名**——你会得到一条在某个时间点断掉的曲线，以及一条从那时才开始的新曲线。

  **所以：第一次就想清楚。** 上线前花十分钟对着这张表检查一遍，比日后迁移省一百倍力气。
</Callout>

## 标签设计：自由度与成本的交易

[标签是提问的自由度](https://daiw.org/manual/observability/why-observability)，但每个标签都在**放大 series 数**。设计标签就是在做这笔交易。

<Callout type="info">
  **三个问题，决定一个标签该不该加：**

  1. **我会用它来筛选或聚合吗？** —— 如果你想不出一个具体的查询会用到它，**别加**。“以后可能有用”是[基数爆炸](https://daiw.org/manual/observability/defining-metrics)最常见的借口。
  2. **它的取值数量有上界吗？** —— `status`（约 10 种）、`method`（约 8 种）有界；`user_id`、`url`、`error_message` **无界**。**无界的绝对不能加。**
  3. **它和别的标签正交吗？** —— 如果 `region` 和 `datacenter` 一一对应，加两个是浪费（series 数不变但存储翻倍）。

  **一个好用的经验法则：如果你无法在心里数出这个标签有多少种取值，它就不该是标签。**
</Callout>

## 基数红线：一个能立刻算出成本的公式

这是本章最实用的东西。

<Callout type="warn">
  **series 数 = 各标签取值数的**乘积** × 实例数。这是**乘法**，不是加法——这就是它会爆炸的原因。**

  举例，一个 HTTP 服务的 [RED 指标](https://daiw.org/manual/observability/red-method)：

  ```
  http_request_duration_seconds_bucket{method, route, status, le, instance}

  method:    5   (GET/POST/PUT/DELETE/PATCH)
  route:    50   (50 个路由)
  status:    8   (200/201/400/401/403/404/500/503)
  le:       12   (11 个桶 + Inf)  ← 别忘了 histogram 的桶也是标签!
  instance: 20   (20 个 pod)
  ─────────────────────────────────────
  series 数 = 5 × 50 × 8 × 12 × 20 = 480,000 条
  ```

  **48 万条 series，一个指标。** 而 Prometheus 的经验值是：**每条活跃 series 大约吃 1~3 KB 内存**（索引 + [标签](https://daiw.org/manual/observability/data-model) + 未落盘的 chunk）。

  ```
  480,000 × 2KB ≈ 1 GB 内存   ← 就这一个指标
  ```

  **现在看看加一个“无界”标签会怎样：**

  ```
  如果 route 用了原始 path(带订单 ID),假设一天 10 万个不同订单:
  series 数 = 5 × 100,000 × 8 × 12 × 20 = 960,000,000 条
  ≈ 1.9 TB 内存 → Prometheus 当场 OOM
  ```

  **这就是[第 1 部分那个 `scrape_series_added` 持续 > 0](https://daiw.org/manual/observability/scrape-loop) 的后果**。**记住这个乘法公式，埋点前算一遍**——它只需要 30 秒，能救你一次线上事故。
</Callout>

### 自己拖一下

光看公式没有手感。拖动下面的滑块，看这个乘法怎么把你送进 OOM：

<CardinalityExplorer />

## 绝对不能进标签的东西

<Callout type="warn">
  **黑名单（记住它）：**

  | ❌ 绝不 | 为什么 | 该放哪 |
  | --- | --- | --- |
  | `user_id`、`session_id`、`request_id`、`trace_id` | 无界 | [日志 / 追踪](https://daiw.org/manual/observability/three-pillars) |
  | 原始 URL path（带 ID） | 无界 | 用[路由模板](https://daiw.org/manual/observability/red-method) `/orders/:id` |
  | `error_message`、异常堆栈 | 无界 + 长 | [日志](https://daiw.org/manual/observability/three-pillars) |
  | 时间戳、日期 | 无界 | 时间是 series 的维度，**不是标签** |
  | email、IP（客户端的） | 无界 | 日志 |
  | 完整的 SQL 语句 | 无界 | 日志 / APM |

  **共同点：它们都是“标识符”或“自由文本”，不是“类别”。** 一个可靠的判据：**标签的取值应该是一个你能提前列举的、有限的“枚举”**。`status` 是枚举，`user_id` 不是。

  **“时间戳当标签”这条值得特别提醒**——听起来很蠢没人会犯，但它会**伪装**着出现：比如把 `version="2026-07-15-build-1234"` 当标签，每次发布就产生一批新 series；或者 `batch_id`、`job_run_id` 这类每次运行都变的东西。**任何“每次都不一样”的值，都是时间戳的变体。**
</Callout>

## 那些“确实需要高基数”的场景怎么办

<Callout type="info">
  **如果你真的需要按 user_id 查询，答案是：**别用指标**。** 这不是妥协，而是[用对工具](https://daiw.org/manual/observability/three-pillars)：

  - **要按用户查** → [日志](https://daiw.org/manual/observability/three-pillars)（Loki/ES）或[追踪](https://daiw.org/manual/observability/three-pillars)。它们**为高基数而生**：Loki 只索引少量标签，内容全文检索；追踪本来就是一条条独立的记录。
  - **要看“最差的那些用户”** → [exemplar](https://daiw.org/manual/observability/three-pillars)：指标里挂几个**样例** trace_id，从曲线一键跳到那条具体请求。**这是“聚合 + 样例”的混合模式**，鱼和熊掌兼得的正确姿势。
  - **要看 Top-N**（如最慢的 10 个接口） → 这个可以用指标，因为**接口数是有界的**。但“最慢的 10 个**用户**”不行。
  - **确实要高基数指标** → 这时该考虑的是[专门的高基数后端](https://daiw.org/manual/observability/three-pillars)（如某些商业 APM），而不是硬塞进 Prometheus。**Prometheus 的整个设计（[倒排索引](https://daiw.org/manual/observability/data-journey)、[内存里的 Head](https://daiw.org/manual/observability/three-pillars)）都建立在“series 数有界”这个假设上**，你打破它就是在跟它的架构对抗。
</Callout>

## 一个 checklist

埋点前**逐条过一遍**，30 秒：

```
□ 名字符合规范?(_total 后缀、单位写全、基本单位、命名空间前缀)
□ 类型选对?(会减少的不是 counter;要看分布用 histogram 不用 summary)
□ 每个标签我都能说出一个会用到它的具体查询?
□ 每个标签的取值数我都能数出来?(数不出来 = 别加)
□ 算过乘法了?(各标签取值数 × 实例数 → 心里有数)
□ histogram 的桶数算进去了?(桶也是标签!)
□ 桶边界覆盖了我的 SLO 阈值?
□ 黑名单里的东西一个都没混进来?
```

## 小结与下一章

**命名**：`<命名空间>_<名词>_<单位>_<后缀>`——[counter 必须 `_total`](https://daiw.org/manual/observability/exposition-format)、**单位写进名字且用基本单位**（[`seconds` 不用 ms，因为你会忘；统一基本单位让跨指标运算永远正确，展示交给 Grafana](https://daiw.org/manual/observability/defining-metrics)）；**改名代价极高**（[面板/告警/规则/文档全要改，历史数据还不跟着改名](https://daiw.org/manual/observability/defining-metrics)），第一次就想清楚。**标签设计**问三个问题：[会用它筛选吗、取值有上界吗、和别的标签正交吗](https://daiw.org/manual/observability/defining-metrics)——**数不出取值数的东西不该是标签**。**基数红线**：**series 数 = 各标签取值数的乘积 × 实例数**（是**乘法**！），每条活跃 series 约 **1~3 KB 内存**——[一个普通 RED 指标就 48 万条 ≈ 1GB；换成原始 path 就是 9.6 亿条 ≈ 1.9TB 当场 OOM](https://daiw.org/manual/observability/defining-metrics)。**黑名单**：[user_id / 原始 path / error_message / 时间戳及其变体（version、batch_id 等“每次都不一样”的值）](https://daiw.org/manual/observability/defining-metrics)一律不进标签。真需要高基数？**别用指标**——[用日志 / 追踪 / exemplar](https://daiw.org/manual/observability/three-pillars)。

规范有了，但还有个最实际的问题：**多少算正常？** 下一章给具体数字，收尾第 2 部分。👉 [常见指标的合理范围](https://daiw.org/manual/observability/metric-ranges)。

## 参考资料

- [1] Prometheus 官方文档：Metric and label naming、Best practices — Instrumentation、Histograms and summaries。
- [2] OpenMetrics 规范（`_total` 后缀与单位约定）。
- [3] 关联[数据模型](https://daiw.org/manual/observability/data-model)、[RED 埋点](https://daiw.org/manual/observability/red-method)、[relabeling 基数急救](https://daiw.org/manual/observability/relabeling)。
