卷 II · 不齐CH 05深度 5/24

把「不齐」变成一个数

上一卷反复说「不齐要收费」,但没说怎么量。这一章给出那个量:一个无量纲的数,一行代码就能从你现有的延迟直方图里算出来。它有名字,叫变异系数的平方,而它是这本书里第二重要的符号。

c² 的定义七种分布无穷方差

▷ 先估一个数

一个接口,90% 的请求花 1 毫秒,10% 的请求花 100 毫秒(比如缓存没命中要回源)。

你想知道这个接口「有多不齐」。用 方差 ÷ 平均值² 这个量来衡量的话,它是多少?(提醒:完全整齐是 0,纯随机是 1。)

A 大约 0.1——毕竟只有一成的请求是慢的 B 大约 1,和纯随机差不多 C 大约 7.4 D 大约 100,因为慢的比快的慢一百倍

为什么不能直接用方差

「不齐」的第一直觉是方差标准差。但它们有个致命的问题:带单位

一个接口平均 1 毫秒、标准差 0.5 毫秒;另一个接口平均 1 秒、标准差 0.5 秒。第二个的标准差是第一个的一千倍——但它们「不齐的程度」完全一样。

所以要除掉尺度。除以平均值,得到变异系数(coefficient of variation):

c = 标准差 ÷ 平均值

# 但排队论里到处用的是它的平方,因为公式里出现的总是平方项
c² = 方差 ÷ 平均值²

这个数是无量纲的:不管你用毫秒、秒还是年,算出来都一样。而且它有一把天然的尺子:

含义典型例子
0完全整齐,每次一模一样定时任务、固定长度的数据包
0.25比较整齐Erlang-4:四道固定工序串起来
1纯随机——这是分界线指数分布,「无记忆」
4相当不齐大部分快、有一批明显慢的
7.4很不齐90% 快 + 10% 慢一百倍
方差不存在帕累托 α ≤ 2,幂律尾

点一遍上面那些分布。注意最后一列:平均值全都是 1.00,可 从 0 跨到无穷,而同样喂给一台 ρ=0.8 的机器,等待从 2.00 走到 16.85,最后一行干脆是无穷

为什么 c² = 1 是分界线

等于 1 对应的是指数分布,而指数分布在排队论里的地位相当于概率论里的正态分布——不是因为它常见,而是因为它有一条特殊性质:无记忆

# 无记忆性
P(还要再等 t | 已经等了 s)  =  P(要等 t)

# 人话:不管你已经等了多久,"还要等多久" 的分布完全不变。
# 一个已经跑了 10 分钟的任务,和一个刚开始跑的任务,
# 剩余时间的期望是**一样的**。

这条性质有点反直觉,但它把「随机」这个词的含义定死了:过去不含任何关于未来的信息

于是 有了一个非常好用的解读:

  • < 1:等得越久,越快结束。因为服务时间集中在均值附近,已经花了很久说明快做完了。(固定工序的流水线就是这样。)
  • = 1:等多久都不影响剩余时间的期望。
  • > 1:等得越久,剩下的越久。因为长任务大概率属于「慢的那一类」。

最后一条在真实软件系统里成立得可怕。一个已经跑了 30 秒的数据库查询,它的期望剩余时间比刚开始时更长,因为它大概率是个全表扫描。这条性质直接决定了超时策略该怎么设——也直接决定了第 18 章那个「让短的先走」为什么那么有效。

◆ 一行代码

你的服务已经在记录每个请求的耗时了。把它们攒起来,算这一行:

const mean = xs.reduce((a, b) => a + b, 0) / xs.length;
const varr = xs.reduce((a, b) => a + (b - mean) ** 2, 0) / (xs.length - 1);
const cv2  = varr / (mean * mean);        // ← 就是它

然后把这条曲线挂在利用率旁边。这两条线放在一起,就是第 8 章那个公式的两个因子——你的监控面板第一次有能力回答「为什么慢」,而不只是「有多慢」。

一个实用的阈值: 长期超过 4,说明你的流量里混着两类完全不同的请求,值得去查(大 key、缓存穿透、N+1 查询、重试)。

无穷方差:公式失效的地方

上面那张表最后一行需要认真对待,因为它不是「一个很大的数」,而是「这个数不存在」。

帕累托分布(幂律)在真实系统里到处都是:文件大小、请求返回的数据量、一个用户有多少好友、一个 key 有多热。它的形状是 P(X > x) ∝ x^(−α)

# 帕累托分布的矩
α > 1  平均值存在
α > 2  方差存在,c² = 1 ÷ (α(α−2))
α ≤ 2  ★ 方差是无穷的

# 也就是说:
α = 2.5  →  c² = 1 ÷ (2.5 × 0.5) = 0.800   还行
α = 2.1  →  c² = 1 ÷ (2.1 × 0.1) = 4.762   有点糟
α = 2.0  →  c² = ∞                          公式在这里失效
α = 1.5  →  c² = ∞                          平均值还在,方差没了

