P95 延迟为什么不能平均:分位数、直方图合并与超时样本验收

多个实例的 P95 延迟即使按请求量加权,也不能直接得到整体 P95。本文从可手算的反例出发,解释原始样本、分位数摘要与直方图的差异,给出经典 Prometheus 直方图的聚合方法,并处理桶精度、超时遗漏和重试分母问题。最后用一组可重复的验收检查,帮助团队判断延迟看板是否真正反映用户体验。

一个实例 P95 为 1 秒,另一个为 10 秒,整个服务的 P95 是多少?仅凭这两个数没有通用答案。把它们平均成 5.5 秒,看似完成了汇总,却可能把实际长尾掩盖掉。企业 AI 服务跨实例、跨模型和跨重试统计时,首先要保留可合并的信息,再谈看板上的小数位数。

先固定计时对象,再选择分位数算法

企业 AI 可观测性与失败定位需要把业务结果和运行信号分开。延迟也有不同对象:用户一次完整任务、一次模型调用、首次输出或完整输出,各自的开始和结束点不同。本文只讨论已经明确总体与计时边界的非负耗时;不能把不同对象混在一起求一个有业务含义的 P95。

本篇主要算例采用离散最近秩定义:将 N 个有效耗时从小到大排序,P95 取第 ceil(0.95×N) 个值,位置从 1 开始。它描述这批已观察数据,不代表下一次请求有被证明的 95% 概率落在该值以内。窗口、样本量和流量构成变化,都可能改变解释。

PostgreSQL 18 文档区分 percentile_disc 与会在相邻值之间插值的 percentile_cont,并说明聚合输入中的空值会被忽略。[1] 对构造数据 1、2、3、4 秒,离散 P95 为 4 秒,连续方法为 3.85 秒。算法不同不是计算故障,但跨系统比较时必须声明一致口径。

落地前写下四项:计时边界、样本纳入条件、窗口归属和分位数算法。例如按任务受理时刻归属窗口,必须等待足够的结果收齐并标注未结束任务;按结束时刻归属则描述该窗口完成的任务。两种方案都可能有用,但不能一边以受理量作分母,一边悄悄只取已完成任务的耗时。

一个请求量加权也无法修复的反例

以下全部为构造数据。实例 A 完成 100 个请求,每个 1 秒;实例 B 完成 10 个请求,每个 10 秒。两者 P95 分别为 1 秒与 10 秒。直接平均得到 5.5 秒,按请求量加权得到 (100×1+10×10)/110,约为 1.818 秒。

合并后共有 110 个样本。离散 P95 的位置是 ceil(104.5)=105;第 101 到第 110 个值都是 10 秒,因此整体 P95 是 10 秒。加权平均得到的 1.818 秒在这个特殊例子里恰好也是整体平均耗时,但仍不是整体 P95。各组内部所有值相同这一条件,不能推广到一般数据。

原因是分位数保留一个排序位置附近的值,不保留全部分布。局部 P95 和局部样本量通常不足以还原整体排序;将实例换成地区、模型版本或五分钟窗口,问题依然存在。一天的 P95 不是每五分钟 P95 的平均值。

可选路径是集中计算原始样本,或保留具备明确合并规则的分布摘要。前者便于复核和重新分组,却增加存储与查询开销;后者节省资源,但要接受精度和兼容性约束。已经只留下局部 P95 的历史数据,不应通过补一个加权公式伪装成准确的全局结果。

合并桶计数,再从合并后的分布估计

OpenTelemetry 对直方图的介绍把它解释为记录分布的多个计数,而不只是一个统计值。[2] 在本文的固定桶示例中,选取上界 0.5、1、2、5、10 秒以及正无穷,使用包含上界的累积计数。上述 110 个样本对应计数依次为 0、100、100、100、110、110;累积桶不能全部相加作为总量,最后一个桶才覆盖全部样本。

对于已按经典桶序列采集的 Prometheus 指标,可使用下面的表达式。前提是所选数据具有相同的单位、计时对象、纳入规则与桶边界,并且选择器已经限定到同一服务及目标总体。示例指标名是自定义名称,不是系统必然存在的内置指标。

histogram_quantile(0.95, sum by (le) (rate(ai_request_duration_seconds_bucket[5m])))

Prometheus 文档说明经典直方图可先合并桶,再估计分位数;预先算好的 summary 分位数不能这样汇总。[3] 函数文档要求组合计数速率与聚合时先 rate 再 sum,以处理各序列的计数重置。[4] 这里保留 le 是为保留桶上界;如果要分别看不同业务类型,还要在选择器或分组中明确保留对应维度。

这个查询输出五分钟窗口内按桶速率估计的分布,不是原始日志的逐条精确排序。缺失抓取、窗口边缘外推和低流量都会影响与日志核对的结果。先用完整、对齐的固定计数验证数学,再检查生产采集链路;不要把两种误差混为一谈。

桶兼容并不等于结果精确

