你等的是最慢的那一台
这一章解释了一件让很多团队困惑多年的事:为什么每个服务的监控都是绿的,用户还是觉得卡。答案不在任何一台机器上,它在「一百台机器的最大值」这个统计量里。
你有 100 台后端。每一台都很健康:99% 的请求在 10 毫秒内返回(p99 = 10ms)。
一个用户请求要同时打到全部 100 台,等所有结果都回来才能返回(典型的搜索、推荐、分布式查询)。
有多大比例的用户请求,会碰到至少一台超过 10 毫秒?
算一下
# 每台不超时的概率 P(一台没事) = 0.99 # 一百台全部没事的概率(假设独立) P(全都没事) = 0.99^100 = 0.3660 # 所以至少一台慢的概率 P(至少一台慢) = 1 − 0.3660 = 63.40%
63.40%。不是 1%。
这就是 Jeff Dean 和 Luiz Barroso 那篇《The Tail at Scale》(2013)的核心。它的结论用一句话说就是:
在扇出系统里,尾巴不是边缘情况,尾巴就是常态。
| 扇出到几台 | 碰到至少一台慢的概率 |
|---|---|
| 1 | 1.00% |
| 10 | 9.56% |
| 100 | 63.40% |
| 1000 | 100.00% |
顺便,「最慢那台平均慢多少」也有闭式解。如果每台的延迟是指数分布,那么 n 台里最慢那台的期望是单台期望的 H(n) 倍(H(n) 是调和级数 1 + 1/2 + … + 1/n):
H(100) = 5.187 # 模拟验证:单台平均 9.93ms,一百台里最慢那台平均 51.98ms # 倍数 5.23 —— 和 H(100) = 5.187 对上了
反过来问:要让扇出 100 台之后仍有 99% 的用户请求达标,每台需要多好?
需要 P(全都没事) ≥ 0.99 每台不超时的概率 ≥ 0.99^(1/100) = 0.9998995 所以每台超时的概率 ≤ 0.0100% # 也就是说:你要的是每台的 **p99.99**,不是 p99。
这条推论对工程实践的影响非常大:
- 你的 SLA 层级要按扇出深度倒推。用户侧要 p99,扇出 100 就得要求单机 p99.99,再往下一层扇出 10 就得要求 p99.999。
- 而 p99.99 是极难保证的。那个量级上,GC 停顿、页错误、内核抢占、磁盘抖动全都是常客。你不可能靠「优化代码」到达那里。
所以《The Tail at Scale》那篇文章的真正主张是:不要试图消灭尾巴,要设计成能容忍尾巴。
容忍尾巴的办法
既然消灭不了,那就绕过去。几种在工业界被反复验证的做法:
对冲请求(hedged requests)
同一个请求发给两台机器,谁先回来用谁,另一个取消。
# 假设两台的慢是独立的 单台尾概率 1% → 对冲两份之后 1% × 1% = 0.0100% 扇出 100 台: 不对冲 63.40% 的请求会碰到慢的 对冲两份 1.00%
63.4% → 1.0%。代价是流量翻倍。
而实践中有一个便宜得多的变体:延迟对冲——先发一份,等到 p95 那个时刻还没回来,再发第二份。这样只有 5% 的请求会产生额外流量,却能覆盖掉绝大部分尾巴。用 5% 的额外成本买走大部分的 63.4%。
减小扇出
上面那张表最直白的读法:扇出从 100 降到 10,问题就小了一个数量级(63.40% → 9.56%)。
所以「把 100 个分片合并成 10 个大分片」是一个纯粹为了尾巴做的架构决策,尽管它在吞吐上什么也没改善。
不等所有人
如果业务允许,只等 95% 的后端返回就出结果(缺的那部分降级)。搜索引擎大量使用这招——少一个分片的结果,排序会略差,但用户感知不到;而等那一个分片,用户一定感知得到。
这在数学上是把「求最大值」换成了「求 95 分位」,而后者对扇出的敏感度低得多。
把慢的原因错开
上面所有计算都假设「各台机器的慢是独立的」。如果不独立——比如所有机器同时做 GC、同时跑定时任务、同时被同一个大客户打——那么对冲和扇出计算全都失效。
所以「给定时任务加随机抖动」不只是第 6 章说的减少扎堆,它还在维持后端之间故障的独立性。
把前面几章串起来看,这一章其实是一个放大器:
# 单台的 p99 从哪来?第 4 章: p99 ≈ ln(ρ ÷ 0.01) ÷ (μ − λ) ← 由利用率和余量决定 # 扇出之后的用户体验?这一章: P(慢) = 1 − (1 − p)^n ← 由扇出深度决定 # 合起来: # 用户体验到的"慢"的概率,同时被 # ① 单机的余量(第 9、10 章) # ② 单机的不齐(第 5、8 章) # ③ 扇出深度(这一章) # 三者放大。而 ③ 是**指数**的。
这解释了一个反直觉的现象:在扇出系统里,单机利用率必须比你以为的低得多。第 14 章说大池子能跑到 98%,但如果这个池子是被扇出访问的,那 98% 的利用率会让每台的 p99 变得很难看,再被 100 次方一放大,用户侧就崩了。
第 10 章提过「为什么大厂的机器看起来那么闲」——现在你有完整的答案了:那不是浪费,那是扇出深度反推回来的余量要求。
「我们的 p99 是 50 毫秒」这句话,必须问一句:谁的 p99?
- 单个后端的 p99:每个后端自己统计的。这是运维最容易拿到的数。
- 用户请求的 p99:从用户点击到看见结果。这是唯一真正重要的数。
这两个数在扇出系统里可以差一个数量级,而且方向永远是用户侧更差。
更麻烦的是,它们经常被混着说。「我们的服务 p99 是 20 毫秒」——如果这是各个微服务各自的 p99 平均出来的,那它对用户体验几乎没有预测力。唯一可信的做法是在入口处打点,端到端地量。
为什么微服务架构会让延迟变差。一个用户请求经过 20 个微服务,每个 p99 = 10ms。就算是串行调用(不是扇出),端到端的 p99 也远远超过 10ms——因为你在 20 次抽奖里更容易抽中至少一次慢的。微服务拆分在架构上的好处是真的,但它在延迟尾巴上的代价同样是真的,而后者很少被算进决策里。
为什么「重试」有时候是对冲,有时候是雪崩。失败后重试和对冲请求在机制上很像,区别在时机:对冲是在系统健康时主动多发一份(成本可控),重试是在系统已经不行时又加一份(火上浇油)。第 21 章会算重试风暴的账。
为什么 Google 的 Spanner 要用 TrueTime。分布式事务需要等待时钟不确定性窗口过去。那个窗口的大小直接决定了每次提交的延迟——而提交是扇出的。花大价钱在数据中心装原子钟和 GPS,买的就是把那个窗口从几百毫秒压到几毫秒。这是「花硬件的钱去压 c²」的一个极端例子。
你的手机 App 启动慢。启动时并发发了三十个请求初始化各个模块,界面要等全部返回。哪怕每个接口都很快,用户感受到的是那三十个里最慢的一个。解法通常不是优化接口,是让界面不要等所有人。
这个错误的顽固之处在于,它对平均值是成立的:如果每台平均 5 毫秒,那扇出的平均值确实也和 5 毫秒有关(虽然是最大值的期望,也有闭式解)。平均值可以组合,分位数不能。
实践后果是资源投错方向。团队会花几个月把单机 p99 从 12ms 优化到 10ms(改善 17%),而用户侧的「碰到慢请求的概率」从 70% 变成 63.4%——几乎感觉不到。同样的精力如果用来实现延迟对冲,用户侧会从 63.4% 掉到 1%。
在扇出系统里,「容忍尾巴」的架构手段几乎总是比「消灭尾巴」的优化手段划算一个数量级。
C63.40%。1 − 0.99^100 = 0.6340。
几乎三分之二的用户请求会碰到至少一台慢的。而每一台机器的监控都是绿的,每一个后端团队都会说「我们达标了」。
这个数字之所以让人吃惊,是因为我们的直觉对「小概率重复很多次」极其不敏感。1% 听起来微不足道,重复 100 次之后它变成了主导。而现代架构的默认形态就是「小概率重复很多次」。
A 「每台都是 p99 嘛」——把 100 次独立抽奖当成了 1 次。这个错误在数学上很基础,但它在工程实践里极其普遍,因为组织结构(每个团队负责自己的服务)会强化它。 B 「大约 10%」——这是扇出到 10 台的答案(9.56%)。方向对了:你意识到了会放大,只是低估了放大倍数。 D 「接近 100%」——扇出到 1000 台时确实接近 100%(99.996%)。所以这个答案不是错,只是提前了一个数量级。而在真实的大型搜索系统里,扇出到几千个分片是常态——那里的答案确实是「几乎每个请求都会碰到慢的」,所以那些系统全都必须做降级和对冲。这一章的一句话
扇出把每台机器的小概率尾巴,指数级地放大成用户侧的常态;而容忍尾巴永远比消灭尾巴便宜。
卷 IV 结束。前面十六章讲的全是「怎么让队变短」。下一卷换一个问题:队已经在那儿了,谁先走?第 17 章会给出一个乍看不可能的结果——先来后到和后来先服务,平均等待完全相同(3.774 对 3.788)。它们的区别不在效率,而在把方差分给谁:LIFO 的中位数只有 FIFO 的四分之一,p99 却是它的 2.7 倍。