一个实例 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 赋予精确外观更有价值。
