bigywater.tech
微服务 · 69

直方图:桶的选取、分位数误差与 histogram_quantile 的近似

Prometheus 的 histogram_quantile 有多准?桶该怎么划分?数据超出最大桶时会怎样?rate 的窗口怎么影响分位数?桶越多序列数增长多快?这篇用 Prometheus 3.15.0 和同一批 10 万个样本逐项量出来。

+43% vs +0%
20 个每 50 ms 一个的线性桶与 9 个 ×2 指数桶估计 p50 的相对误差(真实 p50 = 20.1 ms)
1000.0 vs 5996.7
全部请求都超出最大有限桶时,histogram_quantile(0.99) 的结果与 sum / count 的平均值(ms)
3600
150 个标签组合 × 20 个桶的 histogram 在 Prometheus 里的时间序列条数

桶的划分

实测 · 本机执行输出
  真实分位数(ms):p50 = 20.1,  p90 = 58.3,  p99 = 524.7,  p99.9 = 961.9
  桶划分                                      | 有限桶个数 | p50 估计(相对误差) | p90 估计(相对误差) | p99 估计(相对误差) | p99.9 估计(相对误差)
  -----------------------------------------+-------+--------------+--------------+--------------+---------------
  prometheus_client 默认(×1000)              | 14    | 21.2(+5%)    | 61.5(+5%)    | 523.7(-0%)   | 952.2(-1%)    
  线性,每 50 ms 一个                            | 20    | 28.9(+43%)   | 66.9(+15%)   | 525.3(+0%)   | 956.5(-1%)    
  指数,×2(5 到 1280)                          | 9     | 20.2(+0%)    | 67.0(+15%)   | 400.8(-24%)  | 1196.8(+24%)  
  指数,×1.3(5 到约 1000)                       | 21    | 20.3(+1%)    | 59.5(+2%)    | 489.8(-7%)   | 950.2(-1%)    
  只有两个桶:100、1000                           | 2     | 51.7(+157%)  | 93.0(+60%)   | 723.2(+38%)  | 972.3(+1%)    
  围绕慢请求加密:10、20、40、80、200、400、600、800、1000 | 9     | 20.2(+0%)    | 67.0(+15%)   | 455.6(-13%)  | 952.3(-1%)    
  桶越宽,线性插值的误差越大;最大误差出现在分位数落进宽桶的时候。桶边界要覆盖要关心的分位数和阈值附近(例如 SLO 的 500 ms),并不是桶越多越好

误差最大出现在分位数落进宽桶时;桶的位置比数量重要。

边界与 +Inf

实测 · 本机执行输出
  数据                 | p50 估计(ms) | p99 估计(ms) | 用 sum / count 算的平均值(ms)
  -------------------+------------+------------+------------------------
  (a)全部 3000~9000 ms | 1000.0     | 1000.0     | 5996.7                 
  (b)全部恰好 100 ms     | 75.0       | 99.5       | 100.0                  
  (c)全部 100.001 ms   | 175.0      | 248.5      | 100.0                  
  超出最大有限桶的数据都进了 +Inf 桶,histogram_quantile 在落进 +Inf 桶时只能返回最大有限桶的上界(这里 1000),平均值(sum / count)才反映真实水平;桶边界是“小于等于”,恰好 100 ms 的请求进 le=100 的桶,估计值是这个桶 (50, 100] 内线性插值的位置(p50 = 75、p99 = 99.5),不是 100 本身;100.001 ms 的请求落进下一个桶 (100, 250]

超出最大有限桶的数据只能返回最大有限桶的上界;恰好落在边界上的值归入“小于等于”的桶。

rate 窗口

实测 · 本机执行输出
  查询时刻(服务第 30 秒起变慢) | rate 窗口 W | p50 估计(ms) | 窗口内真实 p50(ms) | p99 估计(ms) | 窗口内真实 p99(ms) | 窗口内慢阶段请求的比例
  ------------------+-----------+------------+---------------+------------+---------------+------------
  第 50 秒            | 10 s      | 175.0      | 200.0         | 248.5      | 200.0         | 100%       
  第 50 秒            | 30 s      | 141.2      | 200.0         | 247.8      | 200.0         | 67%        
  第 50 秒            | 60 s      | 9.4        | 10.0          | 246.5      | 200.0         | 40%        
  第 60 秒            | 10 s      | 175.0      | 200.0         | 248.5      | 200.0         | 100%       
  第 60 秒            | 30 s      | 175.0      | 200.0         | 248.5      | 200.0         | 100%       
  第 60 秒            | 60 s      | 110.0      | 105.0         | 247.2      | 200.0         | 50%        
  窗口里同时有快慢两个阶段时,p50 由占多数的阶段决定:第 50 秒时慢阶段只占 W=60 s 窗口的一部分(见表中最后一列),p50 还是快阶段的值,而 W=10 s 的窗口全是慢阶段;p99 只要慢阶段占比不低于 1% 就落在慢阶段的桶里,所以几个窗口给出几乎一样的值;p99 的估计比真实值(200)高,是因为落进 (100, 250] 这个桶之后在桶内做线性插值,与窗口无关

窗口里同时有两个阶段时,p50 由占多数的阶段决定;p99 只要慢阶段占比不低于 1% 就落在慢阶段。

序列数

实测 · 本机执行输出
  有限桶个数 | 标签组合数 | _bucket 序列数 | 这个 histogram 的总序列数(含 _sum、_count) | 平均每个标签组合的序列数
  ------+-------+-------------+-----------------------------------+-------------
  5     | 150   | 900         | 1350                              | 9           
  14    | 150   | 2250        | 2700                              | 18          
  20    | 150   | 3150        | 3600                              | 24          
  每个标签组合的序列数 = 有限桶个数 + 1(+Inf 桶)+ 2(_sum 和 _count)+ 1(prometheus_client 额外暴露的 _created),数字与上表一致;桶越多、标签组合越多,序列数相乘增长
ERRORS {}

每个标签组合的序列数随桶数线性增长,标签组合数乘进去。

分步演示

直方图与分位数
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Python 3.14.8 / Go / nginx / Envoy / etcd / Consul / Nacos / OpenTelemetry / Prometheus(按各篇注明用到哪些及版本);所有服务都是本机自己起的进程,端口 9150 以上,只监听 127.0.0.1,不连任何外部服务,不碰本机已有的实例;故障用 kill -9 进程、让连接断开或在代码里注入延迟与错误来制造;计时用 time.perf_counter,数字每次运行会有小幅抖动。Prometheus 3.15.0(经典直方图,没有开启 native histogram),prometheus_client 0.26.0;样本是固定种子生成的 10 万个,桶的“相对误差”按真实分位数计算;rate 窗口那节每个时刻、窗口各查一次;每节只跑了一次,histogram_quantile 的结果每次运行基本一致(桶内线性插值是确定的)
    • 没有验证:native histogram(Prometheus 3.x 的新直方图,桶自动按指数划分,序列数固定);Summary 类型的分位数误差;OpenTelemetry 的 explicit bucket / exponential histogram;多个实例的 histogram 在桶划分不一致时怎么聚合;Grafana / VictoriaMetrics 对 histogram_quantile 的实现差异
    京ICP备2026067502号-1京公网安备11010502063487号