我在这台电脑上,真的排了一遍队
前十章的公式全是我算的。你有充分的理由怀疑:这些漂亮的数字是不是只在纸上成立?所以这一章不算,只测——用真的 CPU,烧掉真的 27 秒,排出真的一万条队。
在一台真实的笔记本上跑一条真实的队:每次「服务」是一个真的 CPU busy loop,烧掉恰好 2 毫秒;请求按事先算好的时刻表到达;服务台是单线程的,必须做完一个才能做下一个。利用率精确控制在 80%。
如果到达时刻完全均匀、每次服务时长完全一样(也就是 D/D/1),800 个请求的平均等待会是多少?
怎么测的
排队论有个尴尬之处:它没有标准库可以对照。压缩算法可以拿 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 ms | 2.0000 ms | 1.00 |
| 70% | 30% | 3.8449 ms | 4.6667 ms | 2.19 |
| 80% | 20% | 6.6857 ms | 8.0000 ms | 3.81 |
| 90% | 10% | 18.5242 ms | 18.0000 ms | 10.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)。
这个直觉之所以流行,我猜是因为工程师被「理论 vs 实践」这个框架训练得太好了。在很多领域这个警惕是对的——但在这里它反了:排队论恰恰是那个解释噪声后果的理论,它不和噪声竞争,它以噪声为输入。
看第二张表:真机和 Lindley 递推的中位误差是 0.08–7.59 微秒。整台笔记本上跑着浏览器、编辑器、后台进程,而队列的行为仍然被一行 max(到达, 上一个结束) 预测到微秒级。
真正会让理论失效的不是噪声,是模型假设错了:到达其实是相关的(重试、推送)、服务时间其实是重尾的(c²=∞)、或者队列其实有上限(第 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%。