继续手算同一组数据:P95 落在大于 5 秒、不超过 10 秒的桶内。按经典桶的线性插值,估计值为 5+(104.5−100)/(110−100)×5=7.25 秒;原始样本的离散 P95 却是 10 秒。该结果是本文构造数据的公式推导,不是对生产 Prometheus 环境的实测。

误差来自把桶内未知分布当作线性分布进行估计,而实际十个值全部位于上界。函数文档明确说明经典桶的插值假设。[4] 若业务关注“是否至少 95% 的请求在 5 秒内”,并且所有实例都有准确的 5 秒边界,直接计算该累积桶与总量之比更直接:100/110,约 90.91%,没有达到目标。

这个比例只对被纳入的样本成立,也仍受采集完整性影响。若桶边界不在目标阈值上,不能随意用邻近桶代替精确门槛。若有的实例缺少 5 秒桶,分子只覆盖部分实例而分母覆盖全部,会形成结构性错误,不能靠多保留两位小数修复。

增加桶数可改善某些区间的分辨率,也增加经典时间序列的存储与查询成本。本文展示经典桶是为了说明已有系统的排障方法,不表示它是所有新项目的首选;采用原生直方图时,应按所用客户端、服务端和存储链路验证兼容性,不能直接套用本例的桶序列查询。桶配置升级期间,应按版本隔离或完成兼容性核验后再合并。

超时和重试会悄悄改变总体

如果只在成功完成时上报耗时,最慢且最终超时的请求可能全部消失。构造 100 个任务,其中 90 个在 1 秒成功,10 个在 20 秒超时;只看成功样本,P95 为 1 秒,但全部受理任务只有 90% 成功。不能据此宣传“95% 的用户在 1 秒内得到结果”。

超时记录能告诉我们等待何时结束,却通常不能告诉我们若继续等待,后台将在何时成功完成。把这类未知完成时长填成 0,或者把超时门槛当作实际成功完成时长,都会改变问题。若统计的是到任一终态的用户等待时间,可以记录超时发生时的等待时长,但必须同时展示结果状态,不能把快速失败当作快速成功。

建议分开报告成功交付耗时分布、超时与取消数量、全部受理任务的期限内成功比例,以及仍未结束的任务数。对按受理时刻划分的批次,期限内成功比例要在截止时间已到后再判断,或明确标注暂定。不要强迫未知样本进入一个看似完整的成功耗时分位数。

重试也有两套分母。一个用户任务触发三次模型尝试时,模型层可以记录三次调用耗时,用户层应记录一次完整等待及最终结果。把二者混在同一指标会让尝试次数多的任务获得额外权重。用任务与尝试标识在受控日志中关联;不要把每个请求编号都变成指标标签,造成无法控制的序列数量。

用分组不变性和故障样本验收看板

第一,保存上述 100 个 1 秒、10 个 10 秒的固定样本,核对总数 110、原始离散 P95 为 10、桶计数以及手算估计 7.25。再把相同样本重新分配到三个实例;只要总体和桶不变,合并结果就应保持不变。若改变实例数量就改变整体结果,优先检查是否错误平均了局部分位数。

第二,把同一批样本拆成不重叠的小窗口,合并各窗口的增量桶计数,再计算大窗口结果。不要对累积计数快照直接求和,也不要重复叠加相互重叠窗口。这个检查针对固定数据;生产 rate 查询还需单独检查抓取间隔与重置处理。

第三,加入明确的超时、取消、空耗时和重复上报记录,核对每种记录进入哪个总体。检查空值是否被静默排除、重复数据是否有明确去重责任、没有样本时是否错误显示为零延迟。第四,模拟实例重启和桶配置不一致,确认有可见异常或隔离策略,而不是继续输出没有解释的单一数值。

第五,按版本和任务类型对照原始样本与看板。总体 P95 变好可能只是简单任务占比增加;关键任务仍可能变慢。保留合理业务分组,但控制标签数量。验收材料应同时写出数据范围、时间窗口、样本数、算法、桶边界、缺失比例与已知限制。

这套方法的交付物不是一条更平滑的曲线,而是一条可复核的计算路径:从受理与结果记录确定总体,从耗时记录构成分布,用明确算法得到数值,再据业务门槛作决策。没有原始样本或可合并分布时,诚实保留未知,比给错误的全局 P95 赋予精确外观更有价值。

参考资料

  1. [1] PostgreSQL 18 — Aggregate Functions:ordered-set aggregates(查阅 2026-10-28)
  2. [2] OpenTelemetry — Why Histograms?(Daniel Dyla,2023-05-08;基础概念资料)
  3. [3] Prometheus — Histograms and summaries(查阅 2026-10-28)
  4. [4] Prometheus — Query functions:histogram_quantile、rate(查阅 2026-10-28)
返回洞察
鲁ICP备2024109755号-2
可拖动移动。右键、长按或按 Shift+F10 可选择停靠位置。