GitHub 详解新 Git 存储架构 Rubicon
Inside GitHub’s new Git storage architecture
GitHub 公布新的 Git 存储架构 Project Rubicon,内部测试中单仓库可支持 5,000 个并发智能体、每秒 7,200 次 push。该架构把 Git 对象放在 Azure Blob Storage,引用状态交给自建的 Ref-CAS 复制日志服务,并把压缩与垃圾回收拆到独立后台节点,使存储、请求服务与仓库维护可以各自独立扩容。
GitHub 工程团队拆解了新 Git 存储架构的设计取舍,读者可从中看到为智能体高并发写入重构基础设施的具体路径。
上周在 GitHub Universe,我们发布了全新的 Git 存储架构,每秒可处理数千次写入,读取能力可按需扩展。通过内部测试,我们已证明能够支持 5,000 个并发 agent 在同一个仓库上工作,每秒向一个仓库发送 7,200 次推送,同时以始终如一的低延迟处理读取流量。这一新架构是 GitHub 支持数亿开发者进行 agentic 开发的基础,我们一直在 GitHub 运行的同时在其底层构建这一架构。
在过去一年里,agent 改变了 GitHub 上繁忙仓库的面貌。在本系列的第一篇文章中,我们阐述了下一代 Git 存储架构背后的两项原则:最小化协调,以及将存储与计算解耦。在上一篇文章中,我们介绍了过渡环节:将 99.99999% 的仓库迁移到 Azure,将写入法定人数限定到单个引用,并搭建由 Azure Blob Storage 支持的虚拟副本。
我们还留下了一个观察结论:一次写入中需要排序的部分非常少。Git 对象是不可变的,并以其内容命名,这意味着两个写入者可以同时存储它们,而无需就任何事情达成一致。推送中需要达成一致的部分是引用更新,而引用更新相比之下非常小。虚拟副本是围绕这一点进行构建的第一步,也是我们将仓库接入新架构的手段。
架构的其余部分都建立在这一观察之上。只有引用更新需要达成一致。这使得 Git 基础设施中过去紧密耦合的部分(存储数据、处理请求和维护仓库)能够独立扩展。对开发者而言,Git 仍然是 Git:他们以同样的方式推送、拉取和创建分支,同样的保护机制依然在位。下面,我们将梳理设计背后的需求,跟踪一个请求在系统中的流转过程,并深入剖析其中的两个组件。
为 agent 规模而构建
我们从两个维度来思考 GitHub 平台的规模:
- 水平扩展是指有多少开发者和 agent 能够在众多仓库上工作。GitHub 目前在这一维度上的许多方法已经过多年的实战检验,并被延续到新架构中:跨基础设施的租户分区、租户流量的隔离和/或公平性保障,以及负载感知路由,等等。
- 垂直扩展是指有多少开发者和 agent 能够在一个仓库上工作。随着 agent 编写更多代码,我们预计单个仓库会变得更大、更繁忙,因此我们的大部分需求都来自垂直维度。
如今,GitHub.com 上最繁忙的仓库每秒最多接收几十次写入,通常以短时突发形式出现。随着越来越多的工作转移到并行且全天候运行的 agent 上,我们预计这些突发将在未来一年内变成持续速率。每次推送都会带来更多读取:CI 运行、部署系统拉取新提交,以及其他 agent 为获取上下文而拉取它。这种读取扇出会对整体流量产生巨大的放大效应,并随写入量而扩展。
我们已经在处于增长曲线顶端的代码库上看到了这一趋势的体现。下图展示了一个 GitHub 代码库从 3 月到 5 月的每日请求量,其间其流量增长了约 4 倍。此后,它的增长一直沿着指数轨迹持续。

