# 四种指标类型 · 以及怎么选

> counter、gauge、histogram、summary —— 四种类型对应四种度量语义。这一章讲清各自适用什么、命名约定，并重点拆解那个最经典的实战决策：histogram 还是 summary（答案几乎总是 histogram，原因是可聚合性）。

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

# 四种指标类型 · 以及怎么选

[上一章](https://daiw.org/manual/observability/data-model)说 series 的值就是个 `float64`。那“类型”是什么？**类型是给人和查询看的语义约定**——它决定了你**该怎么查**这个指标。选错类型，后面全是坑。

## 四种类型

看解析器认哪些类型（真实源码 `model/textparse/promparse.go:337`）：

```go
switch s := yoloString(p.text); s {
case "counter":   p.mtype = model.MetricTypeCounter
case "gauge":     p.mtype = model.MetricTypeGauge
case "histogram": p.mtype = model.MetricTypeHistogram
case "summary":   p.mtype = model.MetricTypeSummary
case "untyped":   p.mtype = model.MetricTypeUnknown
default:          return EntryInvalid, fmt.Errorf("invalid metric type %q", s)
}
```

<Callout type="info">
  **四种类型，四种语义：**

  | 类型 | 语义 | 典型例子 | 怎么查 |
  | --- | --- | --- | --- |
  | **counter** | **只增不减**（重启归零） | 请求总数、错误总数、发送字节数 | **必须** `rate()`/`increase()` |
  | **gauge** | **可增可减**的瞬时值 | 内存占用、队列长度、并发连接数 | 直接看，或 `avg`/`max` |
  | **histogram** | **分桶**统计分布 | 请求延迟、响应体大小 | `histogram_quantile()` |
  | **summary** | **客户端算好**的分位数 | 同上（但**慎用**） | 直接读 quantile 标签 |

  **最重要的一条经验：类型不是“标注”，而是“承诺”。** 你把一个会下降的值标成 counter，`rate()` 就会把每次下降当成“计数器重置”从而给出**离谱的结果**。反过来，把 counter 标成 gauge，你就失去了 `rate()` 的正确语义。**类型错了，查询必然错。**
</Callout>

## counter：为什么必须用 rate()

<Callout type="warn">
  **counter 的**绝对值几乎没有意义**——“这个进程自启动以来处理了 10 万个请求”有什么用？你要的是“**现在每秒多少请求**”，这必须靠 `rate()` 算。**

  ```
  # ❌ 没意义:一个自启动累计的数,进程重启就归零
  http_requests_total

  # ✅ 有意义:过去 5 分钟的平均每秒请求数
  rate(http_requests_total[5m])
  ```

  **`rate()` 还会自动处理“计数器重置”**：进程重启后计数器归零，`rate()` 检测到值下降就知道发生了重置，会正确地把它当作“从 0 重新开始”而不是算出一个巨大的负数。这个自动处理是 counter 语义的核心价值——**它让“进程会重启”这件事对查询透明**。

  **命名约定：counter 应以 `_total` 结尾**（`http_requests_total`）。这不只是风格——[OpenMetrics 标准](https://daiw.org/manual/observability/exposition-format)把它规范化了，工具链会依赖这个后缀。
</Callout>

## gauge：直接看，但小心聚合

gauge 是瞬时值，可上可下：内存占用、队列深度、温度、当前并发数。查询上直接看就行，**但聚合时要想清楚语义**：

- `sum(node_memory_used_bytes)` —— 合理，总内存用量。
- `sum(node_cpu_temperature)` —— **荒谬**，温度求和没有物理意义。该用 `avg` 或 `max`。

**gauge 的坑在于“能算不代表该算”**——PromQL 不会拦你把温度加起来。想清楚这个量**能不能相加**，是选聚合函数的唯一标准。

## histogram vs summary：最经典的实战决策

这是四种类型里**唯一需要认真权衡**的地方，也是新手最常踩的坑。两者都用来测**分布**（比如延迟），但机制根本不同：

### histogram：客户端只数桶

一个 histogram 暴露出来其实是**三组 series**：

```
# 落在各个桶里的累计计数(le = less than or equal)
http_request_duration_seconds_bucket{le="0.1"}   240
http_request_duration_seconds_bucket{le="0.5"}   980
http_request_duration_seconds_bucket{le="1"}     1000
http_request_duration_seconds_bucket{le="+Inf"}  1002
# 所有观测值之和 + 观测次数
http_request_duration_seconds_sum    312.5
http_request_duration_seconds_count  1002
```

客户端**只做一件事：把观测值扔进对应的桶，给桶计数**。分位数是**查询时**由 `histogram_quantile()` 从桶里**估算**出来的。

### summary：客户端算好分位数

```
http_request_duration_seconds{quantile="0.5"}   0.23
http_request_duration_seconds{quantile="0.9"}   0.71
http_request_duration_seconds{quantile="0.99"}  1.84
http_request_duration_seconds_sum    312.5
http_request_duration_seconds_count  1002
```

客户端在**本地维护一个滑动窗口**、算出分位数直接暴露。

<Callout type="warn">
  **决定性的区别：**summary 的分位数不能跨实例聚合，histogram 的可以**。这一条几乎单独决定了选型。**

  想想你有 10 个 pod，要算**整个服务**的 P99：

  - **summary** —— 每个 pod 给你一个“我自己的 P99”。**10 个 P99 求平均 = 毫无意义的数**。P99 是个分位数，不是可加的量——“平均的 P99”在数学上什么都不是。**你无法从中还原出全局 P99**，除非只有一个实例。
  - **histogram** —— 每个 pod 给你**桶计数**。桶计数**是可加的**！`sum by (le) (rate(..._bucket[5m]))` 把 10 个 pod 的桶加起来，得到**全局的分布**，再 `histogram_quantile(0.99, ...)` 算出**真正的全局 P99**。

  ```
  # ✅ 正确的全局 P99(histogram)
  histogram_quantile(0.99, sum by (le) (rate(http_request_duration_seconds_bucket[5m])))
  ```

  **在任何多实例的服务里（也就是几乎所有服务），这一条就宣判了 summary 出局。** 这就是为什么业界的默认建议是：**优先用 histogram**。
</Callout>

### 那 summary 还有什么用

公平地说，summary 也有它的场合：

| 维度 | histogram | summary |
| --- | --- | --- |
| **跨实例聚合** | ✅ 可以（桶可加） | ❌ **不行**（致命） |
| **精度** | 受桶边界限制（估算） | 客户端精确算 |
| **桶要预先定好** | ✅ 是（**选不好就废**） | 不用 |
| **客户端开销** | 低（就是几个计数器） | 高（要维护滑动窗口排序） |
| **服务端开销** | 桶多则 series 多 | series 少 |

**summary 只适合：单实例、且你需要精确分位数、且能接受不能聚合。** 现实里这个交集很小。

<Callout type="info">
  **histogram 的代价是“**桶必须预先定好**”，而且选不好就白搭。** 如果你的桶是 `[0.1, 0.5, 1, 5]`，而真实延迟全在 0.6~0.9 之间，那么 P99 只能被估算成“在 0.5 和 1 之间的某处”——[线性插值](https://daiw.org/manual/observability/promql-histograms)出来的数**几乎没有参考价值**。

  **实战原则：桶要覆盖你关心的区间，且在 SLO 阈值附近加密。** 如果你的 SLO 是“P99 < 300ms”，那 300ms 附近必须有桶（如 `[0.1, 0.2, 0.25, 0.3, 0.35, 0.5, 1, ...]`），否则你根本判断不了 SLO 有没有破。**桶的设计 = 你想问的问题的精度上限。**[第 2 部分](https://daiw.org/manual/observability/metric-ranges)会给常见场景的桶推荐。

  Prometheus 3.x 的 **native histograms** 正是来解这个痛点：桶**自动、指数式**生成，不用预先定，精度还高得多。这是近年最重要的演进，[第 3 部分](https://daiw.org/manual/observability/three-pillars)细讲。
</Callout>

## 一张选型速查

```mermaid
flowchart TD
    Q1{"这个量会减少吗?"} -->|"不会,只增"| C["counter<br/>命名 _total,用 rate()"]
    Q1 -->|"会"| Q2{"要看分布吗?<br/>(延迟/大小)"}
    Q2 -->|"否,看瞬时值"| G["gauge<br/>直接看,聚合想清语义"]
    Q2 -->|"是"| Q3{"多实例?"}
    Q3 -->|"是(几乎总是)"| H["histogram<br/>桶在 SLO 阈值附近加密"]
    Q3 -->|"单实例且要精确分位数"| S["summary<br/>(罕见)"]
    style C fill:#3b82f6,color:#fff
    style G fill:#22c55e,color:#fff
    style H fill:#f59e0b,color:#000
    style S fill:#94a3b8,color:#fff
```

## 小结与下一章

四种类型是**语义承诺**，决定你该怎么查：**counter**（只增，[命名 `_total`，必须 `rate()`](https://daiw.org/manual/observability/metric-types)；`rate()` [自动处理重启归零](https://daiw.org/manual/observability/metric-types)）、**gauge**（瞬时值，[聚合前想清楚“这个量能不能相加”](https://daiw.org/manual/observability/metric-types)）、**histogram**（分桶：`_bucket{le}`/`_sum`/`_count`，查询时 `histogram_quantile()` 估算）、**summary**（客户端算好分位数）。**最关键的实战决策是 histogram vs summary，答案几乎总是 histogram**——因为[**summary 的分位数不能跨实例聚合**](https://daiw.org/manual/observability/metric-types)（“平均的 P99”毫无意义），而 histogram 的**桶计数可加**，能算出真正的全局分位数。histogram 的代价是[桶要预先定好且选不好就废](https://daiw.org/manual/observability/metric-ranges)——**桶必须在 SLO 阈值附近加密**；[native histograms](https://daiw.org/manual/observability/three-pillars) 正是来解这个痛的。

类型定了，指标怎么**吐出去**？下一章看暴露格式与 OpenMetrics。👉 [暴露格式](https://daiw.org/manual/observability/exposition-format)。

## 参考资料

- [1] Prometheus 源码 `model/textparse/promparse.go:337`（四种类型的解析）、`openmetricsparse.go`、`protobufparse.go:654`（`_count`/`_sum`/`_bucket` 后缀）。
- [2] Prometheus 官方文档：Metric types、Histograms and summaries。
- [3] 关联[数据模型](https://daiw.org/manual/observability/data-model)、[指标体系方法论](https://daiw.org/manual/observability/metric-ranges)。
