卷 I · 排起来CH 01深度 1/24

慢,不是因为不够快

你的服务器还有 20% 的富余,可用户已经在骂了。你的第一反应大概是加机器、优化代码、换更快的数据库——这三件事都在打同一个靶子:速度。而这一章想说的是,你打错靶了。

排队论入门跨领域对照表两台一样的机器

▷ 先估一个数

一台服务器,平均每秒能处理 1000 个请求。实际平均每秒来 800 个。处理时间和到达间隔都是随机的(没有任何人为的整齐安排)。

平均每个请求,要在队列里等多久?用「一次处理需要的时长」当单位来回答。

A 几乎不用等。机器还有 20% 富余,队根本排不起来 B 大约 0.25 个处理时长——毕竟只忙了八成 C 大约 1 个处理时长 D 4 个处理时长

先在心里落一个数,记住它。章末会告诉你答案,也会告诉你你偏了几倍——而「偏了几倍」这件事本身,就是这本书要讲的东西。

一个没有速度问题的速度问题

先把话说死:上面那台机器没有性能问题

它每秒能干 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、上更快的语言。这些动作不是没用,但它们在打三个因子里的一个。剩下两个因子——「有多满」和「有多不齐」——因为没有词,所以看不见;因为看不见,所以没人去动。

这本书的目标就是给你这套词汇。学完之后,当有人说「系统慢,加机器吧」,你能问出三个问题:

  1. 现在有多满?(利用率是多少,余量还剩多少)
  2. 有多不齐?(到达和服务的变异有多大)
  3. 一次要多久?(单次服务时间)

这三个问题不是我编的,它们是一个公式的三个因子。第 8 章你会拿到那个公式,而这本书剩下的部分,全是在拆解这三个因子怎么变、怎么量、怎么改。

✎ 术语正名:「利用率」

利用率(utilization,记作 ρ,读作 rho)在这本书里只有一个意思:服务台有多大比例的时间是忙的。80% 的利用率就是「每 10 秒钟里有 8 秒在干活」。

这个词被 IT 行业用坏了,因为它听起来像一个效率指标——利用率高 = 没浪费 = 好。但在排队论里它更像一个压力指标:它衡量的是你离崩溃有多近。这本书从头到尾会用「有多满」来提醒你它的真实含义。

一个具体的算法:ρ = 到达率 × 平均服务时间 ÷ 服务台数量。每秒来 800 个、每个花 1 毫秒、1 个服务台,那么 ρ = 800 × 0.001 ÷ 1 = 0.8

▸ 在现实里

你自己的日程表。如果你把一天排到 100% 满,任何一件事超时都会推倒后面全部的事,而且今天推明天、明天推后天,欠的债不会自己消失。日程排到八成的人不是效率低,是他在为「不齐」买单——而不齐是一定会发生的。

机场跑道。大型枢纽机场的跑道利用率在高峰期能到 90% 以上,于是所有人都体验过「飞机在跑道上排队 40 分钟」。跑道不慢,飞机也不慢,慢的是那 10% 的余量吸收不了天气、机务、空管的抖动。

超市收银台。下次去超市,数一下有几个收银台开着、有几个关着。关着的那些不是懒——那是超市在管理它的利用率。全开的成本是人力,全不开的成本是队。

✗ 这个直觉是错的
只要产能大于需求,队就排不起来。剩下的 20% 富余足够消化波动。 只要到达或服务里有任何一点随机性,队就一定会排起来,而且长度和「富余多少」是双曲线关系,不是线性关系。

这个直觉之所以顽固,是因为它在确定性的世界里完全正确。上面那台整齐的机器就是证据:准时来、准时走、80% 的利用率,等待精确地等于零。

问题是,现实里几乎没有确定性的到达。用户不会商量好每 1.25 秒来一个;后台任务不会均匀铺开;重试会扎堆;缓存会同时过期。只要有一点不齐,那 20% 的富余就不再是「缓冲垫」,而是变成了一个会被反复用光的资源。

◇ 结算

D平均每个请求要等 4 个处理时长。也就是说,如果一次处理要 1 毫秒,平均每个请求在队列里躺 4 毫秒——它等待的时间是被处理时间的四倍

如果你选了 A 或 B,你偏了 16 倍以上。这不是丢人的事:几乎所有人第一次都会低估,因为我们的直觉是线性的,而这件事是双曲线的。这本书接下来 23 章,做的就是把你的直觉从直线掰成曲线。

A 「还有富余,队排不起来」——只有当到达和服务都完全准时时才对。任何一点随机性都会让它失效。 B 「只忙了八成,所以等一小会儿」——这是把等待和利用率想成了线性关系。真实关系是 ρ/(1−ρ):80% 对应的不是 0.8,是 4。 C 「大约一个处理时长」——这个答案在 ρ=50% 时是对的。很多人的直觉停在「等一个人的时间」,而那恰好是半满时的答案。

这一章的一句话

系统变慢的时候,「不够快」只是三个嫌疑人之一,而且通常是最贵、最没用的那一个。

下一章先打地基:一条不需要任何假设、任何分布、任何前提就永远成立的定律。它只有三个字母,但它能让你在完全不知道系统内部长什么样的情况下,从两个容易测的数推出第三个难测的数。而且我会用三条完全不同的算法把同一个数算出来——4.07704.07704.0851