一个深夜里,某家科技巨头的测试集群中,两万个代码补全与重构 Agent 在三秒钟内同时发起了 git push,GitHub 的底层存储节点随后亮起了密集的超时告警。这不再是黑客发起拒绝服务攻击,而仅仅是现代 AI 辅助研发系统的一个普通事件。、
![]()
上周在 GitHub Universe 上,我们宣布了新的 Git 存储架构,能够处理每秒数千次写入,并按需扩展读取。通过内部测试,我们展示了支持 5000 个并发代理运行于仓库的能力,每秒发送 7200 次推送到仓库,同时以持续低延迟提供读取流量。这个新架构是 GitHub 支持数亿开发者代理开发的基础,我们一直在 GitHub 下构建架构,同时它还在运行。
在过去一年里,代理改变了 GitHub 上繁忙仓库的样貌。在本系列的第一篇文章中,我们阐述了下一代 Git 存储架构背后的两个原则:
![]()
最小化协调,以及将存储与计算解耦。在我们上一篇文章中,我们讲了桥梁:将 99.99999%的仓库迁移到 Azure,将写入群体范围限定到各个引用,并搭建由 Azure Blob Storage 支持的虚拟副本。
我们还提到了一个观察:几乎不需要写入。Git 对象是不可变的,并以其内容命名,这意味着两个作者可以同时存储它们,而无需对任何内容达成一致。推送中需要同意的部分是引用更新,而引用更新相比之下非常微小。虚拟复制品是围绕这一点构建的第一步,也是我们将仓库引入新架构的方式。
其余建筑也基于这一观察。只有参考资料更新需要双方同意。这使得以前紧密耦合的 Git 基础设施部分(存储数据、服务请求和维护仓库)能够独立扩展。对开发者来说,Git 依然是 Git:他们以相同的方式推送、取回和分支,并采用相同的保护措施。下面,我们将带您了解设计背后的需求,按照系统中的请求,并深入探讨其中两个组成部分。
为代理规模构建
我们将 GitHub 平台的规模从两个方向来思考:
-
横向规模是指开发者和代理能跨越多个仓库工作的数量。GitHub 目前在这一方向上的许多方法多年来经过实战验证,并被延续到新架构中:跨基础设施划分租户、租户流量的隔离和/或公平执行,以及负载感知路由等。
-
纵向规模是指一个仓库中最多的开发者和代理可以工作。随着代理编写代码的增加,我们预计单个仓库会变得更大更忙,因此我们的大部分需求来自垂直轴。
如今,GitHub.com 最繁忙的仓库最多在一秒内接收数十次写入,通常是短时间段。随着更多工作转向全天候并行运行的代理,我们预计这些突发工作将在未来一年内持续增长。每次推送都会导致更多读取:CI 运行,部署系统拉取新提交,其他代理获取上下文。这种读取扇出效应对整体流量产生了显著放大效应,并随着写入次数而扩展。
我们已经看到这种趋势在曲线顶端的仓库上显现。下面的图表显示了从三月到五月,GitHub 上一个仓库的每日请求,期间流量增长了大约 4 倍。自那以后,其增长呈指数级增长。
![]()
我们必须为一个代理驱动开发以这种速度持续加速的世界做规划。我们没有为下一个增量做规划,而是设定了一个至少大两个数量级的目标。对于单个仓库来说,这意味着新的架构需要:
-
每秒接受成千上万次写入。
-
支持多个写入节点。这有两个原因:
-
经验告诉我们,索引输入包和检查对象连接性是写入中最耗费 CPU 的部分,成本会随着写入规模的增加而增加,因此单个节点很快就会被计算限制。
-
将这些工作分散到多个节点可以提升容错能力,缩短节点故障时的恢复时间。
-
-
最小化提交延迟。每推送一次,特工都会等待,然后才进入下一步。
-
按每个参考顺序记录变化,并能快速线性化不同引用。落后的虚拟副本需要通过下载满足来请求所需的数据来赶上。
-
当读写比例没有变化时,保持读取成本低廉,这也是读写比例的常见情况。
-
让每个组件独立扩展。
此外,系统还必须能够提升整体可靠性并具备全球扩展性。
介绍“鲁比孔计划”
2026 年 5 月,紧接着最初的 Azure 迁移,我们开始着手设计能够满足这些需求的方案。我们尝试了几种方法。在所有这些中,包数据都存放在 Azure Blob 存储中,有些我们还尝试在那里保留参考状态。我们将在下面讨论一些权衡,但我们最终确定了一个我们认为既能最大化吞吐量,又能保证 GitHub 平台所需的正确性、耐用性、可用性和延迟的设计。我们称之为“Rubicon”,向行业与自主开发跨越的无可挽回点致敬。
让我们从上往下走一遍 Rubicon。许多内容保持不变:来自 Git 客户端、API 和 Web UI 的请求都会送到 Spokes,Spokes 仍然负责路由和负载均衡,并服务于 GitHub 其他部分用于读写仓库数据的内部 API。Spokes 还保持其作为安置和平衡服务的角色,保持横向规模所需的分区、隔离和租户公平性。相比之下,Spokes 下方的数据层是新的。

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

