# 直方图查询 · histogram_quantile 的真相

> P99 是算出来的还是猜出来的？这一章读 quantile.go 的真实实现与注释——桶内线性插值的假设、落在最高桶时返回第二高桶上界的怪规则、以及那些返回 NaN 的特殊情况。附一套可直接抄的正确写法。

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

# 直方图查询 · histogram_quantile 的真相

[第 1 部分讲过 histogram 只存桶计数](https://daiw.org/manual/observability/metric-types)，分位数是**查询时算出来的**。这一章看它到底怎么算——答案是：**很大程度上靠猜**。

理解这一点，你才不会盲信那个 P99 数字。

## 核心：桶内线性插值

源码注释开门见山（`promql/quantile.go:70`，真实注释）：

> The quantile value is **interpolated assuming a linear distribution within a bucket**.

<Callout type="warn">
  **`histogram_quantile` 的核心假设：**桶内的数据是均匀分布的**。**

  ```
  桶:     le=0.1   le=0.5   le=1.0
  计数:      240      980     1000
                      ↑ P99 落在这个桶里(0.1 ~ 0.5 之间)

  真实分布可能是:
    情况 A:  ▁▁▁▁▁▁▁█   ← 全挤在 0.5 附近 → 真实 P99 ≈ 0.49
    情况 B:  █▁▁▁▁▁▁▁   ← 全挤在 0.1 附近 → 真实 P99 ≈ 0.11
  但 histogram_quantile 假设:  ▄▄▄▄▄▄▄▄  ← 均匀!→ 给出 P99 ≈ 0.3
  ```

  **两种真实情况，它给出同一个答案——而且都是错的。**

  **这就是为什么[桶必须在 SLO 阈值附近加密](https://daiw.org/manual/observability/metric-ranges)**：桶越密，每个桶越窄，“桶内均匀”这个假设的**误差上界**就越小。**桶宽就是你的误差棒。**

  **实战推论：** 如果你的 SLO 是 P99 < 300ms，而桶只有 `[0.25, 0.5]`，那么 `histogram_quantile` 返回的 310ms **完全可能实际是 260ms 或 490ms**——**你根本无法判断 SLO 有没有破**。[第 2 部分反复强调这一点](https://daiw.org/manual/observability/metric-ranges)，现在你看到了源码层面的原因。
</Callout>

## 那个怪规则：落在最高桶时

注释里藏着一条**很多人不知道、但会让你的 P99 看起来很怪**的规则：

> However, if the quantile falls into the **highest bucket**, the **upper bound of the 2nd highest bucket is returned**.

<Callout type="warn">
  **如果分位数落在最高的那个桶（即 `+Inf` 桶）里，返回的是**第二高桶的上界**。**

  ```
  桶:  le=1   le=5   le=+Inf
  计数:  900    980    1000
                       ↑ P99 落在这里(5 ~ ∞)

  返回:  5      ← 第二高桶的上界,不是 ∞、也不是别的
  ```

  **为什么这么设计？** 因为 `+Inf` 桶的上界是**无穷大**，插值无从谈起（你没法在 5 和 ∞ 之间做线性插值）。所以只能**保守地返回已知的最大有限边界**。

  **这个规则的实战后果极其重要：**

  **如果你的 P99 长期“卡”在某个数字上不动（正好等于你倒数第二个桶的边界），那不是巧合——是你的桶不够高，真实的 P99 已经溢出到 `+Inf` 桶里了。** 你看到的 “P99 = 5s” 实际可能是 8s、30s 或者更糟——**你的 P99 被截断了，而且它看起来还挺“稳定”**。

  **这是个危险的静默失败**：图上一条平稳的线，你以为系统很稳，其实是指标失去了分辨能力。**检查方法：看看你的 P99 是不是正好等于某个桶边界，如果是，加更高的桶。**
</Callout>

## 最低桶的特殊处理

注释继续：

> A **natural lower bound of 0 is assumed** if the upper bound of the lowest bucket is greater 0. In that case, interpolation in the lowest bucket happens **linearly between 0 and the upper bound of the lowest bucket**.

**翻译**：如果分位数落在最低桶里，插值范围是 `[0, 最低桶上界]`——**假设了一个自然下界 0**。这对延迟、大小这类**非负**的量是合理的（耗时不可能是负的）。

## 那些返回 NaN 的情况

注释把特殊情况列全了（真实源码）：

| 情况 | 返回 |
| --- | --- |
| 桶里 **0 个观测** | `NaN` |
| 桶**少于 2 个** | `NaN` |
| **最高桶不是 `+Inf`** | `NaN` |
| `q` 是 `NaN` | `NaN` |
| `q < 0` | `-Inf` |
| `q > 1` | `+Inf` |

<Callout type="info">
  **“最高桶不是 `+Inf` 就返回 NaN”这条值得注意**——它意味着**你的 histogram 必须有 `+Inf` 桶**，否则 `histogram_quantile` 完全不工作。好消息是[客户端库会自动加](https://daiw.org/manual/observability/metric-types)，你一般不用操心；但**如果你手工构造直方图指标（比如用 recording rule 或从别的系统转换），别忘了它**。

  **“0 个观测返回 NaN”也很实际**：一个刚启动、还没有任何请求的服务，它的 P99 是 `NaN`。**如果你的告警是 `histogram_quantile(...) > 0.3`，`NaN > 0.3` 是 false，所以不会误报**——这是好事。但反过来，如果你写 `histogram_quantile(...) < 0.3` 来判断“健康”，`NaN < 0.3` 也是 false，**你会误以为它不健康**。[写告警时要考虑 NaN](https://daiw.org/manual/observability/alerting-rules)。
</Callout>

## 正确的写法（可以直接抄）

<Callout type="info">
  **标准姿势：`histogram_quantile(0.99, sum by (le) (rate(..._bucket[5m])))`**

  ```text
  # ✅ 全局 P99(跨所有实例)
  histogram_quantile(0.99,
    sum by (le) (rate(http_request_duration_seconds_bucket[5m]))
  )

  # ✅ 按 service 分的 P99(le 必须在 by 里!)
  histogram_quantile(0.99,
    sum by (le, service) (rate(http_request_duration_seconds_bucket[5m]))
  )

  # ✅ 只看成功请求的 P99(第 2 部分的教训)
  histogram_quantile(0.99,
    sum by (le) (rate(http_request_duration_seconds_bucket{status=~"2.."}[5m]))
  )
  ```

  **三条铁律：**

  1. **`le` 必须出现在 `by` 里** —— 忘了它，桶就被聚合没了，`histogram_quantile` 拿不到分桶信息，结果是垃圾。**这是最常见的错误。**
  2. **先 `rate()` 再 `sum`** —— [和 counter 一样的道理](https://daiw.org/manual/observability/promql-rate)：`_bucket` 也是 counter，必须先 `rate()` 才能正确处理重置。
  3. **顺序是 `histogram_quantile(q, sum(rate(...)))`** —— 从里到外：rate → sum by le → quantile。**记住这个洋葱结构。**

  ```text
  # ❌ 经典错误:le 丢了
  histogram_quantile(0.99, sum by (service) (rate(..._bucket[5m])))
  #                              ↑ 少了 le!结果无意义

  # ❌ 经典错误:顺序反了
  sum by (le) (histogram_quantile(0.99, rate(..._bucket[5m])))
  #            ↑ 先算每个实例的分位数再 sum = 毫无意义(第 1 部分讲过)
  ```
</Callout>

## native histograms 让这一切简单

<Callout type="info">
  **[上一部分提过的 native histograms](https://daiw.org/manual/observability/query-path) 基本终结了本章的大部分纠结：**

  - **桶自动指数生成、又密又广** → [桶内均匀假设的误差极小](https://daiw.org/manual/observability/promql-histograms)，不用你在 SLO 阈值附近手工加密。
  - **不会“溢出到 +Inf 桶”** → [P99 不会被静默截断](https://daiw.org/manual/observability/promql-histograms)。
  - **`le` 不再是标签** → [不会忘记 `by (le)`](https://daiw.org/manual/observability/promql-histograms)，也不吃[基数](https://daiw.org/manual/observability/defining-metrics)。

  查询上，`histogram_quantile` **对 native histogram 直接可用**（语法一样，不用写 `_bucket`）：
  ```text
  histogram_quantile(0.99, sum(rate(http_request_duration_seconds[5m])))
  #                                  ↑ 没有 _bucket 后缀,没有 by (le)
  ```
  **新项目直接上 native histograms**——它把本章讲的坑消掉了一大半。
</Callout>

## 小结与下一章

`histogram_quantile` **本质是估算，不是精确计算**。核心假设是**桶内线性插值**（[真实分布不管挤在桶的哪一头，它都当均匀 → 给同一个（错的）答案](https://daiw.org/manual/observability/promql-histograms)）——**桶宽就是你的误差棒**，这就是[桶必须在 SLO 阈值附近加密](https://daiw.org/manual/observability/metric-ranges)的源码级原因。**怪规则**：[分位数落在最高桶时，返回**第二高桶的上界**](https://daiw.org/manual/observability/promql-histograms)（因为 `+Inf` 没法插值）——**后果是 P99 会被静默截断并“卡”在某个桶边界上，看起来还很稳定**；检查方法是看 P99 是否正好等于某个桶边界。最低桶[假设自然下界 0](https://daiw.org/manual/observability/promql-histograms)。特殊情况：[0 个观测 / 桶少于 2 个 / 最高桶不是 `+Inf` → `NaN`](https://daiw.org/manual/observability/alerting-rules)。**三条铁律**：①**`le` 必须在 `by` 里**（最常见错误）；②[先 `rate()` 再 `sum`](https://daiw.org/manual/observability/promql-rate)；③洋葱结构 `histogram_quantile(q, sum by(le) (rate(...)))`。**[native histograms](https://daiw.org/manual/observability/query-path) 消掉了本章一大半的坑**——新项目直接上。

PromQL 会写了，但写得慢会拖垮 Prometheus。下一章收尾第 4 部分。👉 [查询性能与陷阱](https://daiw.org/manual/observability/promql-performance)。

## 参考资料

- [1] Prometheus 源码 `promql/quantile.go:70`（`BucketQuantile` 的完整注释与特殊情况列表、线性插值实现）。
- [2] Prometheus 官方文档：`histogram_quantile()`、Histograms and summaries、Native histograms。
- [3] 关联[指标类型](https://daiw.org/manual/observability/metric-types)、[桶设计](https://daiw.org/manual/observability/metric-ranges)、[rate 的坑](https://daiw.org/manual/observability/promql-rate)。
