8.2 KiB
Agent Note: 基于实证选用 GitHub 托管大型运行器
Status: implemented
English | 中文
问题
高度分片的 CI 拓扑通过把主 Node 工作分散到 40 个 Linux 作业、把 Windows 工作分散到 9 个作业来达到延迟目标。大多数门禁本身的耗时短于代码检出、运行器设置、缓存恢复和依赖安装这些准备阶段,因此反复执行多轮设置既增加成本,也带来延迟波动。一次托管运行中最慢的 Linux 作业用时 49 秒,而一个 Windows lint 分片却耗时 231 秒,其中仅代码检出、缓存恢复和安装就占了 158 秒。
大型运行器可以让 CI 只承担一次设置开销,再由仓库调度器在内部并行执行,但无法仅凭核心数选出有实际价值的规格。关键通道基准测试的性能提升不呈单调变化,完整仓库聚合流程暴露出的瓶颈也不同于单独运行类型检查或网站构建时的瓶颈。
决策
组织在仅限本仓库使用的 dsh-larger-ci 运行器组中保留 12 个 x64 大型运行器池:Ubuntu 24.04 和 Windows 2025 各设 4、8、16、32、64、96 核规格。公网 IP 已禁用。每个池的自动扩缩容上限为 256;该上限既不会分配闲置机器,也不能免除限制工作流需求的必要性。
生产 CI 使用 3 个大型运行器作业,并让 Node 兼容性、Python 和最终聚合作业继续使用标准运行器:
node 24 / complete使用 96 核 Linux 池。只需执行一次代码检出、设置、缓存恢复和安装,即可供完整且未分片的 40 项主门禁清单使用。run-gates最多同时启动 32 项相互独立的门禁,ESLint 使用 32 个工作线程,快照最多使用 32 个子进程,覆盖率运行使用 16 个 fork。构建与类型检查同时启动;快照和产物消费方仍会等待生成的输出。覆盖率运行的 fork 数保持低于 32,因为将其设为 32 曾两次导致 Node 24 的 CJS 词法分析器终止一个 Vitest 工作进程,使覆盖率结果失效。windows / blocking builds使用 16 核 Windows 池。一次设置完成后,构建任务与生产版 VitePress 网站任务并发运行。windows node 24 / observational使用 32 核 Windows 池。其完整且未分片的 37 项静态、lint 和产物门禁清单使用 32 个外层调度器槽位运行,并保持非阻塞。ESLint 本身仍采用单线程,因为启用 16 个 ESLint 工作线程会把完整 lint 的耗时增加至 174.54 秒;在启用外层并发且不使用 ESLint 工作线程时,同一项完整 lint 只需 31.67 秒。
两个 Windows 作业有意使用不同的运行器池。首个候选方案让二者都使用 16 核池;尽管已配置自动扩缩容上限,GitHub 仍花费 93 秒才预配好第二台同标签运行器。分别使用 16 核池和 32 核池后,最终验证运行中的每个生产作业都在 2 秒内开始运行。
工作流保留 4 项手动诊断。suite=larger-runner-benchmark 比较所有规格下相互独立的关键通道,suite=consolidated-runner-benchmark 比较完整聚合流程,suite=sharded-reference 保留原生产分片拓扑,suite=serial-reference 则继续作为未分片的跨平台完整性判定基准。当拉取请求无法生成合并提交时,suite=optimized-larger-runners 会直接针对分支引用运行与生产环境完全相同的拓扑。
首次涵盖 12 种规格的关键通道基准测试以标准运行器基线为基础,只叠加了一个仅修改工作流的提交,因此代码、锁文件和命令完全相同:
| 关键作业 | 标准 | 4 核 | 8 核 | 16 核 | 32 核 | 64 核 | 96 核 |
|---|---|---|---|---|---|---|---|
| Linux 类型检查 | 56 秒 | 38 秒 | 35 秒 | 40 秒 | 35 秒 | 44 秒 | 40 秒 |
| Windows 生产网站 | 160 秒 | 117 秒 | 103 秒 | 113 秒 | 75 秒 | 105 秒 | 108 秒 |
这些单项结果表明设置开销占主导地位,却无法确定生产环境应选用的规格。一项未启用 ESLint 原生并发的完整聚合基准测试发现,Linux 单线程 lint 门禁耗时 69 秒。启用 Linux ESLint 原生并发后,第二次完整聚合基准测试得到了以下作业活动耗时:
| 聚合作业 | 4 核 | 8 核 | 16 核 | 32 核 | 64 核 | 96 核 |
|---|---|---|---|---|---|---|
| Linux 完整主流程 | 147 秒 | 104 秒 | 95 秒 | 57 秒时失败 | 66 秒 | 60 秒 |
| Windows 阻塞性构建 | 137 秒 | 127 秒 | 113 秒 | 107 秒 | 105 秒 | 131 秒 |
Linux 32 核作业的失败是首次发生的 CJS 词法分析器工作进程崩溃。在所有规格的结果中,96 核聚合作业是唯一成功达到 1 分钟边界的结果。Windows 超过 16 核后的收益很小,因此阻塞性作业使用 16 核;观测作业则使用单独的 32 核池,以避免同标签运行器的预配延迟,并让全部外层门禁同时启动。
生产环境的精确验证运行在受测分支头通过了所有作业:
| 生产作业 | 活动耗时 | 仓库工作 | 结果 |
|---|---|---|---|
| Linux 完整主流程 | 50 秒 | 40 项门禁耗时 23.23 秒 | 通过 |
| 最慢的标准非 Windows 作业 | 40 秒 | Node 26 兼容性 | 通过 |
| Windows 阻塞性构建 | 91 秒 | 2 项门禁耗时 28.69 秒 | 通过 |
| Windows 观测作业 | 153 秒 | 37 项门禁耗时 31.83 秒 | 通过 |
Windows 观测作业花费 57 秒恢复 pnpm 缓存,因此其剩余余量既反映托管环境的设置波动,也反映仓库工作耗时。最终运行中每个非 Windows 作业仍低于 1 分钟,两个 Windows 作业也都低于 3 分钟。
曾考虑的替代方案
在生产环境中保留原分片拓扑。 各分片在一同完成预配时可以很快,但 49 个大型运行器作业会重复设置,也增加了出现冷启动异常值的机会。耗时 231 秒的 Windows 对照结果表明,短小的 lint 分片并不能保障端到端作业达到时长目标。
根据关键通道基准测试选择生产规格。 对单独的类型检查和网站构建而言,4 核看起来具备成本效益,但完整聚合流程发现了这些命令未覆盖的全仓库 lint 和存在依赖关系的产物工作。
在启动 Linux 聚合流程前预先构建。 此方案让构建成为设置路径的一部分,并产生了一个耗时 66 秒的候选结果。在 run-gates 内尽早启动构建,既能保留产物依赖关系,又能让构建与无关检查重叠执行;最终聚合流程耗时 23.23 秒。
在 Windows 上使用 ESLint 原生工作线程并发。 16 个工作线程让 lint 比最终的单线程结果慢 5 倍以上。外层门禁并发能够利用 32 核运行器,同时不会成倍增加 ESLint 在 Windows 上启动工作线程和加载 TypeScript 项目的开销。
将兼容性、Python 和聚合作业迁移到大型运行器。 这些标准运行器作业都在 40 秒以内完成。付费容量不会缩短关键路径。
后果
最终生产验证产生的计费时长为:96 核 Linux 1 分钟、16 核 Windows 2 分钟和 32 核 Windows 3 分钟。按已配置的大型运行器费率计算,其大型运行器成本为 $0.902。全规格关键通道基准测试的成本为 $2.936。GitHub 会把每个大型运行器作业向上取整到整分钟计费,因此把付费作业数从 49 个减少到 3 个,与缩短仓库工作耗时同样重要。
现有的零美元 Actions 预算并未阻止大型运行器作业。仅限本仓库的运行器组、有界的工作流拓扑、手动基准测试触发和作业超时限制才是经实测有效的成本控制机制;该预算不被视为执行防护措施。
生产 CI 依赖本 Agent Note 和 .github/workflows/ci.yml 中由组织持有的运行器名称。池缺失或改名会让作业一直排队,不会回退到标准容量。手动全规格套件和原分片套件均予以保留,以便在映像、依赖、调度器或定价发生变化后重新测量,再调整生产标签。