大部分数据写入一次独立键,之后不变,这也是它非常适合 Blob 存储的原因。
推荐人则恰恰相反。繁忙仓库的引用是相对较小的状态部分,许多写入会同时争相更新。这种访问模式更难从 Blob 存储中提供,我们的首批原型就展示了原因。对于这种工作负载,对象存储是一个直观的选择,并且在一定规模范围内表现良好:条件写入可以免费进行比较和交换,数据本身就已经很耐用且可复制。然而,我们遇到了一系列艰难的选择,下面通过我们尝试的两种方法说明了这一点。
我们的第一个方法是先写一个记录仓库所有引用的日志,由一个 blob 索引。每次更新都是对那个 blob 的条件写入。它是正确的,读取很廉价:虚拟复制品会问斑点是否变了,如果没有,那就已经变了。写作部分就比较弱。一次只能对一个 blob 进行一次条件写入,所以即使我们把每次写入过程中到达的推送都批处理到下一个 blob,仓库的写入上限也是每次对单个 blob 每次往返一次写入。实际上,这让我们的写入吞吐量达到了每秒数百。
第二种方法是将引用拆分到多个段块,每个引用对其中一个片段进行哈希。不同段落的引用不再相互等待,这提高了上限。但现在读者必须检查每个片段,知道有没有变化,而且速度也取决于最慢的部分。我们用写延迟换取了读延迟,而读入数量远远超过写入。我们可以通过并排展示多次写入和读取数据来可视化:
这两种方案都无法满足所有要求,因此我们构建了另一种方案:专门用于引用的复制日志服务,或者 Ref-CAS 服务。这就是我们在上次帖子结尾说过的(小)共识小组,仍然存在于堆栈中。事情是这样的。
Ref-CAS 中的每次引用更新都是由一个法定人数提交的比较与交换。事务只锁定其接触到的引用,检查每个引用是否仍具备写入者期望的值,并在日志副本的大多数记录后才可见。代理大多推送到自己的分支,所以大多数更新不会争用,并且并行复制。当两次推送更新同一个引用时,日志中只有一次会附加。
每个对 Ref-CAS 的提交都会获得一个日志序列号(LSN),而仓库的版本就是其最新提交的 LSN。读取开始时会对该版本进行版本检查。虚拟副本发送它已经持有的参考状态版本。如果该版本仍然有效,回复中完全没有引用,未更改的仓库只需进行一次比较。如果虚拟副本落后,它会返回当前的引用和版本,以及打包数据和其他需要知道的信息,比如上一次垃圾回收周期发生的时间。
与我们遗留架构中的共识组不同,Ref-CAS 从不保存或验证对象数据。它只存储指向它的指针。虚拟副本会在提交引用事务之前,将推送包上传到 Blob 存储。参考点只指向已经耐用的物体。
这些方法使我们能够以每秒数万次操作完成引用状态,并为复制日志中额外的注释提供基线,帮助读取决定为特定请求下载什么。
但支持单一仓库每秒数千次写入会带来二阶问题:维持读写性能。为此,我们需要一种不同的压实方法。
存储与计算的解耦:每秒数千次写入时的压缩
每次推进都会给 Blob Storage 增加一个包。Git 可以从多个包中提供一个仓库,但每增加一个包,查找成本会略高一些,所以没有绑定包的数量无法增长。在我们的遗留架构中,压缩运行在服务流量的副本上,并且必须在每个副本上运行。在新架构中,它完全在独立节点后台运行,直接对抗持久存储,利用 Ref-CAS 服务通知虚拟副本演变状态。
压缩节点各自通过租赁协议声称对存储库数据的子集拥有所有权,这使我们能够横向扩展压缩。以下是一个压缩节点如何通过仓库包的单个碎片工作:
节点首先在 Blob Storage 中租用分片,因此每次只有一个节点压缩一个分片。与此同时,虚拟复制品不断上传包装并在包装清单中列出。节点读取清单以寻找新的包,并将其收集到一个开放段,这实际上是即将压缩数据的缓冲区。当分段达到基于包数、大小或年龄的定义上限时,会封闭分段,不接受新包,然后将包合并成更少、更大的包。在压缩过程中,分片的下一个段可以开始捕获其输入包。在发布任何内容之前,节点会检查新包是否包含与输入完全相同的对象。然后它会上传新包装并添加到清单中。第二个事务会从清单中移除输入,清理则在后续删除它们。
扩展时,该过程会同时独立运行多个分片。每个包的哈希映射到一个分片,所以添加分片和节点会增加压缩吞吐量。一个压缩包如果仍然低于大小限制,会通过自己的哈希回到分片并再次合并,所以包会分层增长直到达到限制。最终结果是,在推车爬升时,实际上是平坦的包数:
压缩合并包,但保留所有对象。垃圾回收是移除不再有引用的对象的步骤,比如删除分支和强制推送留下的对象。在我们的新架构中,垃圾回收的工作方式与压缩非常相似,使用专门的工人来处理计算量大的部分。它还有一些用于安全移除数据的特殊行为,值得单独讨论。
通过这些方法,我们能够持续展示写入量增加时的平坦延迟曲线,并且我们还计划进行其他优化,以提升压缩和垃圾回收过程的性能。
接下来会发生什么
我们最初采用的两个原则是让架构的每个部分独立扩展。我们通过添加虚拟副本来扩展读写。Ref-CAS 独立排序每个引用,而非整个仓库。我们通过添加压缩节点来扩展维护。我们的目标是让每个仓库,从一个开发者的副项目到最忙碌的单仓库,都能在同一架构上运行,忙碌的仓库无需特殊处理。仓库不会一直保持小规模。一个副项目可以一夜之间吸引一批代理,不需要我们进行迁移或特殊处理来跟上。一种架构也意味着我们所做的每一次改进都会同时覆盖所有仓库。
这个基础让我们能做更多事情。接下来我们将重点关注三个领域:将架构扩展到超出我们目前预测需求的范围,提高代理在 GitHub 上的工作效率,以及推动 Git 本身以实现代理开发。
改进架构
我们计划将这种架构进一步发展。这些改进让仓库能够处理更多活动,同时减少每个请求背后的工作:
-
只读必要的内容。 从 Blob Storage 里抓一个对象,而不是整个装着它的包。
-
按类型组织包。 将提交、树状结构和 blobs 分成自己的包。许多请求因此触及的数据更少。
-
部分复制品。 在本地磁盘上不包含所有打包即可提供请求。
-
具象化的视角。 将仓库数据保持在现成型表单中,比如准备好的归档、几乎实时的默认分支浅副本,以及与每次提交一起存储的代理会话上下文。
-
全球复制品。 将副本放置在任何区域,靠近使用它们的工作负载。阅读保持在最新写作的有限陈旧范围内。
-
归零。 用实际活跃的仓库来扩充和缩小舰队。
GitHub 上的高效代理操作
扩展基础设施是支持代理的一个方面。另一方面是让每个代理操作更便宜:更少的字节取用,减少往返次数,减少等待变更的时间。这些快速跟踪项目都是在本帖中存储工作的基础上进行的:
-
不用克隆体工作。 许多代理任务从未需要完整的工作树。他们需要少量文件和足够的背景信息来做出改变。他们可以直接从服务器读取两者。
-
原子提交 API。 一次完成多文件变更。整个变更要么提交,要么在冲突时净利落地拒绝。
-
活动流。 代理及其周边系统在实时信息中运作最佳。通用事件流允许他们订阅变化并实时响应。
改进代理的 Git
我们也需要在 Git 本身中工作。GitHub 长期以来一直是 Git 项目的贡献者,我们现在不会停止。以下是我们期待继续开展的工作示例:
-
轻量级克隆体。 部分克隆、浅克隆和稀疏结账都让代理获得更少的资源。每个都有空隙。它们也不总是配合得很好。我们希望改进每一部分,使其契合。
-
移除隐藏的扩展成本。Git 的代码路径速度比仓库增长快得多。最近的版本修复了其中几个,但我们预计还会发现更多。
-
历史步行更快。 像提交图这样的元数据可以回答合并基和可达性的问题,而无需每次提交都走动。当代理的 API 调用转化为服务器上的 Git 工作时,这一点尤为重要。
-
避免常见的陷阱。 代理犯的错误和开发者一样。Git 可以捕捉更多常见的,并在它们造成伤害前发出警告。
代理改变了软件的构建方式。GitHub 和 Git 也在跟着变化。我们很高兴能将这个新架构带过起点线。目前已提供有限的私人预览,明年初将有更广泛的推广。我们迫不及待想看到当代理数量受限于他们的想法时,团队会做出什么样的作品。我们会与客户和 Git 社区一起不断发展 Git 和 GitHub。
