---
title: "多实例接口的 p95 怎么算？先合并 Prometheus 直方图的桶"
date: 2026-11-13
slug: prometheus-aggregate-histogram-p95
canonical: https://kernelranch.com/blog/2026/11/prometheus-aggregate-histogram-p95
markdown_url: https://kernelranch.com/blog/2026/11/prometheus-aggregate-histogram-p95.md
tags: ["prometheus", "可观测性", "metrics"]
---服务级 p95 应从服务级请求分布计算。使用 Prometheus 经典直方图时，对每条累积桶计数先取 `rate()`，按服务与 `le` 聚合，再交给 `histogram_quantile()`。实例 p95 的平均值会把低流量实例的尾延迟当成和主实例同等重要，即使加权平均也无法从各实例分位数恢复总体分位数。

## 1. 用 1000 个合成请求验证差异

保存为 `histogram_p95_lab.py` 并运行。假设每组桶计数来自同一个固定窗口。样本从零起算，含三个有限桶和 `+Inf` 桶，数值单位为秒。函数只实现目标分位数落在有限桶内的线性插值，以复算文档中的计算规则。

```python
from math import inf, isclose

bounds = (0.1, 0.3, 1.0, inf)
fast = (0, 980, 990, 990)
slow = (0, 0, 10, 10)


def estimate_p95(cumulative):
    rank = 0.95 * cumulative[-1]
    lower_bound, previous = 0.0, 0
    for upper_bound, count in zip(bounds, cumulative):
        if count >= rank:
            if upper_bound == inf or count == previous:
                raise ValueError("sample requires a populated finite bucket")
            return lower_bound + (upper_bound - lower_bound) * (
                (rank - previous) / (count - previous)
            )
        lower_bound, previous = upper_bound, count
    raise ValueError("invalid cumulative counts")


merged = tuple(a + b for a, b in zip(fast, slow))
fast_p95, slow_p95 = estimate_p95(fast), estimate_p95(slow)
combined_p95 = estimate_p95(merged)
print("merged buckets:", merged)
print(f"instance p95s: {fast_p95:.4f}s, {slow_p95:.4f}s")
print(f"mean of instance p95s: {(fast_p95 + slow_p95) / 2:.4f}s")
print(f"merged p95 estimate: {combined_p95:.4f}s")
print(f"at or below 300ms: {merged[1] / merged[-1]:.1%}")
assert isclose(combined_p95, 0.1 + 0.2 * 950 / 980)
assert merged[1] / merged[-1] == 0.98
```

```text
merged buckets: (0, 980, 1000, 1000)
instance p95s: 0.2919s, 0.9650s
mean of instance p95s: 0.6285s
merged p95 estimate: 0.2939s
at or below 300ms: 98.0%
```

## 2. 从计算对应到 PromQL

假设指标单位为秒，所有实例的桶边界一致，且 `service` 标签标识服务。经典直方图查询如下。

```promql
histogram_quantile(
  0.95,
  sum by (service, le) (rate(http_request_duration_seconds_bucket[5m]))
)
```

`rate()` 放在 `sum` 内，先让 Prometheus 按原始序列处理计数器重置。查询还须保留 `le`，供分位数函数识别桶的上界。若希望按路由观察，在埋点提供稳定的路由模板标签，并把它加入聚合维度。

判断“有多少请求在 300 ms 内完成”时，若所有实例都设有该边界，可以直接用该桶的速率除以总请求速率。例如 `le` 的实际标签值为 `0.3` 时可用下式。无请求时的比率缺乏分母，告警逻辑应显式处理零流量窗口。

```promql
sum by (service) (
  rate(http_request_duration_seconds_bucket{le="0.3"}[5m])
)
/
sum by (service) (rate(http_request_duration_seconds_count[5m]))
```

合并结果的 300 ms 达标率来自累积桶计数。p95 落在 100–300 ms 桶时，需要假设桶内分布并插值，估算精度取决于桶边界。变更桶边界期间，需确认参与同一聚合的实例是否仍使用一致的分布定义。

## 小结

服务级分位数先按实例合并桶。固定阈值的达标率在对应桶存在时，可直接从桶和总数计算。

## 数据来源与检索时间

| 来源 | URL | 检索日期 | 对应内容 |
|---|---|---|---|
| Prometheus histogram_quantile | https://prometheus.io/docs/prometheus/latest/querying/functions/#histogram_quantile | 2026-11-13 | 经典桶聚合、le 和线性插值 |
| Prometheus rate | https://prometheus.io/docs/prometheus/latest/querying/functions/#rate | 2026-11-13 | 处理计数器重置和先 rate 后聚合 |
| Prometheus 直方图与摘要 | https://prometheus.io/docs/practices/histograms/ | 2026-11-13 | 分位数聚合与固定阈值达标率 |

## 核验说明

- Windows、Python 3.11.9 对合成桶计数复算并验证关键结果。两条 PromQL 按 Prometheus 官方文档核对，未在 Prometheus 服务实例执行。
