博客

新用户连上仓库要等一两分钟,我们压到 10 秒:长轮询,没加一台机器

有个用户在环境已经准备好之后走了,因为页面还没告诉他。我们改用长轮询消掉排队,再抠掉几处准备时间:以前要等一两分钟,现在测试环境上 10 秒左右,机器一台没加。

本页目录 · 11 节
  1. 先拆开看,时间到底花在哪
  2. 新用户排在一个刷新任务后面
  3. 从「问得更勤」到长轮询
  4. 给开通脚本加一个开关,不再新写程序
  5. 审查早就提醒过的 bug
  6. 剩下的时间也一点点抠出来
  7. 等的那十几秒,给人看什么
  8. 结果
  9. 活是谁干的
  10. 如果你的用户也在等
  11. 相关

有个新用户在 Orbi Cloud 上连好了仓库,页面显示「正在准备工作环境」。他等了一会儿,走了。

他走的时候,环境其实已经准备好了。页面每 10 秒才刷新一次,还没来得及告诉他。

那天夜里我开始查这件事。我做规划用的 AI 助手 Claude Code 告诉我,准备环境「要 5 分钟左右」。我回它:5 分钟,一个操作系统都装好了。

查下来,这一步一般要一分钟左右,慢的时候两分多钟。一夜之后,测试环境上变成了 10 秒左右。改动拆成 31 张票,全是 Orbi 自己写、自己评审、自己合并的(Orbi 是我在做的产品,agent 时代的软件黑灯工厂:从 Issue 到发版,不用人守着)。中间还撞上了一个 bug:审查早就点过名,我们觉得没必要修,当天晚上就撞上了。

都从点「连接仓库」算起,算到环境就绪、能写第一个需求。改前是生产上的真实记录:一般 1 分钟左右,最慢 2 分多钟,页面上只有一句「约几分钟」。改后是测试环境上的实录,按真实秒数播放:约 12 秒。

后面会反复出现两样东西:Cloud 是我们的网站,用户在这里登录、连仓库;主机是我们自己的一台机器,给每个连上的仓库建一个沙箱。为了安全,主机只能往外连,Cloud 没法直接叫它。

先拆开看,时间到底花在哪

我们从数据库里拉出时间戳,把这一分钟拆成几段:

环节 大概要多久 谁说了算
请求排队,等主机来接 平均二十多秒,最慢接近两分钟 主机上每分钟跑一次的定时任务
主机准备沙箱 半分钟到一分钟 开通脚本
页面发现已经好了 最多 10 秒 状态页每 10 秒刷新一次

Claude Code 把两件事混在了一起:准备环境大约一分钟,看到 Orbi 给仓库的建议要三分多钟,「5 分钟」说的其实是后者。这次先压前一件。

那天还定了一条规矩:用户在操作的时间,比如挑仓库,不算等待。要压的,是他什么都做不了、只能盯着屏幕的那段。

新用户排在一个刷新任务后面

主机上有个每分钟跑一次的任务。它先给所有老用户的沙箱刷新一遍凭据(GitHub 的令牌一小时就过期),刷完了,才去看有没有新用户在等。

一轮刷新要一分半钟左右,一轮接着一轮。新用户的请求要是刚好错过了这一轮的检查,就得等这一轮跑完,再等下一轮刷新完,才轮到他。用户越多,刷新越久,新用户等得越久。

改前:一个任务一轮接一轮地跑,每轮先给所有老沙箱刷新约 90 秒,再查新请求、开通。新请求刚好在第 1 轮的检查之后进来,就要干等完第 2 轮的整轮刷新,才会被看到。改后:两条路同时在跑,每分钟一轮照旧管刷新和重试,快路径挂在 Cloud 上长轮询,新请求 1 到 2 秒就被接走。
改前,新用户排在全量刷新后面;改后,新用户走单独的快路径。

最顺手的想法是把每分钟改成每 10 秒。我们差点就这么改了。读了脚本才发现,这么改一点用都没有:刷新本来就一轮接一轮在跑,新用户永远排在刷新后面。要改的是先后顺序。