我们必须为一个由智能体驱动的开发持续以这种速度加速的世界做好规划。我们没有按下一个增量来规划,而是设定了至少两个数量级以上的规模目标。对于单个代码库而言,这意味着新架构需要:
- 每秒接受数千次写入。
- Support multiple writer nodes. There are two reasons for this:
- 经验告诉我们,对传入的 pack 建立索引以及检查对象连通性是写入过程中最消耗 CPU 的部分,而且这一成本会随写入规模增长,因此单个节点很快就会受限于计算能力。
- 将这项工作分散到多个节点上,可以提升容错能力,并在节点故障时缩短恢复时间。
- 最小化提交延迟。智能体会等待每次推送完成后才进入下一步。
- 按顺序记录每个引用的变更,并具备跨引用快速线性化的能力。落后的虚拟副本需要仅下载其服务传入请求所需的数据来追赶进度。
- 在没有任何变化时保持读取开销低廉,考虑到读写比例,这是常见情况。
- 让每个组件能够独立扩展。
除此之外,系统还必须能够提升整体可靠性并实现全球扩展。
引入“Project Rubicon”
2026 年 5 月,紧随最初的 Azure 迁移之后,我们开始着手设计一个能够满足这些需求的新方案。我们尝试了多种方法。在所有方案中,pack 数据都存放在 Azure Blob Storage 中,其中一些方案还尝试将引用状态也存放在那里。我们将在下文介绍发现的一些权衡取舍,但最终确定了一个我们认为能够在保持 GitHub 这类平台所要求的正确性、持久性、可用性和延迟保证的同时,最大化吞吐量的设计。我们将其命名为“Rubicon”,以此致敬行业随着智能体开发而跨过的不可回头之点。
让我们自上而下地梳理 Rubicon。许多东西保持不变:来自 Git 客户端、API 和 Web UI 的请求到达 Spokes,Spokes 仍然负责路由和负载均衡,并提供 GitHub 其余部分用来读写代码库数据的内部 API。Spokes 还继续承担放置与均衡服务的角色,维护水平扩展所需的分区、隔离和租户公平性。相比之下,Spokes 之下的数据平面是全新的。

Spokes 将每个请求路由到分配给该代码库的一小组虚拟副本中的某一个。虚拟副本是一组一次性计算资源,负责处理读取、写入或两者兼有。虚拟副本的本地磁盘是一个可以随时重建的缓存。
Spokes 会尽可能将某个代码库的流量路由到相同的虚拟副本,以保持其缓存热度和低延迟,但正确性并不依赖于请求落在某个特定的虚拟副本上。当某个虚拟副本消失时,该代码库的其他虚拟副本会继续提供服务,而替代者会随着流量的到来填充其缓存。
当仓库变得繁忙时,我们会增加更多虚拟副本。另外,一组压缩节点在后台直接针对 Blob Storage 和 Ref-CAS 工作,合并每个仓库的包并移除不可达对象。这项工作从不在虚拟副本上运行,这意味着它们的 CPU 和内存几乎可以完全专用于处理请求。
持久状态存在于两个地方:
- Git 对象,以包和辅助读取加速数据的形式,存放在 Azure Blob Storage 中。
- 引用以及构成仓库的包列表(包清单)存放在一个独立的复制日志服务中,我们称之为 Ref-CAS(意为“引用比较并交换”)。
让我们从一次写入和一次读取的角度来梳理这个架构:
- 写入:一个虚拟副本对传入的包建立索引并上传到 Blob Storage,然后向 Ref-CAS 提交一个事务,在清单中列出该包并更新引用。之后,一个压缩节点将该包与其他包合并,以保持读取速度。
- 读取:一个虚拟副本向 Ref-CAS 检查其引用版本,从 Blob Storage 拉取所需的包到其缓存中,并从那里提供读取服务。
我们在新架构中采取了许多不同的方法,从虚拟副本上的软件栈到针对不同用例(例如许多小仓库 vs. 一个大仓库)调整流量路由和工作负载调度的方式。让我们深入探讨架构中的两个组件,并围绕两条设计原则讲述它们的故事。
最小化协调:以高吞吐量变更引用状态
像 Azure Blob Storage 这样的对象存储服务非常擅长它们所做的工作,并且天然适合许多数据密集型应用,比如源代码存储。它们持久且已复制,可以容纳我们需要的任意多数据,每字节成本相对较低,并且只要流量分散在许多键上,就能提供极高的聚合吞吐量。因此,新 Git 存储架构中几乎所有持久数据都存放在 Azure Blob Storage 中,包括:
- 由推送、压缩和垃圾回收写入的包和索引。
- 我们的维护过程从这些包构建的各种读取加速器(如多包索引、可达性位图以及一些自定义格式)。
- 让压缩和垃圾回收能够恢复或审计其工作的记录,以及压缩节点用于认领工作的租约。
- 频繁重复获取的缓存响应。
- 仓库引用状态的检查点,尽管不是用于服务请求的权威状态。

