深度解析塑造未来的技术文章。

Git 单体仓库失效之时:每天 3 万次合并的五个教训

Git 单体仓库在极高合并频率下会以可预见的方式失效。本文梳理始终在线的客户端与服务端提交的关键经验。

插图:机器人快递员挤在巨大档案库的文件箱之间的结账台前。
Git 默认假设每个客户端都持有完整的档案库。这个假设没问题,直到每秒有成千上万个客户端排队。

如果你在运营一个 git 单体仓库,迟早会遇到某个临界点,系统在那里彻底崩溃。对 Anthropic 来说,这个数字大约是每天 3 万次合并、主分支 100 GB 的历史,以及每天约 2500 万次 CI 检出。如果按最朴素的方式拉取,每天的 git 流量超过 600 PB。他们的解决办法是用五个月时间自己写一套版本控制系统。你不该照搬这条路。但他们踩过的坑,我在十分之一规模的团队里也反复见到,所以我们来逐一拆解。

错误一:把集中式工作流当成分布式来用

Git 的核心设计押注在去中心化上。每个克隆都是一个完整的仓库:完整的历史、完整的对象存储,离线也能正常工作。这个押注让 git 成为了世界标准,对大多数仓库来说至今依然正确。但有个几乎没人愿意承认的事实:实际上几乎没人是这样使用 git 的。你们的组织在 GitHub 或 GitLab 上只有一个权威远程仓库,开发者频繁推送和拉取,CI Runner 按定义就是常驻在线的。你其实是在用集中式系统,却穿着分布式的外衣,还承担着分布式的成本:每个 CI 任务都在拖着它永远用不到的历史,每个 agent 沙箱都在生成它根本不需要的 packfile。

这是 Anthropic 重新设计中给我们的第一个真正教训。他们退一步审视后发现,客户端“按定义就是始终在线的 agent 和任务”,于是他们围绕一个随时可达的服务端来设计。客户端被刻意做得很“笨”:有界的本地缓存、磁盘上的工作副本、一个文件系统监听器。服务端负责 blame、计算 diff、执行合并。Google 的 Piper 和 Meta 的 Sapling 也得出了同样的结论。这不是反 git 的立场,而是承认架构应当匹配实际的工作负载,而不是二十年前这个工具设计时假设的工作负载。我在安全工具领域也见过同样的模式:团队部署了一套支持离线的 agent 架构,结果却运行在“离线”根本不会发生的环境里,然后纳闷为什么同步层会变成噩梦。

这对你的团队来说,结论不是“去造一个 VCS”,而是:审视你的假设。写下你真正依赖哪些 git 特性。离线工作?真正的 fork?基于密码学的提交签名?如果诚实的答案是“只有一个远程、始终在线、CI 包揽一切”,那就别再为一个你根本没有的分布式工作流做优化,转而优化检出和查询的成本。

错误二:用镜像和种子掩盖规模问题

每个大型仓库团队都会经历同样的痛苦阶段。先是浅克隆,然后是稀疏检出和 promisor 远程仓库,接着是内部 git 镜像,再后来是存放在对象存储里的快照“种子”,让 CI 任务能从接近主干最新状态的地方启动。Anthropic 把这些全都用上了,高峰时期内部镜像多达数百个。而这些方案的失败模式总是一样的:它们不会大声报错,而是慢慢腐烂。种子在两次刷新之间离主干越来越远。部分克隆会以令人困惑的方式打破 git blame 和 git log --follow。有人的 bisect 因为历史被截断而悄无声息地返回错误结果。直到事故复盘时,才会有人注意到。

最让我睡不着的正是最后这一点。部分历史的正确性问题,正属于那种藏在你以为系统保证了什么与它实际做了什么之间的差距里的隐蔽错误。浅克隆在你请求超出其深度的 blame 时并不会报错,它只会给你一个不完整的答案。如果你的自动回滚流水线信任了这个答案,那你就搭建了一台会自信满满地回滚错误变更的机器。

  • 浅克隆会破坏依赖历史的工具(blame、bisect、merge-base),而且是悄无声息地失败,而不是大声报错。
  • 快照种子会偏离主干最新状态;合并频率越高,偏离越快,因此变通方案恰恰会在负载增加时失效。
  • 镜像集群会放大运维面:数百个有状态的副本可能各自落后、损坏或耗尽磁盘。
  • 任何涉及历史语义的变通方案都需要回归测试,否则它会在事故期间反咬你一口。

在 Anthropic 的规模下,解决办法是结构性的:整个仓库在任何地方都不存在完整物化的副本,连服务端也没有,所有内容都按内容寻址,并且可以水平扩展缓存。在你的规模下,做法则要温和得多:统计有多少台机器持有完整或接近完整的仓库副本,并诚实地评估每一个副本带来的偏差与失败模式。

错误三:让每个 CI 任务都维护自己的仓库副本

关于你的 CI 集群,有个值得问的问题:这些任务中有多少真的需要工作副本?Anthropic 在搭建完系统之后给出的答案是“比我们预想的少得多”。一个需要获取某次提交与其合并基点之间 diff、某个文件的 blame,或者某个路径相关提交列表的任务,完全可以直接通过 API 向服务端查询。不需要克隆,不需要 fetch,也不需要磁盘。他们的自动 bisect 和回滚流水线,如果没有这种廉价的程序化访问,“在规模化场景下根本不现实”。我相信这一点,因为我见过不少团队尝试在本地克隆之上运行同样的流水线,最后被 fetch 耗时拖垮。

