Prometheus 多实例延迟分位:先按 le 聚合再用 histogram_quantile 求值
问题场景
服务部署多个实例后,常见做法是先在每个实例上算出 P95,再对所有实例的 P95 取平均,作为服务整体延迟。这个数字看起来平滑,却经常与真实整体分位对不上:上游 SLO 看板显示达标,用户侧仍能感知长尾。原因不是查询写错,而是分位数本身不可加。
分位数是从分布(直方图)里算出来的统计量,把它算完再平均,等价于对结果做二次压缩,丢失了分布形状。正确顺序是:先把同维度、同桶边界的累计计数器按 le 汇总成一份“全局分布”,再用 histogram_quantile 求一次值。
正确的 PromQL 形态
核心只有三步:对 _bucket 取 rate,按 le 求和,交给 histogram_quantile。
sum by (le) (
rate(http_request_duration_seconds_bucket{job='demo-app'}[5m])
)
histogram_quantile(
0.95,
sum by (job, le) (
rate(http_request_duration_seconds_bucket{job='demo-app'}[5m])
)
)
需要下钻到实例时,把 instance 保留在 by 里;需要服务级总览时,le 后面只留 job。两种粒度都从同一份桶数据算起,因此彼此自洽。
反例(不要这样写):
avg by (job) (
histogram_quantile(0.95,
sum by (job, instance, le) (rate(http_request_duration_seconds_bucket[5m]))
)
)
当各实例流量占比和分布形状都不同时,这个平均值的偏差方向与幅度都不固定,不能当成整体 P95 使用。
最小可运行示例
1. 暴露直方图的应用 app.py
import os
import random
import time
from prometheus_client import Histogram, start_http_server
BUCKETS = (0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0)
latency = Histogram(
'http_request_duration_seconds',
'HTTP 请求处理耗时(秒)',
buckets=BUCKETS,
)
def main():
mu = float(os.environ.get('LATENCY_MU', '-3.5'))
start_http_server(8000)
while True:
latency.observe(random.lognormvariate(mu, 0.8))
time.sleep(0.05)
if __name__ == '__main__':
main()
2. Dockerfile
FROM python:3.12-slim
RUN pip install --no-cache-dir prometheus-client
COPY app.py /app.py
CMD python /app.py
3. docker-compose.yml
services:
app-a:
build: .
environment:
LATENCY_MU: '-3.5'
app-b:
build: .
environment:
LATENCY_MU: '-2.8'
prometheus:
image: prom/prometheus:vX.Y.Z # 占位,替换为你环境可用的稳定版本
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
- ./rules.yml:/etc/prometheus/rules.yml:ro
ports:
- '9090:9090'
两个容器刻意给了不同的 LATENCY_MU,模拟分布不一致的多实例。
4. prometheus.yml
global:
scrape_interval: 5s
evaluation_interval: 5s
rule_files:
- /etc/prometheus/rules.yml
scrape_configs:
- job_name: demo-app
static_configs:
- targets: ['app-a:8000', 'app-b:8000']
5. 记录规则 rules.yml
groups:
- name: latency-quantile
interval: 15s
rules:
- record: job:http_request_duration_seconds_bucket:rate5m
expr: sum by (job, le) (rate(http_request_duration_seconds_bucket[5m]))
- record: job:http_request_duration_seconds:p95
expr: histogram_quantile(0.95, job:http_request_duration_seconds_bucket:rate5m)
- record: instance:http_request_duration_seconds:p95
expr: histogram_quantile(0.95, sum by (job, instance, le) (rate(http_request_duration_seconds_bucket[5m])))
- record: job:http_request_duration_seconds:p95_avg_of_instance
expr: avg by (job) (instance:http_request_duration_seconds:p95)
最后一条记录规则只用于对比验证,不应用于线上看板。
运行与验证
promtool check rules rules.yml
docker compose up -d
docker compose ps
curl -s localhost:9090/-/ready
等两三个抓取周期后,在 Prometheus 查询框依次执行:
- 查看聚合后的桶:
sum by (le) (rate(http_request_duration_seconds_bucket{job='demo-app'}[5m])),确认le从小到大递增、最后一个为+Inf。 - 对比
job:http_request_duration_seconds:p95与job:http_request_duration_seconds:p95_avg_of_instance,观察两条曲线的分离。
分桶边界与一致性检查
聚合前必须确认各实例桶边界一致,否则 sum by (le) 会把缺失的桶当成 0,导致分布被削平。
# 每个实例的桶数量,应完全相同
count by (job, instance) (http_request_duration_seconds_bucket)
# 全局出现过的 le 集合
count by (le) (http_request_duration_seconds_bucket)
# +Inf 桶速率应与 _count 速率一致
sum by (job) (rate(http_request_duration_seconds_bucket{le='+Inf'}[5m]))
- sum by (job) (rate(http_request_duration_seconds_count[5m]))
再检查分位是否已被最高有限桶“截断”:
job:http_request_duration_seconds:p95 >= 5
结果为真说明 P95 已落在 +Inf 桶,histogram_quantile 只能返回最高有限桶的上界,这个值可用于告警,但不适合再做容量判断。此时应扩展桶边界。
Grafana 对比验证
- 添加 Prometheus 数据源,新建面板,Query A 填入
job:http_request_duration_seconds:p95,Query B 填入job:http_request_duration_seconds:p95_avg_of_instance,单位设为秒,同一张图叠加。 - 再建面板,Query C 填入按
instance分组的 P95,Legend 使用{{instance}},观察单实例曲线是否都低于全局曲线。 - 用 Explore 的 Instant 查询在同一时间点取 A、B 两条记录规则的值,切换 Table 视图逐点比对;差距越稳定,说明实例间分布差异越系统化。
- 把
app-b的LATENCY_MU调大或降低其请求速率后重建容器,重复步骤 1 至 3,观察平均分位与聚合分位的偏离如何随流量占比变化。 - 在面板上叠加
up{job='demo-app'},确认对比期间没有实例掉线,避免把“实例缺失”误读成“延迟下降”。
边界与限制
histogram_quantile在桶内做线性插值,结果只是近似;桶越粗,误差越大,SLO 阈值附近必须有足够的桶边界。- 只能传入
_bucket的rate/increase结果,不能把已经算好的分位数再喂给它,也不能跨不同指标名聚合。 - 分位数不可再聚合:平均分位、分位之分位都不等价于总体分位。
- 低流量时短窗口内的
rate事件数太少,分位抖动明显,可拉长窗口或改用更长区间的increase。 - 实例整体下线后不再产生样本,聚合结果只反映在线实例,需要配合
up一起判断。 - 若使用原生直方图,其聚合与求值语义与经典直方图不同,本文示例不覆盖,请按对应文档单独验证。
- 本文示例的镜像 tag、端口、
LATENCY_MU均为占位值,落地时按实际环境替换。
- 点赞
- 收藏
- 关注作者
评论(0)