AI 编程让 CI 成了瓶颈,于是我们重做了 CI

查看原文 HN 讨论

文章摘要

这篇文章来自项目管理工具 Linear 的工程博客,作者 Mufeez Amjad,发表于 2026 年 9 月 21 日。故事开头很有 Linear 风格:今年早些时候,他打开 Linear,发现 CTO Tuomas 给他分配了一个 issue,标题是”CI 成本太高”,顺便还希望他把 CI 变快。

问题的背景是 agent 写代码太快了。 作者写道,agent 让交付代码的速度呈指数级提升,但验证这些改动的速度没有跟上。每个 PR 都要过 CI,开发越快,CI 越成为瓶颈,既推高基础设施成本,也让开发者和 agent 等反馈的时间越来越长。Linear 优化的目标有两个:一个 PR 要等 CI 多久,以及它消耗多少运行器(runner)时间。结果是,尽管测试套件从年初到现在几乎翻了两番,PR 等待时间反而从 6 分多钟降到 5 分钟出头,每个测试消耗的运行器时间大约减半。作者把改进归为四类:升级基础设施和工具链、优化卡在关键路径上的前置任务、减少重复的环境准备、让测试执行更高效。Linear 的代码库以 TypeScript 为主,但大部分优化适用于任何语言。

第一类:换更快的机器和更快的工具。 最早的一批收益几乎不需要优化 CI 本身。把工作负载从 GitHub Actions 自带的运行器迁到第三方运行器(CPU 更快、存储性能更高、缓存设施更好)后,对比切换前后两天,任务平均快了 34%,tsc 类型检查快了 52%。换用原生 TypeScript 编译器 tsgo 后,tsc 检查的周中位数耗时又降了 73%,类型检查从此不再是瓶颈。lint 也是早期目标:有几条自定义 lint 规则依赖 TypeScript 类型信息,导致每次 lint 都要先构建完整的类型图,成了最吃内存的 CI 任务之一。他们把这些规则改写成基于抽象语法树的静态分析,不再需要类型信息,ESLint 得以完全甩掉 TypeScript,API 部分的 lint 时间减少 68%,全仓库 lint 减少 55%。纯语法的规则也让之后迁移到 Oxlint 变得轻松。

第二类:优化那些”挡在所有人前面”的小任务。 每次 CI 运行都先检查 PR 改了哪些路径、这些测试是否已经在相同输入下通过过。这些检查在任务级别做门控,被跳过的工作就不会占用运行器,但它们也因此直接处在关键路径上:八个 API 测试分片要等它们跑完才能开始,任何一点延迟都会被放大。

第三类:减少重复的环境准备。 启动运行器、安装依赖、准备构建环境,这类开销意味着一个只干几秒活的任务可能要消耗几分钟的基础设施时间。API 测试分片每次都花 7 到 8 秒用 apt 安装同一个 Postgres 客户端,于是他们做了一个包含 Node 和该客户端的小型 CI 基础镜像,后来又把原生构建头文件也放了进去(在准备阶段下载它们偶尔会卡住)。Linear 是 pnpm workspace 管理的 monorepo,API 测试原本安装整个 workspace,改成只安装 API 包及其依赖后,pnpm install 从 44 到 73 秒降到 16 到 18 秒。他们还测试了缓存 node_modules,发现重新安装反而更快:缓存键依赖一个频繁变动的 lockfile,即使命中缓存,恢复也要 28 秒左右,而过滤后的安装只要 7.5 秒左右。这三项加起来,每个分片的准备时间减少约 44%,从 110 到 140 秒降到 67 到 73 秒。此外,API 容器原本每次都重放完整的数据库迁移历史,现在在 PR 没改 schema 时改为加载生成好的 schema 快照和引导文件,每个容器的数据库准备从约 12 秒降到 1 到 2 秒。另有七个独立的小检查,各自启动运行器、检出仓库、安装依赖,然后只干几秒活;把它们合并成两个任务、在任务内部并发执行后,按六月的用量计算,每月省下约 8.7 万运行器分钟,相当于 CI 总用量的 11.8%。

第四类:让测试执行更高效。 Vitest 按文件而不是按单个测试的时长分配工作,几个特别大的测试文件会拖住某个分片,进而拖住整个套件。他们把大文件拆成更小、更聚焦的文件,年初已经从 3 个分片加到 4 个,现在加到 8 个:初步基准测试显示关键任务快了约 19%,成本也低了约 19%;改动一周后,最慢分片从 5.25 分钟降到 4.33 分钟。

最大的单项收益,也是风险最高的一项:共享模块状态。 Vitest 默认隔离每个测试文件,对 Linear 来说意味着每个分片都要重建一遍实体、GraphQL 和装饰器组成的依赖图。他们引入了一个需要主动选择加入的 vitest 项目,设置 isolate: false,让安全的文件在同一个 worker 里共享模块注册表。这带来了约 17% 的月度成本节省,最慢分片从 300 到 379 秒降到约 195 秒,每次运行的 API 分片运行器总时间从约 32.8 分钟降到 22 分钟。为了控制正确性风险,每个文件都要显式写一行注释声明加入,共享状态都补上了清理逻辑;少数使用假计时器或以难以理清的方式共享状态的文件被留在了隔离项目里。有意思的是,因为现在大部分测试都是 agent 写的,他们还更新了相应的 agent skill,让生成的测试默认遵守这些约束。

