卷 III · 那面墙CH 11深度 11/24

我在这台电脑上,真的排了一遍队

前十章的公式全是我算的。你有充分的理由怀疑:这些漂亮的数字是不是只在纸上成立?所以这一章不算,只测——用真的 CPU,烧掉真的 27 秒,排出真的一万条队。

★★ 真机实测10200 位顾客微秒级对照

▷ 先估一个数

在一台真实的笔记本上跑一条真实的队:每次「服务」是一个真的 CPU busy loop,烧掉恰好 2 毫秒;请求按事先算好的时刻表到达;服务台是单线程的,必须做完一个才能做下一个。利用率精确控制在 80%

如果到达时刻完全均匀、每次服务时长完全一样(也就是 D/D/1),800 个请求的平均等待会是多少?

A 几毫秒。理论上是 0,但真机总有抖动、调度、GC,实际会有几毫秒 B 零点几毫秒 C 千分之一毫秒量级——也就是几微秒 D 不可能测出接近 0 的结果,操作系统的噪声会淹没它

怎么测的

排队论有个尴尬之处:它没有标准库可以对照。压缩算法可以拿 zlib 核,因果图可以拿 networkx 核,颜色空间可以拿 Google 的库核。排队论呢?没有一个公认的实现能说「这就是标准答案」。

所以这本书的对照对象是这台机器本身

// scripts/probe-wall/real-queue.mjs 的核心,一共就这么点

// 服务 = 真的烧 CPU,烧到时钟走够为止
function burn(ms) {
  const end = nowMs() + ms;
  let x = 1.0000001;
  while (nowMs() < end) {
    for (let i = 0; i < 64; i++) x = x * 1.0000001 + 1e-9;
  }
  return nowMs() - t0;      // ← 返回**真的**烧了多久
}

// 服务台:单线程,必须等前一个做完
for (let i = 0; i < n; i++) {
  const due = base + arr[i];            // 这一位该到的时刻
  if (nowMs() < due) spinUntil(due);    // 服务台闲着,等下一位到
  start[i] = nowMs() - base;            // ← 真的开始服务的时刻
  actual[i] = burn(want[i]);            // ← 真的烧掉的时长
}

没有队列数据结构,没有事件循环,没有随机数发生器在跑。队是真的排出来的:如果第 7 位到达时第 6 位还在烧 CPU,那第 7 位就真的在那儿干等着。

有一个实验设计上的细节值得说,因为它本身就是这本书的论点:

◆ 为什么要把 ρ 钉死

抽出来的服务时长和到达间隔,我做了一步整体缩放,让它们的样本均值精确等于目标值。

为什么?因为 n 只有一两千,光是抽样波动就能让平均服务时长偏 4%——于是实际的 ρ 从 0.90 漂到 0.94。而在墙脚下,这 4% 会被那条曲线放大成 30% 的等待差

换句话说:如果不做这一步,实验会被它想验证的那个效应本身给毁掉。这大概是这本书里最好的一个自指例子。

第一张表:真机 vs 公式

点上面那个 demo 的第一个标签页。六条队,每条都在这台机器上真的跑过。

最该看的是第一行

D/D/1 ρ=0.8,800 个请求,平均等待 0.0006 毫秒

零点零零零六毫秒——0.6 微秒。而这 0.6 微秒里,没有一微秒是排队:它是操作系统调度和时钟读取的噪声。

这台机器忙到 80%。它有整整八成的时间在烧 CPU。而 800 个请求里,没有一个等过

把它和第三行放在一起看:

利用率吞吐真机平均等待
D/D/1(准时来、准时走)80%400/秒0.0006 ms
M/D/1(随机来、准时走)80%400/秒4.2635 ms
M/M/1(随机来、随机走)80%400/秒6.6857 ms

三行的利用率完全相同、吞吐完全相同、机器完全相同。这不是三台机器,是同一台笔记本上的三次运行,中间只隔了几秒钟。

唯一的区别是「整齐程度」。而平均等待从 0.0006 毫秒走到 6.6857 毫秒——一万倍以上

第二张表:逐个顾客对照

光看平均值不够。平均值可以蒙对。所以我做了更狠的一件事。

把真机真的烧掉的那些服务时长(不是我要求的,是实际测出来的)喂回 Lindley 递推:

开始时刻 = max( 到达时刻 , 上一位的结束时刻 )

然后逐个顾客,比对「公式预测的开始时刻」和「真机上真的开始的时刻」。

点第二个标签页。10200 位顾客,中位误差 0.08 到 7.59 微秒。

微秒。在一个平均服务时长是 2000 微秒的系统里。

◆ 这个对照证明了什么,没证明什么

证明了:Lindley 递推——也就是这本书所有模拟的基础——不是一个模型,它就是这台机器实际在做的事。误差是操作系统的调度抖动,不是排队论的误差。

没证明:稳态公式(比如 Wq = ρ/(1−ρ)·E[S])在有限样本上会精确成立。看第一张表就知道:M/M/1 那一行,真机量到 6.6857 毫秒,公式说 8.0000 毫秒,差了 16%

这个差距不是错误,它是样本量不够。第 24 章会给出这个差距该有多大的精确分布——剧透一下:在 n=3000 的样本上,M/M/1 的样本均值有 90% 的概率落在 [6.48, 9.79] 这个区间里,而稳态值是 8.00。真机量到的 6.6857 落在这个区间的低端,完全正常。

