从一个循环到十九条消息:先测清 Cloudflare Queues 怎么扩,再把检查放上去
2026-10-07 之前,所有检查都由一次 Cloudflare cron 调用完成,每五分钟一次。它领取到期的 server,一台接一台地检查,一轮通常是 21 台。从第一个任务到最后一个检查结束,一轮的中位数是 116 秒,每二十轮里有一轮超过 200 秒。下一轮在这一轮开始五分钟后就会开始,而这些工作已经占掉了其中的大半。
从 2026-10-07 22:13 UTC 起,每一轮分到 20 个调用上。cron 调用往队列里发 19 条消息,每条消息启动一个独立的调用,最多领取 5 台 server;每个调用同时检查最多 3 台。头 72 小时里,从第一个任务到最后一个检查,一轮的中位数是 23 秒,最慢的一轮 42 秒。
这篇讲的是这两段之间的那一步。我们事先不知道 Cloudflare Queues 启动 19 个调用有多快,所以先测了这一点。
文档没有回答的问题
队列只有在消息被同时处理时才有用。Cloudflare 的文档说,Queues 只在一个 consumer 处理完一批之后,才判断要不要增加 consumer,但没有说加得多快。如果 19 条消息是一条接一条处理的,并行版本反而会比它替换掉的循环更慢。
所以我们队列的第一个版本不做任何检查。它的 consumer 只接受一种消息:一条测试消息,按指定次数调用我们签名服务的公钥接口,然后等待。它不领取 server,也不签任何东西。我们在 2026-10-07 11:50 UTC 部署了它,发了两次测试。
第一次是一条消息,要求调用 40 次。40 次全部成功。每次检查都要调用签名服务,设计要求一个调用至少能做 30 次。我们由此知道上限至少是 40,但并不知道上限到底是多少。
第二次是同时发 9 条消息,每条都让 consumer 等待 60 秒。第一条在发出后约 2 秒开始。第二条在第 63 秒才开始,正好是第一条结束之后。之后并发逐渐增加,最高到 5;第九条在发出后第 257 秒结束。总共 9 分钟的等待,用了四分多钟。
用一条曲线拟合的模型
我们用这条曲线拟合了一个简单的模型:先只有一个 consumer;一个结束了,马上开始下一条消息;每次有一个结束,约 12 秒后再允许多一个 consumer 同时运行。拿这次测试回放,模型算出 9 条消息的开始时刻是第 2、62、74、123、135、135、147、183、195 秒,实测是第 2、63、77、123、137、137、147、183、197 秒。模型在第 255 秒结束,测试在第 257 秒结束。
这个模型说明了一件有用的事:并发是随着每个调用结束而增长的,所以调用越短,扩得越快。我们最初的设计是 9 条消息,每条最多 30 台 server。在模型里,这样的调用每个要跑约 90 秒,早早结束的很少,扩容一直很慢;负载再高一些,就会有消息在队列里等太久,超过我们判定过期、直接丢弃的时限。最后上线的设计正好相反:19 条消息,每条最多 5 台 server,同时检查 3 台。在下文的 72 小时里,这样一个调用的中位耗时不到 8 秒。
先验证模型,再相信它
用一条曲线拟合出来的模型,可能只描述得了这一条曲线。新形态的代码动笔之前,我们先写下模型对第二次测试的预测,然后才去跑:40 条消息,每条调用 5 次、等待 20 秒,大致相当于一个领取 5 台 server 的调用的工作量。
| 预测 | 实测 | |
|---|---|---|
| 最后一条开始 | 约 127 秒 | 126.1 秒 |
| 最后一条结束 | 约 148 秒 | 146.1 秒 |
| 最多同时运行的调用 | 约 19 个 | 17 个 |
40 条全部成功。形态站得住,代码就照它写,消息数、每条消息的 server 数、同时检查的台数都做成配置值,而不是写死的常量。
对 server 来说什么没变
并行意味着同一时刻我们发出的检查更多,不意味着每台 server 被检查得更多。每台 server 仍按自己的周期检查,没有维护者认领的 server 每 12 小时一次,给运营方的说明页里就是这样写的。不管同时有多少个调用在领取,数据库里有两条规则保证这一点:有未结束任务的 server 不会被再次领取;任何主机同一时刻最多只有一个任务在进行。在下文的 72 小时里,没有任何主机出现过两个。
打开开关
并行代码上线时是关着的,消息数设为 0,所以每一轮跟以前完全一样。打开开关是另一次部署,只改了两个值:每轮的消息数从 0 改为 19,同时检查的台数从 1 改为 3。
第一次打开是在 2026-10-07 20:53 UTC。它在零点几秒内把四台 server 各检查了两次,原因是两次写入之间有一个空档,只有一个循环时永远撞不上。我们在七分半钟后回滚,修好了它,那件事另有一篇文章。修复后的版本在 22:13 UTC 上线。
72 小时之后
打开开关之前,我们先定好了标准:每个任务都有着落,没有 server 被检查两次,没有主机同时有两个任务,签名 run 的比例不低于之前。从 2026-10-07 22:13 UTC 到 2026-10-10 22:13 UTC:
- 18,446 个任务,18,446 个 run,每个 run 都有它的证明记录。
- 两个任务相隔不到十分钟的 server:0 台。
- 同时有两个任务在进行的主机:0 个。
- 三天里每一天,列出的 server 全部检查到:2,980 台中的 2,980 台。
- 每天的 run 数:6,149、6,149、6,148;之前 24 小时是 6,151。
- 签名 run 的比例:61.8%、61.8%、61.7%;同样那 24 小时是 61.7%。这个比例取决于 server 怎么回答,不取决于我们怎么排程,它也确实没有变。
- 每一轮从第一个任务到最后一个检查结束:中位数 23 秒,最慢 42 秒;单循环时是 116 秒和 214 秒。如果从五分钟整点算起,一轮结束的中位时刻是第 80 秒,单循环时是第 175 秒。
也有两处不完美。18,446 个任务里,有两个没有记下结束时间,不过两者的 run 都是完整的,对应 server 的下一个任务也照常跑了。其中一个所在的调用被 Cloudflare 取消了:这 72 小时里,平台在约 17,500 个调用中取消了 1 个;之前的 72 小时里,在约 1,000 个调用中取消了 13 个。我们从外部看不出调用变短是不是原因,所以只报数字。另一个任务的结束时间为什么没记上,我们还没查清。
我们的收获
最有用的那次测量,不是我们原本计划的那次。我们发 9 条长消息,本来只想知道调用到底能不能并行,结果拿到了一条曲线。曲线给了我们一个模型,模型告诉我们要把调用做短;而第二次测试的预测是跑之前就写下来的,它告诉我们这个模型能不能信。
我们怎么核对的
一轮指的是同一个五分钟区间内请求的全部探测任务。单循环的数字覆盖 2026-10-06 12:00 UTC 到 2026-10-07 12:00 UTC 的 288 轮;并行的数字覆盖 2026-10-07 22:13 UTC 到 2026-10-10 22:13 UTC 的 860 轮。两者每轮任务数的中位数都是 21。一轮的耗时是最后一个任务的结束时间减去第一个任务的请求时间;从整点算起,则是最后一个结束时间减去区间的起点。两种算法的差别在于一轮的第一个任务何时开始:单循环时中位数是整点后 34 秒,并行版本是 56 秒。为什么开始得更晚,我们还没弄清楚。一台 server 的两个任务请求时间相隔不到十分钟,就算被检查了两次。一个主机的两个任务在领取到结束之间有重叠,就算同时有两个任务在进行。签名 run 指证明是签名的、而不是记为未签名的 run。队列测试的数据来自 consumer 自己的日志行;取消次数来自 Cloudflare 对我们探测 Worker 的调用统计。所有数据库数字都是对生产库的只读查询。