深入解析 GitHub 全新 Git 存储架构
上周在 GitHub Universe 大会上,我们发布了新的 Git 存储架构。它支持每秒数千次写入,读流量可随需扩展。内部测试表明,该架构能支撑 5,000 个并发 Agent 同时操作一个仓库,每秒向仓库推送 7,200 次数据,同时保持读请求的低延迟。这一新架构是 GitHub 为数以亿计的开发者提供智能体开发支持的基础,并且我们是在 GitHub 保持正常运行的情况下,在其底层逐步构建起这套架构的。
过去一年,Agent 彻底改变了繁忙仓库在 GitHub 上的运作形态。在本系列的第一篇博文中,我们阐述了下一代 Git 存储架构背后的两大原则:最小化协调成本,以及将存储与计算解耦。在上一篇博文中,我们介绍了过渡方案:将 99.99999% 的仓库迁移至 Azure,将写入仲裁范围缩小到单个引用(reference),并建立了由 Azure Blob Storage 支持的虚拟副本。
我们还留下一个观察结论:写入操作中需要排序的部分极少。Git 对象是不可变的,且以其内容命名,这意味着两个写入者可以同时存储对象,无需就任何事项达成一致。推送过程中需要协调一致的是引用更新,而相比而言,引用更新的数据量极小。虚拟副本是围绕这一特性构建的第一步,也是我们让仓库接入新架构的途径。
其余架构设计正是基于这一观察。只有引用更新需要达成一致。这使得原本紧密耦合的 Git 基础设施组件(数据存储、请求服务、仓库维护)能够独立扩展。对开发者而言,Git 依然是 Git:他们以相同的方式推送、拉取和创建分支,原有的保护机制也保持不变。下文将梳理设计背后的需求,跟踪请求在系统中的流转过程,并深入剖析其中两个核心组件。
面向智能体规模构建
我们从两个维度考量 GitHub 平台的规模:
- 横向扩展指能同时为多少开发者和 agent 在众多仓库上提供服务。GitHub 在这个方向上的许多现有做法历经多年考验,并在新架构中得以延续:租户在基础设施上的分区、租户流量的隔离与公平性控制,以及感知负载的路由等。
- 纵向扩展指能支撑多少开发者和 agent 在同一个仓库上工作。随着 agent 写入越来越多代码,单个仓库会变得更大、更繁忙,因此我们的需求大多来自纵向扩展。
目前,GitHub.com 上最繁忙的仓库每秒最多只有几十次写入,且通常以短暂爆发的方式出现。随着更多工作转移到全天候并行运行的 agent 上,我们预计这种爆发将在一年内变成持续负载。每次 push 还会引发大量读取:CI 运行、部署系统拉取新 commit、其他 agent 抓取它作为上下文。这种读放大效应会让整体流量随写入量大幅增长。
在曲线顶端的仓库上,这一趋势已经显现。下图展示了 GitHub 上某个仓库从 3 月到 5 月的每日请求量,期间流量增长了约 4 倍。此后它仍在以指数曲线继续增长。

