卷 I · 排起来CH 03深度 3/24

半空的系统,为什么也排队

上一次你看到「CPU 利用率 50%,但 p99 很难看」的时候,大概会觉得是监控出了问题,或者是某个隐藏的瓶颈。都不是。一台闲着一半时间的机器排起长队,是完全正常、完全可以预测的——只要你知道该看哪个数。

第二个成因变异性ρ 钉死 50%

▷ 先估一个数

一台机器,利用率精确地钉在 50%——它有整整一半的时间是空转的。到达是随机的(这一章里始终不变)。

现在只改一件事:服务时间的分布。从「每次都花恰好 1 秒」,改成「大部分很快、偶尔很慢,但平均仍然是 1 秒」。

平均等待时间会怎么变?

A 不变。平均服务时间没变,利用率没变,等待当然不变 B 涨一点点,也许 10%–20% C 大约翻倍 D 能涨十倍以上,而且没有上限

先确认一件让人不舒服的事

把利用率固定在 50%。机器有一半时间在空转。按管子模型,这样的系统不应该有队

但它有。而且队的长度完全取决于一个你可能从没量过的东西。

拖那根滑杆。利用率一动不动地钉在 50%,吞吐一动不动,机器的速度一动不动。而平均等待从 0.50 个服务时长走到 5.50 个服务时长——十一倍,而且滑杆右边还有路。

这条滑杆叫 cs²,读作「服务时间的变异系数平方」。它衡量的是不齐的程度。下一章会讲清楚它是怎么算的,这一章你只需要知道两件事:

  • 它等于 0,表示每次服务的时长完全一样
  • 它等于 1,表示服务时长是「纯随机」的(指数分布)——这是软件系统里最常见的默认假设;
  • 它大于 1,表示比纯随机还不齐:大部分请求很快,偶尔来一个特别慢的。

而现实中的软件系统,cs² 几乎总是大于 1。

为什么半空的机器会排队

直觉上的解释只需要一句话:机器闲的时候,你不在。

把一小时切成 60 分钟。机器一半时间闲着,也就是 30 分钟。但这 30 分钟不是整整齐齐地穿插在忙碌里的——它们东一块西一块。而请求来的时候,也不会挑机器闲的那一刻来。

于是就出现了这本书最核心的浪费:闲置产能和需求错过了。而错过了就是错过了。

# 同样是 50% 的利用率,同样的总工作量

# 整齐:忙一秒闲一秒,谁来都能立刻办
   时间轴 →
   ◉─○─◉─○─◉─○─◉─○─◉─○─◉─○
   队:                                    永远是空的

# 不齐:闲的时候没人来,来的时候撞一块
   时间轴 →
   ○─○─○─○─◉◉◉◉◉◉─○─○─○─◉◉◉
   队:            ▮▮▮▮▮              ▮▮▮
                  ↑ 这几个人在等,而机器刚刚闲了四格

两行的忙碌总量完全相同,利用率完全相同。第一行没有人等,第二行有人等——而且第二行浪费掉的空闲,和第二行产生的等待,是同一件事的两面

◆ 排队的两个成因

到这里,这本书的两个主角都到齐了:

  1. 太满(利用率高):闲置产能本来就少,禁不起错过。
  2. 太不齐(变异大):闲置产能和需求对不上,于是被错过。

这两个是相乘的关系,不是相加。这意味着:如果你的系统很不齐,那么即使利用率不高,你也会排队;如果你的系统很整齐,那么即使利用率很高,你也可能不排队。

第 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 倍。平均值落在这两群人中间的空地上,谁也不代表。