是无穷时,这本书从第 8 章开始的所有公式都不成立——不是「不准」,是「无意义」。稳态平均等待时间不存在。

这不是数学洁癖。它对应一个非常具体的现实:你测出来的「平均等待」会随着观察时间越来越长而越来越大,永远不收敛。你今天量到 50 毫秒,下周量到 80 毫秒,下个月量到 200 毫秒,中间什么都没改。这不是系统在退化,这是重尾分布在展现它自己。

好消息是:真实系统总有物理上限(超时、最大响应体、磁盘大小),所以严格的无穷方差不会发生。坏消息是:在超时被触发之前,系统的行为和无穷方差没有区别。这也是为什么「设一个合理的超时」不只是防御性编程,它是在数学上把你的系统从一个没有解的区域拽回有解的区域

✎ 术语正名:「抖动」

网络工程里的「抖动」(jitter)通常指延迟的标准差,或者相邻包延迟差的绝对值。它和这一章的 是亲戚,但有一个关键区别:抖动带单位, 不带

这个区别在比较不同系统时很致命。「A 服务抖动 5 毫秒,B 服务抖动 50 毫秒,所以 B 更不稳定」——如果 A 的平均延迟是 2 毫秒而 B 是 500 毫秒,那么 分别是 6.25 和 0.01,结论完全反过来:A 才是那个乱七八糟的。

排队的账单认的是 ,不是抖动的绝对值。

▸ 在现实里

丰田的「平準化」。丰田生产方式里有一个词叫 heijunka,直译是「平準化」,做的事就是把订单打散、把生产量摊平,让每天、每小时的产量尽量一样。西方管理学界一开始把它理解成「减少浪费」,其实它更精确的解释是 压下去——因为压 和加产能对等待时间的效果是同量级的,而前者几乎不花钱。

为什么大 key 那么可怕。Redis 里一个 1MB 的 value 和一堆 100 字节的 value 混在同一个单线程实例上。平均值看着没事,但那个大 key 每次被读,都会把整条队堵住一小会儿。它对 的贡献是平方级的——一个慢一百倍的请求,对方差的贡献是慢十倍的那个的一百倍

为什么固定长度的包更好。ATM 网络当年选择 53 字节的定长信元,而不是变长包,理由之一就是把 钉死在 0。虽然 ATM 最后输给了以太网(原因跟排队论无关),但那个设计动机在今天的实时音视频里仍然活着。

✗ 这个直觉是错的
只有一成的请求慢,那对整体的影响也就一成左右。先去优化那 90% 的主路径,收益更大。 方差是平方加权的。10% 的请求慢 100 倍,贡献的方差是那 90% 的一千倍以上。优化少数极慢的请求,几乎总是比优化多数正常请求划算。

算一下就清楚了。设 90% 的请求耗时 1,10% 耗时 100:

平均值 = 0.9×1 + 0.1×100 = 10.9        ← 已经被那 10% 拉高了
E[X²]  = 0.9×1 + 0.1×10000 = 1000.9
方差   = 1000.9 − 10.9² = 882.1
c²     = 882.1 ÷ 10.9² = 7.424

# 那 10% 贡献了 E[X²] 的 99.91%。
# 换句话说:**排队的账单几乎完全由那一成慢请求开出**。

把那 10% 从 100 优化到 50(只快一倍), 会掉到 3.7 左右,平均等待直接减半。而把那 90% 从 1 优化到 0.5(也快一倍),几乎什么都不会发生。

同样的工程量,收益差一个数量级。而决定往哪投的那个数,就是

◇ 结算

C = 7.424。推导就在上面那个方框里。

顺带一提,这个接口在 ρ=0.8 时的平均等待是 16.85 个服务时长——而如果它的耗时完全整齐,只要 2.00八倍的差距,全部来自那一成的慢请求。

A 「一成的请求慢,影响也就一成」——线性直觉。方差是平方加权的,慢一百倍的东西在方差里的分量是一万。 B 「和纯随机差不多」——纯随机(=1)其实相当「温和」。双峰分布比指数分布不齐得多,因为它把概率质量推到了两个极端。 D 「大约 100」——注意 除以平均值的平方之后的量。平均值本身已经被那一成慢请求拉到 10.9 了,所以最终结果远小于 100。这也是为什么用无量纲的 而不是原始方差。

这一章的一句话

「不齐」是一个可以用一行代码算出来的无量纲数,而排队的账单几乎完全由分布最右边那一小撮开出。

下一章处理「随机」这个词本身。我会给你两排点,一排是真正的随机到达,一排是「排得比较匀」的假随机,让你猜哪排是真的。绝大多数人会猜错——因为真随机长得比人们想象的成团得多:平均每秒来一个人的流量里,有 36.9% 的秒钟一个人都不来,同时有 26.3% 的秒钟来两个以上。