电梯调度:LOOK、RSR 与目的楼层派梯

关于本文

译文,原文是 john.fun/elevators(作者 John Herrick)。原文是一篇带可交互演示的网页文章。

原文没有公开源码,但它的前端 bundle 没有做混淆,只是被压平了。我把模拟引擎和全部派梯算法还原了出来,并逐帧验证与原站一致。文中所有可交互演示都是从原站移植过来的真实引擎在跑,不是重画的示意图。

译者注是我加的,用引用块标出,与原文正文明确区分。

目录


电梯调度

每个人都体会过等一部迟迟不来的电梯。「我按了按钮,它怎么还不来?」你会这么问。电梯看起来再普通不过,实际比表面上复杂得多。

这篇文章会拆开电梯的运作方式——你怎么按它的按钮,它又怎么按你的。

一、单梯

最简单的电梯算法叫 SCAN,1961 年申请了专利。电梯从大堂出发,一路开到顶层,然后掉头往回走,沿途接人和放人。

大多数时候你并不需要去顶层。如果电梯只开到被请求的最高层就掉头,这个算法叫 LOOK——大多数人熟悉并期待的就是它。

二、多梯

从这里开始变得有意思了。如果有好几部电梯,它们怎么协调谁去接谁?

最基本的系统里有一个中央调度器,告诉每部电梯该停哪些楼层。新请求进来时,分配给最近的那部。但后面会看到,我们可以做得更好。

三、漫长的等待

那要怎么衡量一个电梯算法的好坏?最直观的指标是你等电梯到的时间。

一个很简单的度量是「电梯在 30 秒内到达的频率」,或者「90 秒内到达的频率」。

四、应用统计

更严格地说,我们要看等待时间的分布。如果把几千次乘梯的等待时间画出来,就得到下面这张直方图。

p90 是 2 分钟,意思是 90% 的情况下乘客等待不超过 2 分钟。p50 是 1 分钟,意味着有一半时候电梯在 1 分钟内就到了。

人们通常记不住自己平均等了多久。他们记住的是电梯等了好久好久的那些时刻——也就是 p90 那一档。

译者注:原文这里有个可以直接验证的点。上面这个演示跑了 8 层 4 车、每分钟 18 人到达的配置,等它积累到几百个样本再观察——p50 会很快稳定在 20 秒左右,但 p90 要跑很久才收敛(我实测从 62 秒一路降到 53 秒还没停)。这解释了为什么小样本下的 p90 完全不可信:它衡量的恰恰是稀有的坏情况,而稀有事件需要更多样本才出现。

五、早高峰

不是所有客流都是一样的。想象一栋大型写字楼:早上几乎所有出行都是从大堂上楼。

到了傍晚就反过来了,所有人都往外走。午间的流量两者兼有,剩下的一些是楼层之间的往来。

等待时间的分布随一天中的时段和电梯面对的客流模式而剧烈变化。早高峰的等待统计是出了名的差。

六、更聪明的电梯

分析 LOOK 算法时,我们看(原文在这里玩了个双关:LOOK)了乘客是怎么分配给各部的。我们当时天真地按「最近的车」分配,并说可以做得更好。

如果最近的那部车已经满了呢?可以用 Otis 的 RSR(Relative System Response,相对系统响应) 算法做得更聪明。RSR 给每部车打分,衡量它有多适合去接某位乘客,分数越低越好。

RSR 接送评分

分数 = 预计接到时间
     + 车内载客惩罚
     + 同向防扎堆惩罚
     − 方向匹配奖励
     − 空闲就近奖励
     − 低载奖励

防扎堆:如果另一部车已经朝同一个楼层的同一方向去了,惩罚这部车。

空闲就近:呼叫者两层以内的空闲车获得奖励。

RSR 还每 5 秒重新优化一次。原本由 A 车接的乘客,如果 A 车遇到延误,可以被重新分配给 B 车。这个重新优化被证明是理顺客流的关键。

七、LOOK vs RSR

有了分析电梯的工具,就可以对比 LOOK 和 RSR 的表现,看看更聪明的算法究竟能把等待时间改善多少。

有意思的是,流量越高,LOOK 反而开始胜过 RSR。当电梯总是满载、每层都停时,那些额外规则就不再重要了。

在电梯数量少的小楼里,LOOK 也倾向于胜过 RSR。有时候,保持简单就是更好。

另一个可以追踪的指标是乘梯时长,也就是你进了电梯之后多久才到自己的楼层。RSR 和 LOOK 在这方面也有不同的特性,但超出了本文的范围。

