Prometheus 多实例延迟分位:先按 le 聚合再用 histogram_quantile 求值

举报
yd_217513249 发表于 2026/11/05 11:49:53 2026/11/05
【摘要】 多实例延迟统计中直接平均各实例分位数会失真。本文用可运行示例演示先按 le 聚合 rate 桶再调用 histogram_quantile,并给出分桶边界检查与 Grafana 对比验证步骤。

问题场景

服务部署多个实例后,常见做法是先在每个实例上算出 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 查询框依次执行:

  1. 查看聚合后的桶:sum by (le) (rate(http_request_duration_seconds_bucket{job='demo-app'}[5m])),确认 le 从小到大递增、最后一个为 +Inf。
  2. 对比 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 对比验证

  1. 添加 Prometheus 数据源,新建面板,Query A 填入 job:http_request_duration_seconds:p95,Query B 填入 job:http_request_duration_seconds:p95_avg_of_instance,单位设为秒,同一张图叠加。
  2. 再建面板,Query C 填入按 instance 分组的 P95,Legend 使用 {{instance}},观察单实例曲线是否都低于全局曲线。
  3. 用 Explore 的 Instant 查询在同一时间点取 A、B 两条记录规则的值,切换 Table 视图逐点比对;差距越稳定,说明实例间分布差异越系统化。
  4. 把 app-b 的 LATENCY_MU 调大或降低其请求速率后重建容器,重复步骤 1 至 3,观察平均分位与聚合分位的偏离如何随流量占比变化。
  5. 在面板上叠加 up{job='demo-app'},确认对比期间没有实例掉线,避免把“实例缺失”误读成“延迟下降”。

边界与限制

  • histogram_quantile 在桶内做线性插值,结果只是近似;桶越粗,误差越大,SLO 阈值附近必须有足够的桶边界。
  • 只能传入 _bucket 的 rate/increase 结果,不能把已经算好的分位数再喂给它,也不能跨不同指标名聚合。
  • 分位数不可再聚合:平均分位、分位之分位都不等价于总体分位。
  • 低流量时短窗口内的 rate 事件数太少,分位抖动明显,可拉长窗口或改用更长区间的 increase。
  • 实例整体下线后不再产生样本,聚合结果只反映在线实例,需要配合 up 一起判断。
  • 若使用原生直方图,其聚合与求值语义与经典直方图不同,本文示例不覆盖,请按对应文档单独验证。
  • 本文示例的镜像 tag、端口、LATENCY_MU 均为占位值,落地时按实际环境替换。
【版权声明】本文为华为云社区用户原创内容,未经允许不得转载,如需转载请自行联系原作者进行授权。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。