分片数受制于准备开销。 作者指出,只有每个分片的固定开销足够低,继续加分片才划算,因为分片数翻倍,花在准备上的总时间也翻倍。如果每个分片还要准备 110 到 140 秒,8 个分片光准备就要消耗 15 到 19 分钟运行器时间,比测试本身还多。现在准备时间约 40 秒,8 个分片的准备总时间(7.5 分钟)反而比之前 4 个分片(8.3 分钟)还少,测试并行度却翻了一倍。文章结尾说,如果今年没有专门优化 CI,现在的测试套件要跑约 11 分钟,接近现在的两倍;而他们目前每周新增约 2,000 个测试,保持 CI 快速会是一项持续的工作。

HN 评论精华

407 条评论,但讨论和标题只有一半相关。整个帖子超过六成的评论(约 250 条)集中在一个离题的子串里: torben-friis 问,既然大家都快到处处撞墙,为什么我们没看到产品变好?这个问题演变成了一场关于 AI 生产力是否真实存在的大辩论。真正讨论 CI 技术的评论相对分散,主要围绕 Bazel、自托管运行器,以及”AI 写的测试到底有没有用”。rrvsh 自己也察觉到了:”感觉大家都盯着 AI 瓶颈这个钩子,而不是文章真正讲的内容。我很喜欢这篇,把 CI 优化到超出合理程度是我暗暗最喜欢的事。”

离题主线:产品在哪里? torben-friis 的原话是:”每个帖子都像 90 年代的华尔街办公室,可新款安卓和 iPhone 的新功能比往年还少。没有独立开发者搞出一个 Linux 规模的替代操作系统。Switch 2 依然没被破解。Windows 右键菜单要 3 秒才弹出来。是不是大家都在原地全速兜圈子?”(TonyStr 试了一下:”哇,还真是。我以为你在夸张。”)

saithound 把离题讨论拉回了文章本身,而且火力很猛。 他认为 Linear 这篇文章本身就说明了问题:多处改动改变了 CI 的行为,却没有给出相应的质量度量。把依赖类型信息的自定义 lint 规则换成纯 AST 分析,没说新规则能检测什么,没做新旧规则对比;把 tsc 换成 tsgo,只骄傲地报了性能提升,却不关心诊断结果是否等价;甚至没报告新旧 CI 的通过/失败一致率。”他们的测试多了 4 倍,但完全不知道这个大测试套件是否比原来的小套件更有效,甚至不知道它到底有没有用。”

“每周新增 2000 个测试”引发了对 AI 测试的集体吐槽。 plant-ian 惊讶地问:”他们真的每周往代码库里加 2000 个测试?”maccard 说文章里写了大部分测试是 agent 写的,所以大概是真的。Rapzid 说这正是很少有人提到的问题:除非你专门提示,模型会写”多得离谱的无用测试,而且用最低效的方式”,测试数和代码行数膨胀到疯狂的程度,最终可能让 AI 自己的上下文都装不下整个代码库。Aeolun 说 Codex 特别喜欢检查 UI 和测试里的字符串是否相等;Rapzid 补充它还爱测试外部库;plagiarist 说他的 agent “犯傻的日子里”还会测试语言特性本身,比如验证回调函数会执行回调里的代码。dgroshev 问:”如果你还在审 PR:你上一次没有直接跳过测试是什么时候?”MikeNotThePope 说他的应用里大概一半生成的测试是在测 markdown 文件。saagarjha 只问了一句:”测试多了四倍,它们产生了四倍的价值吗?”不过也有相反的声音:sz4kerto 说现在审 PR 恰恰只看测试,”如果测试相关并且通过了,代码大概率没问题”,大量的端到端和特征测试正是 agent 编程的主要支撑,有了它们才敢让 AI 做大规模重构。yieldcrv 则认为单元测试大多只是装饰,agent 和人类都只是在应用改了之后顺手改测试,端到端测试才真正有用。

Bazel 的门槛变低了。 azkalam 说只要能承受初始成本,Bazel 在热缓存下能让大型项目构建时间降到 10 秒左右。criemen 分享了一个很有说服力的对比:几年前他主导了一个五种以上语言、面向三大操作系统发布原生二进制的复杂项目的 Bazel 迁移,花了好几年;最近一个单语言项目,借助 agent 两周就完成了,”Bazel 的初始成本刚刚大幅下降,我觉得整个行业还没意识到这一点”。teaearlgraycold 接着说,整个行业,包括 LLM 训练所用的那些产出,都还没重新考虑什么是容易、什么是困难:”LLM 总是以’要花一个周末’为由劝你别改代码。不,Claude。你来干,20 分钟就搞定。”但 manquer 提醒说,他用过的 nx 和 turbo 瓶颈通常是网络和磁盘 IOPS 而不是计算,在 TypeScript 生态里一个像样的大型 monorepo 能做到一两分钟就谢天谢地了;seniorsassycat 说他做过实验,把一个包迁到 Bazel 后回放一周的真实改动,只省了 20%,远没有全量热构建那种标题数字好看。BuildBuddy 的 sluongng 介绍了 Bazel 生态的应对办法:中间产物只拉取摘要来推算默克尔树,大的最终产物用内容定义分块只拉缺失的块。Dagger 创始人 shykes 则认为要真正解决问题,就不能再把”构建”和”测试”当成两类分开设计和扩展的工作,并反对把整个 CI 绑定在任何单一构建工具上,”哪怕是 Bazel”。