第三张表:真机上的那面墙

点第三个标签页。同一台机器,同样的代码,只改利用率:

利用率余量真机平均等待公式相对 50% 时
50%50%1.7533 ms2.0000 ms1.00
70%30%3.8449 ms4.6667 ms2.19
80%20%6.6857 ms8.0000 ms3.81
90%10%18.5242 ms18.0000 ms10.57

机器一点没变慢。每次服务仍然精确地烧 2 毫秒。吞吐倒是涨了(那是利用率上升的定义)。而平均等待涨了 10.57 倍

这就是第 9 章那条曲线,在一台真的笔记本上,用真的 CPU 烧出来的。

✎ 术语正名:「模拟」和「实验」

这本书里有两种不同性质的数字,我尽量把它们分开标注:

  • 解析:公式算出来的。精确,但依赖模型假设。
  • 模拟:引擎跑出来的。也在纸上,但不依赖闭式解,可以处理公式解不出的情况。
  • 实测:这一章这种。真的 CPU,真的时钟,真的等待。

三者对上,结论才可信。三者对不上,得说清楚是哪一环出了问题——这一章的 M/M/1 那 16% 的差距就属于这种情况,而它的解释(有限样本)本身是可以验证的(第 24 章)。

一个实测数字每次跑都不一样。所以这一章引用的那张表是某一次真跑的快照,存在 scripts/probe-wall/snapshot.json 里。你自己重跑一遍,数字会不同——第 24 章会告诉你不同多少才算正常。

▸ 在现实里

去测你自己的系统,而不是相信这本书。这一章真正想传达的方法是:排队论的每一个结论,都可以在你自己的环境里花几十行代码验证。你不需要相信我的笔记本。

「让请求变整齐」是一个真的能做的动作。D/D/1 那一行不是一个数学理想国。定时任务加抖动、批量作业拆成固定大小、限流器用漏桶而不是令牌桶(漏桶输出是定时的)、给下游发请求时做速率整形——这些都是在把 ca² 往 0 推。而那一行说明了这么做的上限有多高。

压测工具的默认模式在骗你。大多数压测工具默认「固定并发数 + 尽快发下一个」,这产生的是闭环负载,到达间隔被系统自己的延迟调节了,ca² 很小。而生产环境是开环的:用户不会因为你慢就少发请求。所以压测结果系统性地乐观。要测真实情况,得用固定到达率模式(很多工具叫 constant arrival rate 或 open model)。

✗ 这个直觉是错的
这些公式是理想化模型,真实系统有那么多噪声——上下文切换、缓存、GC、调度器——实际表现肯定和理论差很远。 噪声确实存在,但它的量级是微秒,而排队效应的量级是毫秒。差三个数量级。排队不是被噪声淹没的效应,排队是那个把噪声放大成事故的机制。

这个直觉之所以流行,我猜是因为工程师被「理论 vs 实践」这个框架训练得太好了。在很多领域这个警惕是对的——但在这里它反了:排队论恰恰是那个解释噪声后果的理论,它不和噪声竞争,它以噪声为输入。

看第二张表:真机和 Lindley 递推的中位误差是 0.08–7.59 微秒。整台笔记本上跑着浏览器、编辑器、后台进程,而队列的行为仍然被一行 max(到达, 上一个结束) 预测到微秒级。

真正会让理论失效的不是噪声,是模型假设错了:到达其实是相关的(重试、推送)、服务时间其实是重尾的(=∞)、或者队列其实有上限(第 21 章)。这些都是建模问题,不是「理论不接地气」。

◇ 结算

C千分之一毫秒量级。实测 0.0006 毫秒,也就是 0.6 微秒。而这 0.6 微秒还不是排队,是时钟读取和调度的噪声。

选 A 或 D 的人(我第一次做这个实验之前也是这么以为的)低估了一件事:D/D/1 在 ρ<1 时的等待是精确的 0,不是「很小」。不是「噪声掩盖了小的排队」,是根本没有排队发生——每一位到达时,上一位早就走了。噪声是唯一的贡献者,而它只有微秒级。

A 「真机总有几毫秒抖动」——高估了操作系统噪声三个数量级。在一台空闲的现代机器上,单线程 busy loop 的调度抖动中位数在微秒量级;毫秒级的抖动要靠 GC 停顿或者页错误才能造出来。 B 「零点几毫秒」——同样是高估,但方向感对了。逐位对照里的 p99 误差确实到了几十微秒,最大值到过 0.13 毫秒——那大概是一次 GC 或者一次页错误。 D 「噪声会淹没它」——这正好说反了。因为理论值是精确的 0,所以测出来的一切都是噪声,也就等于给你测了一遍噪声的量级:0.6 微秒。这个实验反而成了一次很好的噪声测量。

这一章的一句话

那行 开始 = max(到达, 上一个结束) 不是模型,它就是机器在做的事——微秒级地对得上。

下一章是这本书的招牌,也是卷 III 的收尾。同一份工作量:同样的吞吐、同样的平均服务时间、因此同样的利用率 80%。只改「整齐程度」,五种走法。平均等待从 0 走到 22.0000 个服务时长,p99 从 0 走到 140.48,而「我最多能忍两个服务时长」这个要求下,你能买到的产能从 100% 掉到 26.7%