我们必须为 agent 驱动开发持续以这种速度加速的未来做准备。与其只规划下一个增量,我们直接把目标定在至少高出两个数量级的规模上。对单个仓库而言,这意味着新架构需要做到:
- 每秒接受数千次写入。
- 支持多个写入节点。原因有二:
- 经验告诉我们,索引收到的 pack 和检查对象连通性是写入中最消耗 CPU 的部分,且开销随写入规模增长,因此单节点很快就会成为算力瓶颈。 ```html
- 将这类操作分散到多个节点,有助于提升容错性,并在节点故障时缩短恢复时间。
- 最小化提交延迟。代理在每个推送后才会推进到下一步,而推送本身必须尽快完成。
- 为每个引用(reference)按顺序记录变更,并能快速跨引用进行线性化。落后的虚拟副本只需下载处理当前请求所必需的数据即可追赶上来。
- 在数据未变更时保持读取成本低——考虑到读写比,这通常是常态。
- 让各个组件可以独立扩展。
在此基础上,系统还必须提升整体可靠性并具备全球扩展能力。
“Project Rubicon”
2026 年 5 月,紧随最初的 Azure 迁移之后,我们开始着手设计一套能满足上述要求的方案。我们尝试了多种思路:所有这些方案中,pack 数据都存放在 Azure Blob Storage 中,部分方案还试验了将引用状态也保留在那里。下文会介绍我们摸索出的一些权衡取舍;最终,我们选定了一个在最大化吞吐的同时,保留正确性、持久性、可用性和延迟等 GitHub 平台所必需保障的设计,并将其命名为“Rubicon”——这个名字向智能体(agentic)开发这一不可逆转的行业节点致敬。
下面从上到下梳理 Rubicon。很多部分保持不变:来自 Git 客户端、API 和 Web UI 的请求仍然到达 Spokes。Spokes 继续负责路由与负载均衡,并对外提供内部 API,供 GitHub 其他部分读写仓库数据。它同样继续承担放置与均衡服务的角色,维护分区、隔离和多租户公平性,以实现横向扩展。相比之下,Spokes 之下的数据平面则是全新的。
```
Spokes 会把每个请求路由到分配给该仓库的一小组虚拟副本之一。虚拟副本是一个统一的、可随时丢弃的计算集群,负责处理读、写或两者兼有。虚拟副本的本地磁盘只是一个缓存,随时可以重建。
Spokes 会尽量把同一仓库的流量路由到相同的虚拟副本,以保持缓存热度、降低延迟,但请求落在哪个副本上并不影响正确性。某个虚拟副本下线时,仓库的其他副本会继续提供服务,替补副本会随着流量的到来逐步填充缓存。
当某个仓库变得繁忙时,我们会增加更多虚拟副本。此外,一组 compaction 节点在后台直接对 Blob Storage 和 Ref-CAS 进行操作,合并各仓库的 pack 并清理不可达的对象。这些工作从不在虚拟副本上执行,因此虚拟副本的 CPU 和内存几乎可以全部用于处理请求。
持久化状态存储在两个地方:
- Git 对象,以 pack 和辅助读加速数据的形式,存放在 Azure Blob Storage 中。
- 引用(References)和构成仓库的 pack 列表(即 pack manifest),存放在一个独立的复制日志服务中,我们称之为 Ref-CAS("Reference Compare-and-Swap" 的缩写)。
下面从一次写入和一次读取的视角来走一遍这套架构:
- 写入:虚拟副本对收到的 pack 建立索引并上传到 Blob Storage,然后向 Ref-CAS 提交一个事务,将该 pack 记入 manifest 并更新引用。之后,compaction 节点会把这个 pack 与其他 pack 合并,以加快读取速度。
- 读取:虚拟副本通过 Ref-CAS 检查引用版本,把所需的 pack 从 Blob Storage 拉取到缓存中,然后直接从缓存提供服务。
新架构采用了多种不同的技术路径,从虚拟副本上的软件栈,到针对不同使用场景(例如大量小仓库对比单个大型仓库)调整的流量路由和负载调度机制。下面将深入剖析架构中的两个核心组件,并围绕两大设计原则讲述它们的故事。
最小化协调:高吞吐下的引用状态变更
Azure Blob Storage 等对象存储服务极其擅长其本职工作,是源代码存储等数据密集型应用的理想选择。这类服务具备持久性和复制能力,容量可随需扩展,单字节成本相对较低,且只要流量分散在众多键上,就能提供极高的聚合吞吐量。因此,新 Git 存储架构中几乎所有的持久化数据都存放在 Azure Blob Storage 中,包括:
- 由 push、压缩(compaction)和垃圾回收(garbage collection)生成的 pack 文件和索引。
- 由上述 pack 文件构建的各种读取加速器(如 multi-pack 索引、可达性位图及若干自定义格式),这是维护流程的一部分。
- 支持压缩和垃圾回收进行工作恢复或审计的记录,以及压缩节点用于声明工作的租约。
- 针对频繁重复 fetch 操作的缓存响应。
- 仓库引用状态的检查点,尽管这些检查点并非用于服务请求的权威状态。