自托管运行器派。 classictraffic 对 Linear 离开 GitHub Actions 毫不意外,认为 Actions 方便但慢,而且可靠性越来越成问题。wereHamster 不明白为什么大公司不自托管:买一台 TB 级内存、256 核、高速 NVMe 的机器,测试比任何托管平台都快,还能共享页缓存、保留工作目录,”我的 pnpm install 耗时 0 秒,因为 node_modules 本来就在”。oblio 的回答一针见血:”因为他们正在逃离本地机房和相应的运维团队。”zackify 说他们有数千个测试,2 分钟跑完,每当超过 3 分钟就加并行,做法是在 OVH 上跑一批常驻的 lxd 容器,开始时用 1 到 2 秒替换一次数据库 SQL 转储。cyberax 说他去年把 CI/CD 压到 60 秒以内,靠的是自托管运行器、用 Podman 构建镜像,以及把 Docker 镜像当作内容寻址缓存。gabriel-uribe 把 Actions 迁到了家里一台闲置的 M2 MacBook Air 上。rurban 则用形式化验证替换了一批慢速单元测试。deaux 认为 99.9% 的 Web 应用的 CI 需求用一台每月 10 美元的机器就能满足。

瓶颈只会转移。 aliclark 说他那里的瓶颈不是 CI,而是人工测试:它能跑是一回事,是否真的是客户想要、能理解、会喜欢的,是另一回事。shykes 认为这是分层问题,底层 CI 跟不上,上层只会更慢;Sharlin 反驳”如果 X 阶段是瓶颈,加速 Y 阶段毫无意义”,vips7L 只留了一句:阿姆达尔定律(Amdahl’s law)。关于 QA 能否自动化,bluGill 认为不能:”QA 是为了找出你没想到、所以没写测试的东西。”他说自己早年做过 QA,结论是任何人都没法测试自己的代码,盲点太多。shykes 说他也是 QA 出身,当年 99% 的工作今天 AI 都能做,他现在把时间都花在剩下的 1% 上。Sharlin 讽刺说,那就用 agent 测试员替换人类测试员,等人类用户跟不上迭代速度时,再把人类用户也换成 agent 用户;dwaltrip 接话”顺便把顾客也换成 agent 消费者”;oblio 贴出了那段著名的对话:亨利·福特二世问工会领袖沃尔特·鲁瑟”你打算怎么让这些机器人交会费?”,鲁瑟反问”那你打算怎么让它们买你的车?”。LastTrain 说下一个瓶颈是客户:”他们最好人数够多、钱够多,而且愿意为每一个该死的版本安装更新。”

其他有意思的片段。 d_sc 建议 Vitest 按测试时长而不是按文件做负载均衡,疑似作者本人的 moofeez 回复说他们试过,收益不足以抵消额外的复杂度。frangonf 酸了一句:Linear 是到了 1 亿美元 ARR、10 亿美元估值之后才开始操心这些事;以 Linear 团队口吻回应的 jorde 说,他们一直在改进工具,只是 agent 编程让代码产出大增、CI 压力更大,”和估值没关系,我们的工程团队规模也还没有明显扩大”。algesten 调侃”还好 Linear 早就做完了,不需要新功能……糟糕,它正在变成下一个 Jira”;reticulates 说 Linear 没有滑向 Jira,但”代码变便宜后,它失去了那种精心打磨的产品设计,失去了灵魂”;heeton 则说他的团队用了多年 Linear,过去半年它的更新密度明显提高,”对我们的用处是去年的两倍”。prplfsh 问有没有人让开发者在本地跑 CI、再推送签名的产物,jackothy 贴出了 DHH 那篇《我们要把持续集成搬回开发者的机器上》,Rapzid 提醒开发机不应成为产物供应链的一环,有安全风险。nicwolff 挑了文章的用词:”我们在任务级别做门控(gate)”……”这个任务在分片完成后运行,但不门控任何东西”,他问:”是有什么海量训练语料让 Claude 以为人们常用’gate’来表示’阻塞’吗?”言下之意是文章有 AI 代笔的痕迹。eriwang915 留言”加速 CI 只会把瓶颈转移到审查和部署。agent 让队列变得更吵,但队列不是它们发明的”,aldos 回了一句”行吧,铁皮人”(clanker,网络上对 AI 和机器人的蔑称),暗讽这条评论本身就像 AI 写的。评论区还有一场小型口水战:xracy 抱怨文章没有展开”CI”这个缩写,自称做了近十年 SRE 的他因此被一群人围攻。