从「问得更勤」到长轮询

先想到的是把「接新用户」单独拆出来,每 2 秒问一次 Cloud。能用,但还是在轮询。我问了一句:为什么不做成基于消息的,来了就知道?

把请求从 Cloud 送到主机,有三种做法:

做法 怎么做 结论
短轮询,问得更勤 每 2 秒问一次 简单,但总有延迟,没活也一直问
长轮询 主机发一个请求,Cloud 先不回答,有新活了立刻回,20 秒没有就回个空 一秒左右就能接到,主机还是只往外连
推送 Cloud 主动连主机 主机得开一个对外的端口,我们的设计不允许

选了长轮询。GitHub 的自托管执行器也是这么干的:执行器向 GitHub 挂一个 50 秒的长轮询,超时就再挂一个,外面不需要能连进来。我们的主机处境一样:能往外连,外面连不进来。

Cloud 这边加了一个接口:请求最多挂 20 秒,每秒看一次有没有新用户。它只接新来的请求,失败了要重试的老请求还走原来每分钟那一轮,不然一次小故障就会让同一个请求几秒内把重试机会用光。这一条是动手写代码之前,独立审查查出来的。

给开通脚本加一个开关,不再新写程序

第一版设计是在主机上新写一个常驻的 Python 程序,循环挂长轮询、接请求、再去调开通脚本。部署脚本得学会第一次启用它、代码一更新就重启它,它还要配一套自己的测试。

Orbi 已经按这一版开工了,我问:能不能再薄一点?

回头看现有的开通脚本,发现根本不用新程序。它可以同时跑两份;刷新是一整段,可以整段跳过;新接口返回的东西和原来的清单一模一样。于是只给脚本加了一个开关 --claim-only:跳过刷新,改去挂长轮询,接到请求后,开通的代码全部照旧。跑一轮就退出。

循环交给 systemd。完整的服务文件就这么几行:

[Unit]
Description=Orbi Cloud provisioning fast claim (long-poll)
# 这个服务本来就是要不停重启的,关掉默认的启动限流
# (10 秒内启动 5 次就停掉),否则 Cloud 一出错它就被停了
StartLimitIntervalSec=0
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
EnvironmentFile=/etc/orbi-cloud/provisioner.env
ExecStart=/opt/orbi-cloud/host-provisioner/provisioner.sh --claim-only
Restart=always
RestartSec=1

[Install]
WantedBy=multi-user.target

脚本每跑完一轮就退出,systemd 隔 1 秒再把它拉起来。每一轮都从磁盘重新读脚本,所以发新版本时不用写任何「重启服务」的逻辑,下一轮自然就是新代码。前提是部署时先写临时文件、再改名换上去,别直接覆盖一个正在跑的脚本。

主机上 systemd 以 Restart=always、RestartSec=1 运行 provisioner.sh --claim-only。每一轮向 Cloud 发请求,Cloud 每秒看一次、最多挂 20 秒、只交出新请求;主机接下请求、准备好环境后退出,1 秒后 systemd 开始下一轮。每分钟一轮的任务照旧刷新凭据、处理重试。
快路径挂在 Cloud 上等;每分钟那一轮继续管刷新和重试。

第一版那个新程序、它的重启逻辑和那套单独的测试,最后都没写。

审查早就提醒过的 bug

接新用户从每分钟那一轮里拆出来以后,就有两样东西同时在跑。审查在设计阶段就指出了两处会撞车的地方。

一处我们修了。另一处是这样的:每个用户只有一把模型密钥,签一把新的,旧的就作废。接新用户时会签一把交给开通脚本,每分钟那一轮也会给每个用户各签一把。两边要是撞在一起,开通脚本手里的密钥刚拿到就作废了,新用户第一次连仓库就看到「工作环境准备失败」。

审查给的修法很简单:刷新时跳过正在开通的用户。Claude Code 觉得现在接单只要一秒,撞不上,就没写进票里。我也没拦。

