直方图查询 · histogram_quantile 的真相

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

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

直方图查询 · histogram_quantile 的真相

第 1 部分讲过 histogram 只存桶计数,分位数是查询时算出来的。这一章看它到底怎么算——答案是:很大程度上靠猜。

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

核心:桶内线性插值

源码注释开门见山(promql/quantile.go:70,真实注释):

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

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 阈值附近加密:桶越密,每个桶越窄,“桶内均匀”这个假设的误差上界就越小。桶宽就是你的误差棒。

实战推论: 如果你的 SLO 是 P99 < 300ms,而桶只有 [0.25, 0.5],那么 histogram_quantile 返回的 310ms 完全可能实际是 260ms 或 490ms——你根本无法判断 SLO 有没有破。第 2 部分反复强调这一点,现在你看到了源码层面的原因。

那个怪规则:落在最高桶时

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

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

如果分位数落在最高的那个桶(即 +Inf 桶)里,返回的是第二高桶的上界**。**

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

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

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

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

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

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

最低桶的特殊处理

注释继续:

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
最高桶不是 +InfNaN
q 是 NaNNaN
q < 0-Inf
q > 1+Inf

“最高桶不是 +Inf 就返回 NaN”这条值得注意——它意味着你的 histogram 必须有 +Inf 桶,否则 histogram_quantile 完全不工作。好消息是客户端库会自动加,你一般不用操心;但如果你手工构造直方图指标(比如用 recording rule 或从别的系统转换),别忘了它。

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

正确的写法(可以直接抄)

标准姿势:histogram_quantile(0.99, sum by (le) (rate(..._bucket[5m])))

# ✅ 全局 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 一样的道理:_bucket 也是 counter,必须先 rate() 才能正确处理重置。
  3. 顺序是 histogram_quantile(q, sum(rate(...))) —— 从里到外:rate → sum by le → quantile。记住这个洋葱结构。
# ❌ 经典错误:le 丢了
histogram_quantile(0.99, sum by (service) (rate(..._bucket[5m])))
#                              ↑ 少了 le!结果无意义

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

native histograms 让这一切简单

上一部分提过的 native histograms 基本终结了本章的大部分纠结:

查询上,histogram_quantile 对 native histogram 直接可用(语法一样,不用写 _bucket):

histogram_quantile(0.99, sum(rate(http_request_duration_seconds[5m])))
#                                  ↑ 没有 _bucket 后缀,没有 by (le)

新项目直接上 native histograms——它把本章讲的坑消掉了一大半。

小结与下一章

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

PromQL 会写了,但写得慢会拖垮 Prometheus。下一章收尾第 4 部分。👉 查询性能与陷阱。

参考资料

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

本页目录