译者注:原文把原因归结为「额外规则不再重要」。我读了源码后认为实际机制要更强一点——在高流量下,RSR 的额外项从信号退化成了噪声。

原因在第七节的对比里:LOOK 那一侧是纯 ETA,一行代码(score: (sim, car, call) => sim.estimateBoardingETA(...)),连容量检查都没有;而 RSR 那一侧带了五项调整。流量高时所有车都在往各个方向跑、都载得满,方向匹配奖励和低载奖励失去了区分度——它们不再区分好坏,却仍然在给评分加抖动,让派梯偏离真正最优的 ETA。

所以不是「规则不重要」,而是「这些规则在这里有害」。这也意味着原文的对比里混着两个变量:算法思想,和实现完整度。

八、目的楼层派梯

不是所有电梯里都有按钮。一些新的高级电梯在每层放一个终端,让你在电梯到达之前就指定要去哪层,然后终端告诉你该等哪部电梯。

这叫目的楼层派梯(Destination Dispatch)。乍看很棒:优化器现在完全知道谁要去哪里,当然可以用它来降低等待时间,对吧?

结果是,这些花哨的终端在大多数情况下比传统上下按钮更差。确实有例外(每组的电梯数 ≥ 8 的超高层建筑),但在大多数场合,朴素的上下按钮更胜一筹。

这个反直觉的结果,全都要归功于每隔 5 秒重新优化每部电梯路径的那个重平衡步骤。终端强制了刚性——你必须上被指派的那部车。

你按下按钮 30 秒后的世界状态可能已经很不一样了,但系统无法适应。灵活性上的损失,超过了多出来的信息给优化器带来的好处。

译者注:源码把原因交代得比原文更干净——它是一个布尔值造成的。目的楼层派梯的配置里写着 pinAssignment: true(乘客被钉死在被指派的车,只在满载弹回时重新分派);而传统派梯的指派只是建议,任何同向的车到了都能接走人。

关键在于:这个「承诺可被抢占」的设计,恰恰是每 5 秒全局重优化能够成立的前提。传统系统之所以敢把所有人的承诺作废重来,正是因为那个承诺本来就不硬。kiosk 把这个机制的地基抽掉了。

九、完整模拟

这里有一个完整的模拟器,所有按钮和旋钮都能调。

十、结论

这篇文章只触及了电梯算法的表面。下次你卡在等电梯的时候,别太往心里去。电梯确实听见你了,它只是有很多事要想。


附:这份还原是怎么做的

译者注——这一节是我加的。

原文没有公开源码。但它的前端 bundle 没有做混淆,只是被打包器压平了(变量名被改成 e/t/i,控制流和字面量完整保留)。npx prettier 一过就是可读代码。

还原出来的东西包括:模拟引擎(1399 行)、8 个派梯策略(含 3 个原站隐藏的诊断变体)、SVG 渲染器,以及完整的物理参数。

正确性不是自称的,是对拍出来的。 我把还原版和从 bundle 里原样抠出的原函数放在一起跑同样的输入:

函数级对拍:111000 通过,0 失败
整机对拍:72000 帧一致,0 个场景出现偏差

整机对拍是让两个引擎在同一配置、同一种子下各跑 12000 帧(600 秒模拟时间),逐帧比对完整快照(车位置、速度、状态、候梯人归属、等待时间序列)。6 套配置覆盖了 LOOK / RSR / Traditional / 目的楼层派梯,单梯与多梯都有。

对拍抓到了四个我自认为写对了、实际写错了的地方,其中一个是把源码里两个不同的函数当成了同一个(一个返回秒、一个返回速度),会导致 ETA 全是 NaN——动画照跑,但行为完全不对,肉眼很难发现。

复跑验证:

node research/elevators/verify-all.mjs

引擎的几个行为特征

读源码时有两件事值得单独说,因为它们和「交互式演示应该是什么样」有关:

  1. 改布局会重置整个模拟。 楼层数或车数一变,引擎调用 reset()——统计清零、所有人重新生成。所以那句「所有按钮和旋钮都能调」的准确含义是「改参数 → 重新跑」,不是拖滑块看实时反应。
  2. 步长必须是 0.05 秒。 开关门动画的进度由 doorTime 直接驱动,放大 dt 会让动画失步。所谓「100x 加速」是在一帧里跑约 34 个子步实现的,不是把时间轴拉快。

参考资料

  • John Herrick. Elevators. john.fun/elevators
  • Barney, G.C. Elevator Traffic Handbook(SCAN / LOOK / 群控调度的标准参考)
  • Smith, R. & Peters, R. ETD and System Degradation Factor. ELEVCON 2002, ThyssenKrupp(目的楼层派梯的目标函数)