那天晚上,长轮询在测试环境上线。前四轮新用户测试都过了,第五轮就撞上了,一模一样。自动重试一分多钟后救了回来。我们开了最高优先级的票,Orbi 半小时后合并了修复。

记下的规矩:审查点名的竞态,要么把修法写进票里,要么写一个测试证明它撞不上。「现在应该够快了」只是猜。

剩下的时间也一点点抠出来

长轮询消掉了排队,之后每一轮,请求一两秒就被接走。同一批票还抠掉了准备环境本身的时间:Orbi 的程序从主机上备好的目录直接复制,不再每次下载;用户的仓库在沙箱还在建的时候就开始下载;页面每 2 秒原地更新,不再每 10 秒整页刷新。

下面是测试环境上的一轮,从点「连接仓库」开始,第 12 秒环境就绪,第 26 秒出现「Orbi 看懂了你的仓库」:

改前:生产环境请求平均排队 22 秒,准备平均 37 秒,页面最多再过 10 秒才发现;有一次是 79 秒,上线前最后一次是 139 秒。改后(测试环境,从点击算):第 4 秒建出请求,第 5 秒被接走,第 12 秒环境就绪,第 14.4 秒页面不再显示「准备中」。
改前是生产环境上的平均值和两次真实连接;改后是测试环境上的一轮。

等的那十几秒,给人看什么

十几秒还是比人能专注的 10 秒长一点(Nielsen Norman Group 的经典说法)。所以页面上不放转圈,放真实的进度:建好独立的工作环境、拉取你的代码、安装 Orbi、开始读你的仓库,每完成一步打一个勾,写上用了几秒。

这么做有研究撑着。哈佛商学院 2011 年有个 Labor Illusion 实验:两个服务给的结果一模一样,一个立刻出结果,一个要等,但一直把自己在干什么展示给你看。等 60 秒的那个,有 63% 的人选它;如果只让人干等,选它的只剩 23%。

同一篇论文还有一个反面:过程展示得很卖力,结果却很差,评价最低。我们正好踩过:中文页面上,「Orbi 看懂了你的仓库」那段摘要是英文写的。现在 Orbi 会按页面语言来写。

结果

以前 现在(测试环境)
请求排队等主机来接 平均二十多秒,最慢接近两分钟 一两秒
环境准备好 一般一分钟左右,最慢两分多钟 10 秒左右
看到「Orbi 看懂了你的仓库」 没有这一步 25 秒左右
看到 3 条建议 3 分多钟 2 分钟左右

新代码已经上了生产,生产上的数字还要等真实用户来了再看。

活是谁干的

我是 Lawrence Liu,Orbi 的创始人。Orbi 接一张打了 ai-ready 标签的 GitHub Issue,用它的编程 agent Pi 写代码,另开一个会话评审,合并,再发版。这次的 31 张票都是这么走完的。Claude Code 和我一起写票,每个改动合并前它都读一遍。

我做的事情就三件:判断什么重要;问了两个问题,「为什么不基于消息?」和「能不能再薄一点?」;还有,放过了那个 bug。

如果你的用户也在等

  1. 先把等待拆成几段,搞清楚每段归谁管,再动手。
  2. 用户在操作的时间不算等待,只压他只能干看的时间。
  3. 想调快间隔之前,先看看前面挡着它的是什么。
  4. 机器不能被外面连进来,就用长轮询。快路径只接新活,重试留给慢路径。
  5. 让 systemd 当循环:Restart=always、RestartSec=1、StartLimitIntervalSec=0,发版时用改名替换文件。
  6. 审查点名的竞态,没有测试证明之前,都当成一定会发生。
  7. 等的时候给人看真实进度;结果看起来不对,先修好再放上屏幕。

相关

继续阅读:Orbi 怎么把一张 Issue 做到发版、Orbi Cloud 里怎么写需求、和 Claude Code 的对比,以及 Cloud。

每周一个真实交付

每周一封,讲一次真实交付。随时退订。