GitHub 公布新架构支持每秒 7200 次推送

一个深夜里,某家科技巨头的测试集群中,两万个代码补全与重构 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 平台的规模从两个方向来思考:

  1. 横向规模是指开发者和代理能跨越多个仓库工作的数量。GitHub 目前在这一方向上的许多方法多年来经过实战验证,并被延续到新架构中:跨基础设施划分租户、租户流量的隔离和/或公平执行,以及负载感知路由等。

  2. 纵向规模是指一个仓库中最多的开发者和代理可以工作。随着代理编写代码的增加,我们预计单个仓库会变得更大更忙,因此我们的大部分需求来自垂直轴。

如今,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 和内存几乎可以完全用于处理请求。

持久状态存在于两个方面:

  1. Git 对象以包和辅助读取加速数据的形式存在于 Azure Blob 存储中。

  2. 引用和构成仓库的包列表(包清单)存在一个独立的复制日志服务中,称为 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。

原文链接:https://www.he6.net/26180.html,转载请注明出处。
0
没有账号?注册  忘记密码?

社交账号快速登录