每月一千次构建,而且一次都不花钱 ⚙️
先更正一件事。这篇文章的第一版声称,我们的 GitHub 构建分钟数用完了,被迫升级到 Pro 账号。这是错的,而且只要看一眼文档就能弄清楚:对公开仓库来说,GitHub 的构建分钟数是免费的。产物和缓存也一样。我们的仓库从五月底起就是公开的,所以我们从来没有为任何一分钟付过钱,以后也不会。与其悄悄改掉文字,我们宁愿重写一遍,告诉你们测量到底显示了什么。结果比错误的版本有趣得多。
先猜测,后查看。起因是一个晚上,一个写好的拉取请求摆在浏览器里,却没有触发任何检查。没有绿色对勾,只有沉默。我们想到了最显而易见的解释:“额度用完了”,于是开了一个 Pro 账号。之后一切又跑起来了,似乎证实了这个理论。其实并没有:沉默另有原因(GitHub 在那一刻没法和主分支干净地合并的拉取请求,不会启动任何检查)。后来我们查询了每一次运行的计费数据。对每一次运行、每一种操作系统,答案都是:计费时间 = 0 毫秒。用订阅来支持一个平台完全没问题,但作为对这个仓库的解释,它站不住脚。
真实的是:这套机器变大了。每个拉取请求都会启动一个小工厂:整个服务器被构建并运行它的测试套件,代码检查和代码分析把它过一遍,CodeQL 搜寻安全漏洞,还会生成游戏服务器、门户、WorldHost 和 AI 后端的 Docker 镜像。一次发布还会加上客户端的 Windows、Linux 和 macOS 版本,以及给浏览器游戏用的 WebGL 版本。从五月底到现在,这加起来是 1,723 次构建运行,仅七月就有 1,092 次,四周里合并了 131 个拉取请求。没错,最近的激增确实和 AI 有关 😉:这个项目是 100% 在 AI 协助下开发的,订阅升级后,同时发生的事情就是更多。只不过这里的货币不是钱,而是等待时间。
等待时间花在哪里。八天内的测量结果:普通的检查运行(CI)379 分钟,CodeQL 安全分析 230 分钟,发布构建 126 分钟,剩下的是零头。一次发布大约要花 110 分钟的计算时间,分布在 WebGL(31.6)、Docker 镜像(19.4)、macOS(13.7)、Linux(13.7)、大型测试关卡(12.1)、Windows(10.9)和打包(7.2)上。这就是“做完了”到“你能安装了”之间的时间。
现在说说真正稀缺的东西:缓存。为了让构建不用每次都从零开始,GitHub 会保存中间结果,而这方面每个仓库只有 10 GB,不管你有多公开。我们的目前有 5.3 GB,共 63 个条目。问题出在分布上:大约三分之二是 Docker 的中间层,每次运行都会重新上传,它们挤掉了本来最有帮助的东西。这才是真正的瓶颈,世界上没有哪种订阅能帮你绕过去。
最棒的发现是一个真正的 bug。构建时,Unity 会创建一个巨大的中间文件夹(Library),重复利用它能省下好几分钟。测量发现:对 Windows、Linux 和 macOS 来说,这个文件夹从来没有被保存过,构建任务老老实实地尝试恢复它,可从来没有人写入过它。而且 Windows 构建是唯一一个缓存键里没有平台标记的。结果是:它下载了 1.7 GB 的 WebGL 中间状态,Unity 又尽职地把它们全扔掉,再重新导入一切。日志里白纸黑字:资源导入花了 257 秒,而缓存命中时是 85 秒。每次发布白白浪费近九分钟,只因为键里少了一个词。
还发现了什么。我们的测试套件在每个发出去的提交上会运行三次(在拉取请求里、合并后,以及发布时再跑一次)。CodeQL 在每个拉取请求上检查三种语言,尽管它甚至不是必需的检查。.NET 软件包的缓存完全没有。明明只为一种处理器架构构建,却还设置了模拟其他处理器架构的模拟器。还有 7.4 GB 的旧构建产物按默认的 90 天保留期堆在那里。没有哪一项很严重,加在一起却是相当可观的一块生命。
教训和我们的服务器当年教给我们的一样。当时我们测量发现,被占用的内存有一半根本不是给游戏用的,而是给了 Docker 和监控。这次更尴尬:我们假设了一张根本不存在的账单,还没查看就付了钱。测量,不要猜测,这句话看来要到你真正把它内化了才算数。清理工作已经计划好了,但还没做;等做完了,我们会告诉你们换来了什么。🚀
留言