大部分数据只写入一个不同的键,之后永不改变,这正是它非常适合 Blob Storage 的原因。
引用则相反。一个繁忙仓库的引用是一块相对较小的状态,许多写入同时争相更新它。这种访问模式更难从 Blob Storage 提供服务,我们的第一个原型向我们展示了原因。对于这类工作负载,对象存储是一种直观的选择,并且在一定规模范围内效果良好:条件写入免费提供了比较并交换,而且数据已经持久且已复制。然而,我们遇到了一组艰难的选择,下面通过我们尝试的两种方法来加以说明。
我们的第一种方案是为仓库的所有引用建立一个预写日志,并以单个 blob 作为索引。每次更新都是针对该 blob 的条件写入。这个方案是正确的,而且读取成本很低:虚拟副本会询问该 blob 是否已发生变化,如果没有变化,就完成了。不足之处在于写入。对同一个 blob 同时只能有一个条件写入在进行中,因此即使我们把一次写入期间到达的所有推送批量合并到下一次写入中,仓库的写入上限仍然是每往返一次单个 blob 只能写入一次。在实践中,这给我们的写入吞吐量带来的结果是每秒数百次(中段)。
我们的第二种方案将引用拆分到多个分段 blob 中,每个引用通过哈希映射到其中一个分段。对不同分段中引用的写入不再相互等待,从而提高了上限。但现在读取必须检查每个分段才能知道是否有任何变化,而且其速度取决于最慢的分段。我们用写入延迟换取了读取延迟,而读取的数量比写入高出几个数量级。我们可以通过并排展示多次写入和一次读取来直观地说明这一点:
两种方案都无法满足所有要求,所以我们构建了另一种东西:一个专用于引用的复制日志服务,即 Ref-CAS 服务。这就是我们在上一篇文章末尾提到的、仍然存在于技术栈某处的那个(小型)共识组。下面是它的工作原理。
Ref-CAS 中的每次引用更新都是由法定人数提交的比较并交换(compare-and-swap)操作。事务只锁定它涉及的引用,检查每个引用是否仍具有写入者期望的值,并且一旦日志的大多数副本记录了该事务,它就会变得可见。代理大多推送到自己的分支,因此大多数更新不会产生争用,并且它们会并行复制。当两次推送确实更新同一个引用时,只有一次会追加到日志中。
对 Ref-CAS 的每次提交都会获得一个日志序列号(LSN),而仓库的版本就是其最新提交的 LSN。读取首先会针对该版本进行版本检查。虚拟副本会发送它已经持有的引用状态的版本。如果该版本仍然是最新的,回复中根本不包含任何引用,因此未发生变化的仓库只需一次比较。如果虚拟副本落后,它会收到当前的引用和版本,以及指向 pack 数据的指针和其他需要知道的信息,例如上一次垃圾回收周期发生的时间。
与我们旧架构中的共识组不同,Ref-CAS 从不持有或验证对象数据。它只存储指向对象数据的指针。虚拟副本在提交引用事务之前,会将推送的 pack 上传到 Blob Storage。引用只会指向已经持久化的对象。
这些方案使我们能够仅对引用状态进行事务处理,达到每秒数万次操作,并为复制日志上的额外注解提供了基础,这些注解帮助读取操作决定针对给定请求需要下载什么。
但在单个仓库上支持每秒数千次写入会带来一个二阶问题:维持读取和写入的性能。为此,我们需要一种不同的压缩方法。
将存储与计算解耦:在每秒数千次写入下的压缩
每次推送都会向 Blob Storage 添加一个 pack。Git 可以从多个 pack 提供仓库服务,但每增加一个 pack 都会让查找略微更昂贵,因此 pack 数量不能无限制增长。在我们的旧架构中,压缩运行在同时处理流量的同一批副本上,并且必须在每个副本上运行。在新架构中,它完全在后台的独立节点上运行,直接针对持久化存储,并使用 Ref-CAS 服务通知虚拟副本不断变化的状态。
每个压缩节点通过租约声明对仓库数据某个子集的所有权,这使我们能够水平扩展压缩。下面介绍一个压缩节点如何处理仓库 pack 的单个分片:
该节点首先在 Blob Storage 中对分片获取租约,因此同一时间只有一个节点压缩一个分片。与此同时,虚拟副本继续上传 pack 并将它们列在 pack 清单中。节点读取清单以查找新的 pack,并将它们收集到一个开放段中,该段实际上是即将被压缩的数据的暂存区。当该段达到基于 pack 数量、大小或时间的既定限制时,它会封存该段,使其不再接受新的 pack,然后将其中的 pack 合并为更少、更大的 pack。在压缩进行期间,该分片的下一个段可以开始捕获其输入 pack。在发布任何内容之前,节点会检查新 pack 包含的对象与输入完全相同。然后它上传新 pack 并将它们添加到清单中。第二个事务从清单中移除输入,随后清理会删除它们。
为了横向扩展,此过程会同时为许多分片独立运行。每个 pack 的哈希映射到一个分片,因此增加分片和节点就能增加压缩吞吐量。压缩后仍低于大小限制的 pack 会按其自身哈希回到某个分片并再次合并,因此 pack 会分层增长,直到达到限制。最终结果是,在推送量攀升的同时,pack 数量实际上保持平稳:
压缩会合并 pack,但会保留每个对象。垃圾回收是移除不再被任何内容引用的对象的步骤,例如已删除分支和强制推送留下的对象。在我们的新架构中,垃圾回收的工作方式与压缩非常相似,使用专用工作节点来处理计算密集的部分。它还有一些用于安全删除数据的特殊行为,值得单独讨论。
使用这些方法,我们能够一致地展示出随着写入量攀升而保持平稳的延迟曲线,并且我们还有其他计划中的优化,使压缩和垃圾回收过程本身更加高效。
接下来是什么
我们开始时的两条原则让此架构的每个部分都能独立扩展。我们通过添加虚拟副本来扩展读取和写入。Ref-CAS 独立地对每个引用排序,而不是对整个仓库排序。我们通过添加压缩节点来扩展维护。我们的目标是让每个仓库,从单个开发者的副业项目到最繁忙的 monorepo,都运行在同一架构上,无需对繁忙的仓库做特殊处理。仓库不会一直很小。一个副业项目可能一夜之间迎来大量代理,它不应该需要我们进行迁移或特殊处理才能跟上。单一架构也意味着我们所做的每一项改进都会同时惠及每个仓库。
这个基础让我们能做更多事情。接下来我们聚焦三个领域:将架构扩展到超出我们今天所能预测的需求规模,让智能体在 GitHub 上执行的工作更高效,以及为智能体开发演进 Git 本身。
改进架构
我们计划进一步推进这一架构。以下每一项改进都能让仓库处理更多活动,同时减少每个请求背后的工作量:
- 只读取所需内容。从 Blob 存储中获取单个对象,而不是获取包含它的整个 pack。
- 按类型组织 pack。将提交、树和 blob 分组到各自的 pack 中。这样许多请求触及的数据就更少。
- 部分副本。无需本地磁盘上的每个 pack 即可处理请求。
- 物化视图。以即用形式保存仓库数据,例如预生成的归档、默认分支的近实时浅拷贝,以及与每次提交一同存储的智能体会话上下文。
- 全局副本。将副本放置在任何区域,靠近使用它们的工作负载。读取与最新写入之间保持有界陈旧度。
- 缩容到零。根据实际活跃的仓库来扩缩容集群。
GitHub 上高效的智能体操作
扩展基础设施是支持智能体的一方面。另一方面是让每个智能体操作更廉价:更少的获取字节、更少的往返次数,以及更少的等待变更时间。以下每一项快速跟进项都建立在本博文的存储工作之上:
- 无需克隆即可工作。许多智能体任务从不需要完整的工作树。它们只需要少量文件和足够的上下文来进行更改。它们可以直接从服务器读取这两者。
- 原子提交 API。在一次操作中提交多文件更改。整个更改要么提交成功,要么在冲突时干净地被拒绝。
- 事件流。智能体及其周围的系统在拥有实时信息时工作效果最佳。通用事件流让它们能够订阅变更并在变更发生时做出反应。
为智能体改进 Git
我们在 Git 本身也有工作要做。GitHub 长期以来一直是 Git 项目的贡献者,我们现在也不会停下。以下是我们期待继续推进的一些工作示例:
- 轻量克隆。部分克隆、浅克隆和稀疏检出各自都能让智能体获取更少内容。每一种都有缺口。它们也并不总能很好地协同工作。我们希望改进每一部分并让它们相互适配。
- 消除隐藏的扩展成本。Git 仍有一些代码路径的减速速度远快于仓库的增长速度。最近的版本修复了其中几个,但我们预计会发现更多。
- 更快的历史遍历。像 commit-graph 这样的元数据可以在不遍历每个提交的情况下回答 merge-base 和可达性问题。当智能体的 API 调用在服务器上转化为 Git 工作时,这一点最为重要。
- 避免常见陷阱。智能体会犯与开发者相同的错误。Git 可以捕获更多常见错误,并在它们造成损害之前发出警告。
Agents 已经改变了软件的构建方式。GitHub 和 Git 也随之改变。我们很高兴能将这一新架构带过起跑线。它目前处于有限私有预览阶段,并将于明年年初更广泛地推出。当在一个代码仓库上工作的 agent 数量只受限于他们的想法时,我们迫不及待想看到团队们会构建出什么。我们将与客户和 Git 社区一起持续演进 Git 和 GitHub。
文章 深入解析 GitHub 的新 Git 存储架构 首次发布于 The GitHub Blog。
来源:GitHub Blog · Engineering · github.blog