P99、样本窗口和测量偏差:删掉超时样本的 P99 是谎言
9,800 个 20ms 成功请求的 P99 是 20ms,看起来远优于"99% 在 200ms 内"的目标——但 200 个 1 秒超时让真实按时成功率只有 98%。本页解释分位数只对指定总体成立、协调遗漏如何让压测自动避开最差时段、重试正反馈如何把系统钉在亚稳态。
分位数只对指定总体、窗口和采样机制成立
低流量期间 P99 样本不足;跨租户聚合可能被大客户主导;负载剔除超时请求会把最慢样本从统计中移除。同样的平均 QPS 在连接复用、请求大小、缓存命中、热点分布和读写混合变化时会产生不同资源成本。负载模型和数据分布必须随压测报告保存。
完整算例:把最慢样本删掉得到的漂亮 P99
合成检索服务在一分钟内计划发出 10,000 个请求,其中 9,800 个 20ms 返回,200 个到 1 秒时客户端超时;目标是 99% 在 200ms 内成功。只取成功请求计算,P99 是 20ms,似乎很好;按用户结果统计,只有 98% 在目标内完成,已经失败。超时请求的真实完成时长未知,至少知道超过客户端 1 秒,不应填为零,也不能声称它们恰好都在 1 秒完成。
对于最近秩分位,排序后取第 ceil(p×n) 个值。10,000 个样本的 P99 位置是 9,900,位于超时样本,因此只能说观测到的完成分布在该处被右删失,不能用成功样本 P99 代表整个总体。业务 SLI 可把超时明确记为坏事件,诊断直方图保留超时计数或专用桶;应公开桶边界与实现算法。Google SRE 监控章节讨论长尾与用户侧症状,本页统计和数字为教学推导。
分组分位也不能平均。区域 A 100 次请求 P99 为 1 秒,区域 B 100,000 次 P99 为 30ms,平均成 515ms 没有合法的总体分位意义。若要聚合,应在相同边界和单位下合并可聚合的分布/直方图计数,再计算近似分位;少量样本、桶内插值与标签选择都会影响结果。先声明总体,再讨论一个数字是否有用。
协调遗漏的时间轴
单客户端闭环测试在 t0 发请求,服务停顿 1 秒,t1 响应后才发下一次。它只记录了一次 1 秒慢请求。若真实用户按每 10ms 一次到达,同一秒本应还有约 99 次后续请求排队。闭环发生器因为被服务减速,减少了向最差时段施加的负载,这就是协调遗漏的一种来源。
开放到达模型按计划时间产生请求,记录 scheduled、sent、completed。发生器也可能 CPU 不足,计划每秒 5,000 实际只发 2,000;如果从 sent 开始测量并不记录漏发,仍会把客户端瓶颈当服务容量。因此报告要保存计划数、实际发出数、完成数、拒绝数、超时数以及客户端资源。开放模型不是无条件更真实,固定并发用户确实适合闭环,关键是实验问题与模型一致。
不能仅把所有计划时间减去完成时间就宣布模拟了真实生产:真实入口的拒绝、网络缓冲和取消也可能改变队列。先用可解释的小模型揭示偏差,再用实际负载发生器验证。纸面修正不等于补采了真实丢失的请求历史。
重试放大的定量与亚稳态
重试增加到达率,延迟升高又触发更多超时和重试,形成正反馈。独立层都做三次重试会乘法放大下游调用;错误预算和剩余 deadline 应限制总尝试。
三层调用各最多三次尝试,在最坏的相关失败下,底层可能承受 3^3=27 次调用/逻辑请求。"重试三次"若指初次之外再重试三次,则应是四次尝试,三层最多 64 次;面试首先澄清术语。若只有一层、最多两次尝试,第一次失败后才重试,首次失败概率 p,则期望尝试数为 1+p。多层乘积的期望模型还需要独立性等条件,真实故障通常相关。
服务安全容量每秒 1,200,原始请求每秒 1,000,失败诱发每个请求平均 1.5 次尝试,实际到达每秒 1,500,超出 300。队列增长增加等待,等待触发更多超时,超时又增加重试,形成正反馈。即使根因消除,旧积压、重连与 GC 也可让系统继续停在低效状态,不能指望"网络好了自然恢复"。这种级联与恢复问题见 Google SRE 级联故障;退避和抖动的适用条件见 AWS Builders' Library。
两个恢复方案
方案 A 保留全部任务并增加消费者。对不能丢失的异步任务合理,但需要持久存储、足够下游能力和有界追赶速度;不代表让所有 HTTP 请求一直占线程等待。方案 B 限制新尝试和并发,提前拒绝过期交互请求,给关键业务留恢复空间。它会增加短期拒绝率,但可能提升长期成功完成量。两案可组合:先安全持久化必须受理的工作,交互层返回可查询任务状态,过期的无副作用请求直接拒绝。
重试应由一处有上下文的边界负责,限定总次数、总 deadline 与令牌预算。全抖动的教学形式为在 0~min(cap, base×2^attempt) 间随机等待,但随机不是保证没有碰撞;也不能超出用户剩余期限。对于未知写结果,优先查询同一业务键,退避本身不提供幂等性。UNKNOWN 的恢复协议见超时重试、熔断限流降级。
可执行模型与产物
运行 完整实验脚本:
python3 labs/capacity_models.py percentile脚本输入 9,800 个 20ms 成功与 200 个超时,预期输出成功子集 P99 为 20ms、整体按时成功率 0.98、最坏尝试数 27,model=censored-quantile-not-latency-distribution,超时完成时刻显式标为 null(右删失)。本轮该公开脚本及断言已运行;真实压测未运行。
纸面第二轮将发生器改成 10ms 固定间隔,给服务插入 1 秒停顿,列出前十个计划与实际完成时间,观察"等响应再发"减少了多少请求。第三轮将实际重试尝试率降至每逻辑请求 1.1,此时到达 1,100,低于 1,200,才有每秒 100 的净余量清旧队列。清积压多久还取决于存量 B 与任务成本,不能只报告恢复后 CPU 降低。
交付样本口径、发生器模型、成功与失败全量计数、分位算法、超时删失说明和恢复流量曲线。没有实际压测时只能叫计算验证;脚本得到的稳定状态不是生产服务容量。
分位样本量与窗口敏感性:P99 需要多少样本才可发布
删失算例说明了"成功子集 P99"的偏差,但没有回答另一个更基础的问题:一个 P99 数字在样本量多大的窗口里才有意义。最近秩分位取第 ceil(p×n) 个样本,要让 P99 落在某个可观测位置上,至少需要约 ln(0.1)/ln(0.99)≈229.1,即 230 个样本;要观测 P99.9 则需要 ln(0.1)/ln(0.999)≈2,301.4,即 2,302 个样本。低于这个量,所谓 P99 只是"最大值"或"第二大的值",不是分位数。
这个约束直接决定窗口长度。低峰期每秒 30 个请求,一分钟只有 1,800 个样本,够 P99 但不够 P99.9;要发布 P99.9 至少需要 2,302/30≈76.7 秒,约 1.3 分钟;换句话说,一分钟的 1,800 个样本仍不够,仪表盘要么再等半分钟,要么承认当前数字只是近似。正确做法是按日或按小时聚合桶计数、滚动发布,并在低峰时明示"样本不足,仅供参考"。
窗口选择还有反向成本:窗口越长,发现异常越慢。30 秒窗能快速反映发布后的尾部恶化,但低流量下样本不足;1 小时窗样本充足,却可能把一次持续 5 分钟的尾部事故稀释成"P99 只升了 3ms"。可组合为:短窗监控错误率与超时计数(不依赖分位),长窗发布分位;两者回答不同问题,不能互相替代。
执行 python3 labs/capacity_models.py quantile_samples:输入 p=0.99、0.999 与低峰 30/秒。预期输出 n_p99=230、n_p999=2302、seconds_p999=76.7。脚本计算样本量与窗口,不证明真实尾部分布;分位算法(最近秩/插值/直方图桶)在可观测主题另有权威讲解。
两个合理选择:A 统一按 1 分钟窗口发布 P99,简单但低峰无效;B 按流量自适应窗口(高流量用短窗、低流量用长窗或桶计数),复杂但样本量可控。本例选 B,并规定:任何分位发布必须同时给出样本数、窗口与算法;样本不足时发布"证据不足"而不是数字。
架构师变化条件
把 SLO 从"99% 按时"改为"P99 ≤ 200ms 且按时率 99.9%":两个指标要分开验证,P99 达标不能抵消按时率违约。把重试从每层三次改为单层两次加全局令牌桶:最坏放大从 27 降为 2 与令牌上限的较小者,恢复期到达率可控。若低峰流量只有每秒 30 个,P99 样本每分钟不足 1 个——需要按日聚合或改用桶计数并声明置信边界。每次变化重新声明总体、窗口与删失规则。
闭卷验收
本页 8 道闭卷题收进了下方自测面板:先回忆,再展开参考答案与评分点,最后给三档自评。