这些数据中的绝大部分仅在初始化时写入唯一的键,之后便不再变更。这种特性使其非常适合使用 Blob Storage。
引用(References)则正好相反。繁忙仓库的引用是一份相对较小的状态,却会有大量写操作同时竞争更新它。这种访问模式很难用 Blob Storage 来支撑,我们的第一批原型也验证了这一点。对这类负载来说,对象存储似乎是顺理成章的选择,而且在一定规模内确实运行良好:条件写入自带 compare-and-swap 语义,数据本身也已具备持久化和多副本。但我们也因此陷入了两难,下面用我们尝试过的两种方案来说明。
第一种方案是把仓库所有引用的更新记录写进一条由单个 blob 承载的 write-ahead log。每次更新都是对这个 blob 的一次条件写入。这种做法是正确的,读取也很便宜:虚拟副本只需检查该 blob 是否有变化,没有就完事了。短板在写入:对同一个 blob 的条件写入同一时刻只能有一个在途,即便我们把一次写入期间到达的所有 push 合并进下一次写入,仓库的写入上限也就是对单个 blob 一次往返只能写入一次。实际测试下来,写入吞吐量只有每秒几百次的水平。
第二种方案是把引用拆分到多个 segment blob 中,每个引用按哈希映射到其中一个。不同 segment 上的写入互不等待,写入上限因此提高了。但代价是读取必须检查所有 segment 才能判断是否有变化,整体速度取决于最慢的那个 segment。我们等于用读延迟换了写延迟,而读取量又比写入量高出几个数量级。下面这张图把多次写入和一次读取放在一起对比,可以直观看出问题:
两种方案都无法完全满足需求,于是我们另起炉灶:为引用专门搭建了一个带复制的日志服务,也就是 Ref-CAS 服务。这正是我们在上一篇文章结尾提到的、还藏在技术栈某个角落的那个(小型)共识组。下面来看看它的工作原理。
Ref-CAS 中的每次引用更新都是一次由法定人数提交的原子的比较并交换(CAS)操作。事务仅锁定其触及的引用,校验每个引用的值是否符合写入方的预期,并在日志的多数副本记录下该事务后对其可见。由于代理主要推送至各自的分支,大多数更新不会产生冲突,且可并行复制。当两次推送更新同一引用时,仅有一次会被追加至日志。
对 Ref-CAS 的每次提交都会分配一个日志序列号(LSN),而仓库的版本即为其最新提交的 LSN。读取操作始于对该版本的校验。虚拟副本会发送其当前持有的引用状态的版本号。若该版本仍然最新,响应中不会包含任何引用,且对于未变更的仓库而言,一次读取仅需单次比较。若虚拟副本状态落后,则返回当前引用及版本号,以及指向打包数据和其他需知信息(如最近一次垃圾回收周期的发生时间)的指针。
与旧架构中的共识组不同,Ref-CAS 从不持有或验证对象数据,仅存储指向这些数据的指针。虚拟副本在提交引用事务之前,会先将推送的打包数据上传至 Blob Storage。引用仅指向那些已持久化的对象。
上述机制使我们能够以每秒数万次操作的事务吞吐量处理引用状态,并为在复制日志中添加额外注释奠定了基础,这些注释有助于读操作决定针对特定请求应下载的内容。
但在单个仓库上支持每秒数千次写入,会引发二阶问题:即维护读写性能。为此,我们需要一种不同的压缩策略。
存储与计算解耦:实现每秒数千次写入的压缩
每次推送都会向 Blob Storage 添加一个打包。Git 可从多个打包提供仓库服务,但每个额外打包都会使查找略微变贵,因此打包数量不能无限增长。在旧架构中,压缩在承载流量的同一批副本上运行,且必须在每个副本上执行。新架构中,压缩完全在独立的节点上后台运行,直接作用于持久化存储,并利用 Ref-CAS 服务通知虚拟副本当前不断演进的状态。
每个压缩节点通过租约(lease)认领仓库数据的一个子集,从而实现压缩能力的水平扩展。下面是单个压缩节点处理仓库某一个 shard 的 pack 文件的流程:
节点首先对 Blob Storage 中的该 shard 加租约,保证同一时间只有一个节点压缩它。与此同时,虚拟副本持续上传 pack 并登记到 pack manifest 中。节点读取 manifest 找出新 pack,将它们收进一个 open segment——可以理解为待压缩数据的暂存区。当 segment 按 pack 数量、大小或时间达到设定上限后,就将其封存(seal),不再接收新 pack,然后把其中的 pack 合并成更少、更大的 pack。压缩进行时,该 shard 的下一个 segment 可以同时开始接收新 pack。发布之前,节点会校验新 pack 中的对象与输入完全一致,然后上传新 pack 并写入 manifest。第二个事务将输入 pack 从 manifest 中移除,稍后由清理流程删除。
为了扩展规模,这一过程可以同时在多个 shard 上独立运行。每个 pack 的哈希值决定它属于哪个 shard,因此增加 shard 和节点就能线性提升压缩吞吐量。压缩后仍小于上限的 pack 会按自身哈希重新进入某个 shard 继续合并,pack 就这样逐层增长直至达到上限。最终效果是:在写入量不断攀升的同时,pack 数量保持基本平稳:
压缩只是合并 pack,并不删除任何对象。垃圾回收(GC)才是清理不再被引用的对象的环节,比如删除分支和 force-push 遗留下来的对象。在新架构中,垃圾回收的实现方式与压缩非常相似:用专门的工作节点承担计算密集的部分,同时针对安全删除数据做了一些特殊处理,值得单独展开讨论。
借助这些方案,我们已经能稳定实现写入量攀升时延迟曲线保持平稳,并且还有其他优化计划,可让压缩和垃圾回收过程本身更快。
接下来的计划
最初确立的两大原则,让这套架构的各个组件都能独立扩展。我们通过添加虚拟副本来提升读写能力;Ref-CAS 独立排序每个引用,而非对整个仓库操作;通过增加压缩节点来扩展维护能力。我们的目标是:无论是一人开发的业余项目,还是最繁忙的单仓库,都能在同一套架构下运行,无需为繁忙仓库做特殊处理。仓库不会永远保持小巧。一个业余项目可能一夜之间涌入大量智能体,而它不应该因此需要迁移或由我们提供特殊支持。统一的架构也意味着,我们做出的每一项改进都能同时惠及所有仓库。
这一基础为我们打开了更多可能性。接下来,我们将聚焦于三个方向:将架构扩展至超出当前可预测需求、提升智能体在 GitHub 上的工作效率,以及为智能体开发演进 Git 本身。
改进架构
我们计划进一步拓展该架构。每一项改进都能让仓库处理更多活动,同时减少每次请求背后的开销:
- 按需读取。从 Blob Storage 中获取单个对象,而非包含该对象的整个 pack。
- 按类型组织 pack。将提交、树和 blob 分组到各自的 pack 中。这样许多请求涉及的数据量会更少。
- 部分副本。即使本地磁盘没有所有 pack,也能响应请求。
- 物化视图。以即用型格式存储仓库数据,例如预打包归档、默认分支的近乎实时的浅层副本,以及与每个提交一同存储的智能体会话上下文。
- 全局副本。将副本部署在任意区域,靠近使用它们的工作负载。读取保持在最新写入的有界陈旧度内。
- 扩展到零。根据实际活跃的仓库数量动态增减集群规模。
GitHub 上的高效智能体操作
扩展基础设施只是支持智能体的一面。另一面是降低每次智能体操作的成本:减少获取的字节数、减少往返次数,并减少等待变更的时间。以下每一项快速跟进功能都建立在本帖介绍的存储工作之上:
- 无需 clone 即可工作。很多 agent 任务根本不需要完整的工作目录,只需要读几个文件、加上足够的上下文就能完成修改——这些都可以直接从服务器读取。
- 原子化提交 API。一次操作就能提交涉及多个文件的变更。要么整体提交成功,要么在冲突时干净地被拒绝。
- 事件流。agent 及其周边系统需要实时信息才能高效运转。一个通用的事件流让它们可以订阅变更,并在变更发生时立即做出响应。
为 agent 改进 Git
Git 本身也有需要改进的地方。GitHub 长期以来一直是 Git 项目的贡献者,现在也不会停下。下面是我们希望继续推进的几个方向:
- 更轻量的 clone。Partial clone、shallow clone 和 sparse checkout 都能让 agent 少拉取数据,但各有短板,彼此的配合也不理想。我们想改进每一项,并让它们协同工作。
- 消除隐藏的性能瓶颈。Git 中仍有一些代码路径的开销会随仓库增长而急剧上升。最近的几个版本已经修复了一部分,但我们预计还会发现更多。
- 更快的历史遍历。借助 commit-graph 这类元数据,无需遍历每个提交就能回答 merge-base 和可达性问题。当 agent 的 API 调用在服务器上转化为 Git 操作时,这一点尤为重要。
- 避免常见的坑。agent 会犯和人类开发者一样的错误。Git 可以更早识别这些常见错误,在造成破坏之前给出警告。
agent 已经改变了软件开发的方式,GitHub 和 Git 也在随之进化。我们很兴奋能推动这套新架构正式落地。它目前处于限量私测阶段,明年年初将逐步扩大开放范围。当仓库中 agent 数量的上限只取决于你的想象力时,团队会构建出什么,我们非常期待。我们也会继续与用户和 Git 社区一起,持续演进 Git 和 GitHub。