半空的系统,为什么也排队
上一次你看到「CPU 利用率 50%,但 p99 很难看」的时候,大概会觉得是监控出了问题,或者是某个隐藏的瓶颈。都不是。一台闲着一半时间的机器排起长队,是完全正常、完全可以预测的——只要你知道该看哪个数。
一台机器,利用率精确地钉在 50%——它有整整一半的时间是空转的。到达是随机的(这一章里始终不变)。
现在只改一件事:服务时间的分布。从「每次都花恰好 1 秒」,改成「大部分很快、偶尔很慢,但平均仍然是 1 秒」。
平均等待时间会怎么变?
先确认一件让人不舒服的事
把利用率固定在 50%。机器有一半时间在空转。按管子模型,这样的系统不应该有队。
但它有。而且队的长度完全取决于一个你可能从没量过的东西。
拖那根滑杆。利用率一动不动地钉在 50%,吞吐一动不动,机器的速度一动不动。而平均等待从 0.50 个服务时长走到 5.50 个服务时长——十一倍,而且滑杆右边还有路。
这条滑杆叫 cs²,读作「服务时间的变异系数平方」。它衡量的是不齐的程度。下一章会讲清楚它是怎么算的,这一章你只需要知道两件事:
- 它等于 0,表示每次服务的时长完全一样;
- 它等于 1,表示服务时长是「纯随机」的(指数分布)——这是软件系统里最常见的默认假设;
- 它大于 1,表示比纯随机还不齐:大部分请求很快,偶尔来一个特别慢的。
而现实中的软件系统,cs² 几乎总是大于 1。
为什么半空的机器会排队
直觉上的解释只需要一句话:机器闲的时候,你不在。
把一小时切成 60 分钟。机器一半时间闲着,也就是 30 分钟。但这 30 分钟不是整整齐齐地穿插在忙碌里的——它们东一块西一块。而请求来的时候,也不会挑机器闲的那一刻来。
于是就出现了这本书最核心的浪费:闲置产能和需求错过了。而错过了就是错过了。
# 同样是 50% 的利用率,同样的总工作量
# 整齐:忙一秒闲一秒,谁来都能立刻办
时间轴 →
◉─○─◉─○─◉─○─◉─○─◉─○─◉─○
队: 永远是空的
# 不齐:闲的时候没人来,来的时候撞一块
时间轴 →
○─○─○─○─◉◉◉◉◉◉─○─○─○─◉◉◉
队: ▮▮▮▮▮ ▮▮▮
↑ 这几个人在等,而机器刚刚闲了四格
两行的忙碌总量完全相同,利用率完全相同。第一行没有人等,第二行有人等——而且第二行浪费掉的空闲,和第二行产生的等待,是同一件事的两面。
到这里,这本书的两个主角都到齐了:
- 太满(利用率高):闲置产能本来就少,禁不起错过。
- 太不齐(变异大):闲置产能和需求对不上,于是被错过。
这两个是相乘的关系,不是相加。这意味着:如果你的系统很不齐,那么即使利用率不高,你也会排队;如果你的系统很整齐,那么即使利用率很高,你也可能不排队。
第 8 章会把这句话写成一个公式,然后这本书剩下的一半就都是它的推论了。
为什么没人监控这个数
这是我做这本书时最想不通、后来觉得最有意思的一点。
去看任何一套监控系统的默认面板:CPU 利用率、内存、QPS、延迟的 p50/p95/p99、错误率。这些指标覆盖了「有多满」和「结果多糟」,但没有一个指标在量「有多不齐」。
可这个数一点都不难算。你手上已经有了每个请求的耗时——延迟直方图就在那儿。cs² 就是那堆耗时的方差除以均值的平方,一行代码的事。
我猜原因是:它没有名字,所以没有人要它。「利用率」有名字,「延迟」有名字,「变异系数」这个词在软件工程的日常词汇里根本不存在。而在制造业和运筹学里,它是一等公民——丰田生产方式里的「平準化」(heijunka,把生产量摊平)整套东西,就是在压这个数。
如果这本书只能让你带走一个动作,我希望是这个:回去把你的服务延迟的 方差 / 均值² 打成一条曲线,挂在利用率旁边。你会立刻看见一堆以前解释不了的事。
我们习惯把偶发的慢请求叫做「毛刺」(glitch / spike),这个词暗示着「异常、偶发、可以忽略」。
排队论的立场恰好相反:那些毛刺不是噪声,它们是信号,而且它们是账单上最大的一项。一批请求里 10% 慢了 100 倍,会把 cs² 推到 7 以上,进而把平均等待推高好几倍——而那 90% 正常的请求,全都在为这 10% 买单。
所以「偶尔有个慢查询,问题不大」这句话在排队论里是不成立的。慢的那一个不只是它自己慢,它把整条队都拖住了。
急诊科。急诊的病床利用率经常只有 60%–70%,但等待时间可以到几小时。原因不是床不够,是「不齐」:病人到达是泊松的(成团来),而处理时长的差异是数量级级别的(缝个针 vs 心梗抢救)。医院管理里花大力气做的「分诊」,本质上就是把一条 cs² 很大的队,拆成几条 cs² 小的队。
CI 流水线。你的构建机器一天忙不到一半时间,但每次提交代码都要等半天。因为提交不是均匀发生的——大家都在下午四点推代码,而且有的构建 2 分钟、有的 40 分钟。这两件事叠在一起,把一台半闲的机器变成了一条长队。
数据库连接池。连接池监控显示「平均使用 12/50 个连接」,你觉得很宽裕。但如果偶尔有一个全表扫描占着连接不放,池子会在那一刻被打穿。平均使用率完全看不见这件事,而 cs² 一眼就能看见。
这个错误直觉的代价特别高,因为它会把人送上一条错误的排查路线:翻火焰图、抓 trace、找锁竞争、怀疑网络。这些都是在找「隐藏的瓶颈」——而当根因是变异时,这条路线什么也找不到,你会得出「找不到原因,先加机器吧」的结论。
加机器当然有用(它降低利用率),但那是在用最贵的方式解决问题。看看上面那台 demo:把 cs² 从 10 压回 1,等待直接掉到 1/5.5,一台机器都不用加。
D能涨十倍以上,而且没有上限。在 ρ=50% 时,服务完全整齐的等待是 0.50 个服务时长;cs² 拧到 4 是 2.50;拧到 10 是 5.50。而 cs² 本身是没有上界的——第 5 章你会见到一个 cs² 等于无穷的分布,它在真实系统里比你想的常见。
选 A 的人,直觉里装的是「平均值决定一切」。这个直觉在没有排队的世界里是对的:如果活可以随便攒着慢慢干,那只有总量重要。一旦引入「必须排队等」这个约束,方差就开始收费了。
A 「平均没变,所以等待没变」——平均值决定的是利用率,方差决定的是等待。这是两笔独立的账。 B 「涨一点点」——低估了一个数量级。这个选项通常来自「方差是二阶效应」的模糊印象,但在排队公式里方差是一阶的:它直接乘在结果上。 C 「大约翻倍」——如果 cs² 从 0 变成 1,答案恰好是从 0.50 变成 1.00,确实翻倍。但真实系统的 cs² 很少停在 1。这一章的一句话
一台闲了一半时间的机器排起长队,不是异常,是「不齐」在收费——而这笔费用不出现在任何一张默认监控面板上。
下一章处理另一个天天在骗你的数:平均值。在 ρ=0.8 的系统里,「平均等待 4 个服务时长」这句话的真实含义是:20% 的人一秒都不用等,63.2% 的人低于平均值,而有 1% 的人等了 21.910 个服务时长——是平均值的 5.48 倍。平均值落在这两群人中间的空地上,谁也不代表。