慢,不是因为不够快
你的服务器还有 20% 的富余,可用户已经在骂了。你的第一反应大概是加机器、优化代码、换更快的数据库——这三件事都在打同一个靶子:速度。而这一章想说的是,你打错靶了。
一台服务器,平均每秒能处理 1000 个请求。实际平均每秒来 800 个。处理时间和到达间隔都是随机的(没有任何人为的整齐安排)。
平均每个请求,要在队列里等多久?用「一次处理需要的时长」当单位来回答。
先在心里落一个数,记住它。章末会告诉你答案,也会告诉你你偏了几倍——而「偏了几倍」这件事本身,就是这本书要讲的东西。
一个没有速度问题的速度问题
先把话说死:上面那台机器没有性能问题。
它每秒能干 1000 个活,来了 800 个,产能是够的,而且富余 20%。你去看监控,CPU 大概 80%,内存正常,没有慢查询,没有 GC 停顿,代码 review 挑不出毛病。所有指标都很健康。
然后你去看 p99 延迟,它是处理时间本身的二十几倍。
这不是一个假想的例子,这是绝大多数线上系统的日常。而它之所以让人困惑,是因为我们脑子里有一个非常顽固的模型:把系统想成一根管子。管子粗(快),水流得快;管子细(慢),水流得慢。按这个模型,来 800 走 1000,水应该畅通无阻。
管子模型错在哪?错在它假设水是均匀流过来的。
# 同样是 80% 的忙碌,同样的吞吐,两种截然不同的日子
# 均匀地来(管子模型想象中的样子)
时间轴 →
◉──○─◉──○─◉──○─◉──○─◉──○─◉──○
队: 永远是空的
# 实际上(到达和服务都带一点随机)
时间轴 →
○─○─◉◉◉◉◉◉◉◉─○─○─◉◉◉◉─○─◉◉◉◉◉◉◉
队: ▮▮▮▮▮▮ ▮▮ ▮▮▮▮▮
↑ 这些人在等,而机器刚刚闲了两格
# ◉ 忙 ○ 闲 ▮ 在队里等着
# ★ 两行的忙碌总量完全相同 —— 但只有一行有人在等。
两台一模一样的机器
下面这台 demo 里有两台机器。它们的每一项「性能指标」都完全相同:
- 平均每 1.25 个时间单位来一个请求(所以吞吐一样);
- 平均每个请求花 1.00 个时间单位(所以速度一样);
- 因此利用率都是 80%(所以忙闲一样)。
唯一的区别是:一台的到达和服务都准时,另一台的到达和服务都随机。
整齐的那台,队列长度在 0 和 1 之间来回跳,没有任何一个请求等过哪怕一瞬间。随机的那台,同样的负载下平均每个请求等 4.11 个处理时长,而且队里最多的时候排了 40 个人。
四十个人。在一台还有 20% 富余的机器上。
排队不是「活太多」造成的,也不是「机器太慢」造成的。排队是忙和闲没对齐造成的:机器闲的那 20% 的时间,和请求来的时间,恰好没有重合。
而一旦错过,那点闲置产能就永远消失了——你没法把上午没用完的 CPU 存到下午用。这是排队论里最容易被忽略、也最要命的一条:产能是易腐品。
你以为它是 X,其实它是 Y
这张表是这本书的地图。左边这些句子,大部分人(包括读这本书之前的我)都会点头。右边是学完之后你会怎么看它们。表里每一行都对应后面的某一章。
| 你以为 | 其实 |
|---|---|
| 系统慢 = 机器不够快 | 系统慢 = 在排队。而排队有两个成因,「不够快」只是其中一个(第 8 章) |
| 服务器跑到 100% 才叫物尽其用 | 跑到 100% 意味着队列无限长。空着的那部分不是浪费,是你花钱买的响应时间(第 9 章) |
| 堵车是因为车太多 | 密度过了临界点,整条路的通过量本身会下降。幽灵堵车里一辆车都没多(第 23 章) |
| 医院等太久是床位不够 | 常常是手术时长的方差太大。压方差比加床便宜一个数量级(第 5 章) |
| 「平均等 10 分钟」意味着大部分人等 10 分钟 | 等待时间是重尾的。ρ=0.8 时有 63.2% 的人低于平均值,1% 的人等 5.48 倍(第 4 章) |
| 我这队排得慢,是运气差 | 不是运气。你在随机时刻加入,本来就更容易掉进长队里(第 7 章) |
| 忙 = 高效 | 忙 = 没有余量。而余量是唯一能吸收「不齐」的东西(第 10 章) |
| 加人能让项目更快 | 过了某个点,加人让总吞吐下降——不是收益递减,是收益为负(第 15 章) |
| p99 是运气,只能靠优化代码 | p99 由利用率和「不齐」决定,可以算出来,也可以设计出来(第 16 章) |
| 缓冲区大一点更安全 | 大缓冲区几乎不增加吞吐,只是把「拒绝」换成了「等到超时」(第 21 章) |
| 先来后到是最公平的 | 先来后到和后来先服务的平均等待完全相同。它俩的区别只是「把方差分给谁」(第 17 章) |
如果这张表里有超过三行让你觉得「等等,真的吗」,那这本书就值得你读下去。
为什么这件事没有名字
有一件事我一直觉得很奇怪:软件工程师人手一套关于「速度」的词汇——延迟、吞吐、QPS、火焰图、算法复杂度——但关于「排队」几乎一个词都没有。
结果就是,当一个系统开始变慢,我们能想到的动作全都指向速度:加机器、加缓存、优化 SQL、上更快的语言。这些动作不是没用,但它们在打三个因子里的一个。剩下两个因子——「有多满」和「有多不齐」——因为没有词,所以看不见;因为看不见,所以没人去动。
这本书的目标就是给你这套词汇。学完之后,当有人说「系统慢,加机器吧」,你能问出三个问题:
- 现在有多满?(利用率是多少,余量还剩多少)
- 有多不齐?(到达和服务的变异有多大)
- 一次要多久?(单次服务时间)
这三个问题不是我编的,它们是一个公式的三个因子。第 8 章你会拿到那个公式,而这本书剩下的部分,全是在拆解这三个因子怎么变、怎么量、怎么改。
利用率(utilization,记作 ρ,读作 rho)在这本书里只有一个意思:服务台有多大比例的时间是忙的。80% 的利用率就是「每 10 秒钟里有 8 秒在干活」。
这个词被 IT 行业用坏了,因为它听起来像一个效率指标——利用率高 = 没浪费 = 好。但在排队论里它更像一个压力指标:它衡量的是你离崩溃有多近。这本书从头到尾会用「有多满」来提醒你它的真实含义。
一个具体的算法:ρ = 到达率 × 平均服务时间 ÷ 服务台数量。每秒来 800 个、每个花 1 毫秒、1 个服务台,那么 ρ = 800 × 0.001 ÷ 1 = 0.8。
你自己的日程表。如果你把一天排到 100% 满,任何一件事超时都会推倒后面全部的事,而且今天推明天、明天推后天,欠的债不会自己消失。日程排到八成的人不是效率低,是他在为「不齐」买单——而不齐是一定会发生的。
机场跑道。大型枢纽机场的跑道利用率在高峰期能到 90% 以上,于是所有人都体验过「飞机在跑道上排队 40 分钟」。跑道不慢,飞机也不慢,慢的是那 10% 的余量吸收不了天气、机务、空管的抖动。
超市收银台。下次去超市,数一下有几个收银台开着、有几个关着。关着的那些不是懒——那是超市在管理它的利用率。全开的成本是人力,全不开的成本是队。
这个直觉之所以顽固,是因为它在确定性的世界里完全正确。上面那台整齐的机器就是证据:准时来、准时走、80% 的利用率,等待精确地等于零。
问题是,现实里几乎没有确定性的到达。用户不会商量好每 1.25 秒来一个;后台任务不会均匀铺开;重试会扎堆;缓存会同时过期。只要有一点不齐,那 20% 的富余就不再是「缓冲垫」,而是变成了一个会被反复用光的资源。
D平均每个请求要等 4 个处理时长。也就是说,如果一次处理要 1 毫秒,平均每个请求在队列里躺 4 毫秒——它等待的时间是被处理时间的四倍。
如果你选了 A 或 B,你偏了 16 倍以上。这不是丢人的事:几乎所有人第一次都会低估,因为我们的直觉是线性的,而这件事是双曲线的。这本书接下来 23 章,做的就是把你的直觉从直线掰成曲线。
A 「还有富余,队排不起来」——只有当到达和服务都完全准时时才对。任何一点随机性都会让它失效。 B 「只忙了八成,所以等一小会儿」——这是把等待和利用率想成了线性关系。真实关系是ρ/(1−ρ):80% 对应的不是 0.8,是 4。
C 「大约一个处理时长」——这个答案在 ρ=50% 时是对的。很多人的直觉停在「等一个人的时间」,而那恰好是半满时的答案。
这一章的一句话
系统变慢的时候,「不够快」只是三个嫌疑人之一,而且通常是最贵、最没用的那一个。
下一章先打地基:一条不需要任何假设、任何分布、任何前提就永远成立的定律。它只有三个字母,但它能让你在完全不知道系统内部长什么样的情况下,从两个容易测的数推出第三个难测的数。而且我会用三条完全不同的算法把同一个数算出来——4.0770、4.0770、4.0851。