在 2000 多个 Discord 服务器上运行机器人教会了我们什么
四件在规模变大后,与你写代码时的设想截然不同的事。

一个在三个服务器上运行的 Discord 机器人,和同一个运行在两千多个服务器上的机器人,并不是同一个程序的放大版。有几条在小规模时完全无害的假设,过了一定规模后,恰恰会变成最难找的 bug。
下面是四条让我们真正出过故障的假设。
1. 故障不会均匀分布,它们会藏起来
AlphaCloud 分布在多个进程中运行,这些进程叫作分片。Discord 决定哪个服务器落在哪个分片上,而每个分片只知道自己的服务器。
这会产生一类从外面看像是随机现象的故障:欢迎消息在一些服务器上正常,在另一些服务器上却不行。日志里没有错误,没有警报,什么都没有。原因是一个没有超时的网络调用。如果对方接受了连接然后就不再响应,代码就会在进程剩余的整个生命周期里一直等下去。之后的一切都不会发生,而且因为没有抛出错误,也就没有任何东西报告错误。
这个教训平淡无奇,却普遍适用:每个对外调用都需要一个上限。一个永远等待的调用比一个失败的调用更糟,因为失败至少是可见的。
2. 重启不是修复,而是一个期限
在我们还不知道原因的时候,重启受影响的分片总是有效。这正是重启的问题所在:它有效,于是寻找原因的压力就消失了。
现在我们把“重启有用吗?”当作诊断,而不是疗法。如果有用,说明出问题的状态存在于内存中,代码里有某个地方在构建它,却从不清理它。
反过来也成立:任何必须在重启后保留的东西都应该放进数据库。如果一个多步骤操作正在运行,比如对成千上万名成员进行身份组变更,而进程在中途挂掉,这个操作不能就这样消失。它必须在下次启动时被检测到并继续执行。
3. Discord 的限制是按机器人算的,而不是按进程
Discord 限制了一个机器人每秒可以发出多少请求。显而易见的假设:进程越多,吞吐量越大。错了。限制作用于机器人,而不是进程。三个分片各自拥有完整的配额,加起来就是允许量的三倍,于是 Discord 会对三个分片都进行限流。
所以我们的每个进程都会计算总共有多少个进程,只取属于自己的那一份。这听起来显而易见,但当时并不是,而且这个 bug 很长时间都没被发现,因为每个进程单独看都表现得完全规矩。
一个相关的陷阱:周期性任务按固定间隔运行,比如每五分钟一次。如果所有分片同时启动,它们就会永远在同一时刻触发任务。从那以后,每个进程在启动时都会得到一个随机偏移。
4. 人们实际配置的东西和你预想的不一样
技术只是一半。另一半是:对大多数服务器来说,默认值就是设置。一个默认值有问题的模块,在绝大多数服务器上都是配置错误的,无论设置页面做得多好。
对我们来说,由此得出两点。第一,每个默认值都比设置本身需要更多思考。第二,对一个新模块来说,更好的问题不是“这里能配置什么”,而是“当没有人配置任何东西时,这个模块会做什么,这对一个普通服务器来说是否正确”。
这也是一个乍听之下似乎相反的决定背后的原因:从 v1.8.0 起,机器人发送的几乎每条消息都可以自由设计。但必需组件,比如警告私信中的原因或工单面板中的分类菜单,可以移动但不能删除。设计自由,可以,但不能自由到让服务器不小心做出一个看起来能用、实际上不能用的东西。
共同的主线
四条中有三条是同一个想法换了不同的装扮:在这种规模下,静默的故障比大声的故障代价更高。崩溃会被报告并修复。一个干脆停止工作的功能可能会保持那样好几个月,而你要等到某个愿意费心写报告的人提交 bug 报告时才会发现。
所以对于新代码,我们现在首先问的问题不是“它能用吗”,而是“它不能用时是什么样子,我们会注意到吗”。