0.3 延迟是分布,不是数字
上一节:0.2 吞吐、延迟、成本的三方权衡 | 下一节:0.4 用 Little’s Law 估算容量 配套代码:00-introduction/03-latency-distribution 动手:建议先跑一遍配套代码,本节的两个现象都能亲眼看到。
一句话结论
「这个接口的平均延迟是 11 ms」这句话,可能同时意味着「99% 的人只等了 1 ms」和「1% 的人等了整整 1 秒」。 后端必须看延迟的分布,尤其是尾部。
一、一个 100 个请求的数字游戏
假设你的接口收到了 100 个请求:
- 99 个请求很快,各花了 1 ms;
- 1 个请求很慢,花了 1000 ms(比如遇到了 GC 停顿或数据库锁等待)。
现在计算平均值:
总耗时 = 99 × 1 ms + 1 × 1000 ms = 1099 ms
平均延迟 = 1099 / 100 ≈ 11 ms
报出去的数是 11 ms。听起来很棒,对吧?
但对那 1 个用户来说,他结结实实地等了 1 秒。如果这个接口一天被调用 100 万次,那就是一万个用户每天遇到「卡了一秒」。
平均值会把少数人的糟糕体验,摊薄到大多数人的好体验里,然后把它藏起来。
这就是为什么性能报告里必须出现「P99」这样的数字。
二、百分位:一个不需要数学的定义
P95 = 95 ms 的意思是:
把所有请求按耗时从短到长排成一队,从队首往后数到第 95% 个位置,那个请求的耗时就是 95 ms。
换句话说:95% 的请求比 95 ms 快,5% 的请求比它慢。
用排队买奶茶的类比:100 个人排成一队,按等待时间从短到长站好。站在第 95 位的那个人等了 95 ms——他不是最慢的,但他的位置告诉你「绝大多数人的体验上限在哪」。
各个百分位代表谁的体验
| 指标 | 含义 | 谁在经历它 | 用途 |
|---|---|---|---|
| P50(中位数) | 一半人比它快 | 「典型用户」 | 反映系统整体是否变慢 |
| P90 / P95 | 90%/95% 的人比它快 | 「偶尔运气不好的用户」 | 日常体验的健康度 |
| P99 | 99% 的人比它快 | 「每小时都会出现的那批人」 | SLO 常用标准 |
| P999 | 99.9% 的人比它快 | 「最倒霉的千分之一」 | 支付、金融等敏感场景 |
| max | 最慢的那一个 | 一个可怜的用户 | 排查极端情况,不适合做告警 |
为什么 P99 是常用的标准?因为它在「统计稳定性」和「用户体验敏感度」之间比较平衡:P999 在高并发下波动很大(样本少),而 P90 又太宽松(掩盖了 10% 的受害者)。
一个必须避免的错误
P99 不是「最慢的 1% 的平均值」。 它是一个具体的「位置上的值」。这个概念混淆会导致后续所有计算错误。
三、更隐蔽的问题:尾延迟会跨服务放大
假设你的页面需要调用 10 个后端服务(很常见的架构)。每个服务都很健康,各自只有 1% 的请求会变慢。
那么这一次页面加载,至少有一个服务变慢的概率是多少?
每个服务「不慢」的概率 = 99% = 0.99
10 个服务全部「不慢」的概率 = 0.99^10 ≈ 0.904
至少一个服务变慢的概率 = 1 - 0.904 ≈ 0.096 ≈ 9.6%
结果:每个服务 P99 都合格(99% 正常),但整页有近 10% 的概率是慢的。
用生活化的说法:10 个朋友约好一起到场,每人都有 1% 的概率迟到。虽然每个人「准时率 99%」,但这次聚会有约 10% 的概率因为有人迟到而推迟。
这个现象的三个重要推论
- 单个服务达标 ≠ 整体达标。 验收 SLO 时,要按「用户实际经历的那条链路」来测,而不是按单个接口。
- 串行链路比并行链路的放大更严重。 上例是并行(取最慢的);如果是串行(每个都必须完成),延迟是相加的:
串行 10 个服务,每个平均 10 ms、P99 50 ms 平均总延迟 = 100 ms P99 总延迟 ≈ 50 ms × 10 = 500 ms(乐观估计,实际可能更差) - 减少依赖数量本身就是一种性能优化。 从 10 个下游减到 5 个,放大效应会显著下降。
四、为什么平均值会「骗人」:三层原因
| 层级 | 原因 | 后果 |
|---|---|---|
| 数学层 | 均值对极值极其敏感,1 个 1000 ms 就能把 99 个 1 ms 拉高 10 倍 | 平均值既不代表典型用户,也不代表最差用户 |
| 统计层 | 延迟分布是重尾的(长尾),不是正态分布 | 用「均值 ± 标准差」描述它是无效的,算出来的「3 个标准差」甚至可能是负数 |
| 架构层 | 一次用户操作会触发多次调用,尾部被放大 | 单服务指标好,用户体验差 |
记住这条经验:在后端性能领域,只要有人只给你一个平均值,你就应该认为这份数据是不完整的。
五、那应该怎么记录和汇报?
5.1 不要做的事
- ❌ 只报平均值。
- ❌ 报「平均值 ± 标准差」(延迟不是正态分布,标准差没有解释力)。
- ❌ 报「最快的一次」或「最好的一轮」。
- ❌ 把多个实例的 P99 求平均(见下方)。
5.2 要做的事
- ✅ 至少同时报 P50 / P95 / P99,加上错误率(错误率是另一个维度,不能混进延迟)。
- ✅ 报样本量和时间窗口(「P99 = 91 ms,基于 5 分钟内的 150,000 个请求」)。
- ✅ 报测量点(客户端 / 网关 / 服务端)。
- ✅ 多条链路组合的场景,额外报「整体链路」的 P99,而不是各段平均。
5.3 一个容易忽略的坑:百分位不能直接平均
假设两台机器:
| 实例 | P99 |
|---|---|
| A | 10 ms |
| B | 1010 ms |
错误做法:(10 + 1010) / 2 = 510 ms。这个数字既不是 A 的,也不是 B 的,也不代表真实用户。
正确做法:采集直方图(分桶统计),把两台机器的桶合并后重新计算分位数。这也是为什么第 4 章会强调:「延迟指标必须用直方图,不能用客户端算好的百分位」。
同理,服务端也不要「在本进程算好 P99 再上报」——上报桶,让查询时再合并计算。
六、本节小结
- 延迟是一个分布,不是单个数字;均值会把少数人的糟糕体验藏起来。
- P99 的含义是「99% 的请求比它快」,不是「最慢 1% 的平均」。
- 尾部延迟会跨服务放大:10 个各 1% 慢的下游 → 整体约 10% 慢。
- 延迟分布是重尾的,不能用「均值 ± 标准差」描述。
- 汇报时必须给 P50/P95/P99 + 错误率 + 样本量 + 时间窗口 + 测量点。
- 百分位不能跨实例直接平均,必须用直方图合并后再算。
七、自测
- 某接口 P50 = 5 ms、P99 = 800 ms。这个分布说明了什么?如果只报「平均 20 ms」,会掩盖什么问题?
- 你的服务要调用 5 个下游,每个下游的「正常率」是 98%。整条链路的正常率大约是多少?(假设并行调用、互不相关)
- 一个同事汇报:「A、B 两台机器的 P99 分别是 50 ms 和 70 ms,平均 60 ms,达标。」这句话有什么问题?
- 说明分布是严重重尾的:大多数请求很快(5 ms),但每 100 个里有 1 个要等 800 ms。只报平均 20 ms 会掩盖「1% 的用户体验到 800 ms」这个事实——而这 1% 在 100 万日请求量下是一万个用户。
- 全部正常的概率 =
0.98^5 ≈ 0.904,所以整体正常率约 90.4%,即近 10% 的请求至少踩到一个慢下游。这就是尾延迟放大。 - 问题在于百分位不能直接平均。正确做法是把两台机器的延迟直方图(分桶)合并后重新计算 P99。而且「达标」还需要前提:多少 QPS、什么数据量、测于哪个点、样本量多少。