这条原则的适用范围远超源码管理。把检出当作昂贵的例外,而不是默认行为。当任务确实需要文件时,稀疏检出和虚拟化检出可以只物化请求的目录,甚至只物化实际被读取的文件,成本能降低几个数量级。Anthropic 的 fetch 协议完全舍弃了 git 的协商过程:客户端直接请求它需要的不可变树和 blob,任何缓存都能应答,整体行为更像一个分层 CDN,而不是一个为每个客户端实时计算 packfile 的智能服务端。如果你曾经分析过 git 服务器在 CI 负载下被压垮的情况,就会明白这为什么重要:packfile 的生成非常消耗 CPU,因为协议要求服务端针对每个客户端已有的内容计算增量。

如果你的客户端始终在线,并且只有一份权威副本,那么你维护的每一个本地克隆,都是一份你全价付费的缓存,而且最终拿到的仍然是缓存的语义。

错误四:让提交成本随仓库规模增长

他们设计中最有意思的工程决策是 changeset。在 git 中,一次提交指向代表整个仓库状态的树;而他们的 changeset 只记录相对于第一父提交发生变化的文件列表,外加指向 blob 存储的内容寻址指针,并追加到预写日志中。不需要接收 packfile,不需要加锁,也不需要跨服务器协调。引用(分支、标签)被刻意弱化,因为“所有服务器都必须达成一致的全局状态,是一个迫在眉睫的扩展性问题”。移动一个引用只是对单个 Spanner 行的 compare-and-swap 操作,与提交创建解耦。

风格化插图:包裹沿传送带送入一条发光的日志,并衍生出各种数据结构。
只把变更记录进预写日志;blame、路径历史、提交图索引等其他数据都从日志中惰性推导出来。

与之配套的思路是“派生数据”:blame 索引、路径历史、提交图的跳表,全部通过同一套通用机制,从每个 changeset 的父节点递归计算出来,可以按类型选择急切计算或惰性计算。新增索引类型时无需改动客户端。这对任何事件溯源系统都是很好的模式。我在审计日志流水线中也推荐过类似结构:追加式日志就是真相,所有查询结构都是可重建的投影。一旦有人在这套机制之外手工维护某个投影,偏差就来了。

从安全角度提一点需要认真对待的注意事项:那些担心集中化的评论者是有道理的。把提交、合并和历史查询全部放在一个服务端系统里,会成为极具价值的攻击目标,而拥有写权限的 AI agent 同时接触“统管一切的那一个仓库”,风险会被严重放大。Anthropic 同时构建了一套“丰富的源码控制 IAM 系统”,而其中的细节比数据结构更重要。如果你走向集中化,你的爆炸半径计算方式也会随之改变,从第一天起就要为隔离、每个 agent 独立的凭证以及审计做好预算,而不是等到第三个季度。那些让有状态的 agent 运行在共享基础设施上的团队,对这种痛苦早已深有体会。

错误五:以为每天 3 万次合并就意味着每天 3 万次正确决策

工程本身令人印象深刻,真正的开放问题在于流经这套系统的是什么。有位从业者说得很直白:每次他们停止审查 AI 生成的代码,“我之后就会发现一堆烂代码。不是每次审查都能找出问题,但次数多到我不敢不逐行审查”。把这个命中率乘以每天 3 万次合并,你就明白为什么 Anthropic 还必须构建“管理和落地大量已审查代码的新方法”,包括审查面、落地队列和自动回滚流水线。版本控制系统本身并不是产品,落地流水线才是。只有当审查能力先成为瓶颈,源码管理才会随之成为瓶颈。

他们的战斗故事里还藏着一个坦诚的细节:项目中的工程师普遍把自己的大部分工作描述为告诉编码 agent “不”。他们手写的原型数据摄取流水线本来还可以,直到一个更新的模型把它重写成内存占用降低 20 倍、速度快 4 倍的版本。但要做到这一点,需要有品味的人来决定接受什么。这与我见过的一切相吻合:AI 把工程师的工作重心从编写转向了审查,而审查吞吐量现在成了稀缺资源。我们之前也写过每一层审查都会让团队变慢,难点在于,对于 agent 生成的代码,减少审查层级同样不安全。你需要的是更好的审查工具,而不是更少的审查。

你实际应该怎么做

你不是 Anthropic。你没有他们的 token 预算(他们自己也承认这一点),也没有每天 2500 万次检出。但 AI 编码工具正把每个人的合并频率推高,压力点的出现顺序是可以预测的。以下是我会遵循的操作顺序,即 Priya 的“最少遗憾”定律:

  1. 先度量。统计每天的检出次数、每个 CI 任务拉取的字节数,以及有多少台机器持有完整仓库副本。在遇到规模问题之前,你很可能先遇到了浪费问题。
  2. 消除不必要的检出。在购买任何基础设施之前,把元数据类的问题(diff、blame、祖先关系)迁移到 API 上,比如 GitHub/GitLab 的 API,或者一个轻量的内部服务。
  3. 让稀疏克隆和部分克隆变得安全。针对浅克隆显式测试你的 bisect、blame 和回滚工具,否则就在自动化中禁止使用它们。
  4. 在可能的情况下,把 CI 默认设为从服务端拉取不可变对象。同时把镜像集群当作技术债来对待,它的利息会不断复利增长。
  5. 把投入放在落地流水线上,而不是仓库格式上。审查能力、合并队列和自动回滚,才是 agent 规模的开发最先崩溃的地方。
  6. 最后再考虑替代的 VCS 架构。Sapling 是开源的,无需自己从零造轮子,就能获得大部分客户端-服务端架构的好处。

这里最深的教训其实与 git 无关。每个工具都会编码它所处工作负载的假设:离线的人类、一次一个提交、代码的编写速度慢于审查速度。而这些假设正在被 agent 打破,发生在基础设施、审查和安全边界等各个层面。最终胜出的团队不会是那些推倒重来的团队,而是那些先注意到哪些假设被打破了,然后精准修复这些假设的团队。