GitHub(@github)

Through internal testing, GitHub’s new Git storage architecture supported 5,000 concurrent agents wo...

8.5内容质量

TL;DR · AI 摘要

GitHub新Git存储架构支持5000并发代理和7200次/秒推送,通过解耦存储与计算实现低延迟,为亿级开发者提供基础。

核心要点

  • 新架构支持5000并发代理和7200推送/秒,保持低延迟
  • 通过解耦存储与计算实现基础设施独立扩展
  • 虚拟副本迁移99.99999%仓库至Azure Blob Storage

结构提纲

按章节快速跳转。

  1. GitHub新架构支持5000并发代理和7200推送/秒,为亿级开发者提供基础。

  2. 架构设计遵循最小化协调和解耦存储与计算两大原则。

  3. 迁移99.99999%仓库至Azure,使用虚拟副本实现扩展。

  4. 仅需对引用更新达成共识,使存储、计算、请求服务模块独立扩展。

  5. 预期单仓库写入量将显著增长,驱动架构设计优化。

思维导图

用一张图看清主题之间的关系。

查看大纲文本(无障碍 / 无 JS 友好)
  • GitHub新Git存储架构
    • 核心原则
      • 最小化协调
      • 解耦存储与计算
    • 桥接方案
      • Azure迁移
      • 虚拟副本
    • 扩展能力
      • 独立模块扩展
      • 垂直规模优化

金句 / Highlights

值得收藏与分享的关键句。

#Git#存储架构#GitHub#Azure#高并发
打开原文

GitHub on X: "Inside GitHub’s new Git storage architecture" / X

GitHub

@github

通过内部测试,GitHub的新Git存储架构支持5000个并发代理同时对一个仓库进行操作,每秒向仓库发送7200次推送,同时保持读取流量的低延迟。

$

GitHub新Git存储架构解析

14分钟阅读

作者:Karthik Raman & Brian Celenza

上周在GitHub Universe上,我们宣布了新的Git存储架构,该架构能够每秒处理数千次写入操作,并根据需求扩展读取能力。通过内部测试,我们已经验证了该架构支持5000个并发代理同时对一个仓库进行操作,每秒向仓库发送7200次推送,同时保持读取流量的稳定低延迟。这种新架构是GitHub为数十亿开发者支持智能代理开发的基础,而我们在GitHub持续运行的同时,一直在后台构建这一架构。

过去一年,代理改变了GitHub上繁忙仓库的形态。在本系列的第一篇文章中,我们阐述了下一代Git存储架构的两大原则:最小化协调,以及将存储与计算解耦。在上一篇文章中,我们介绍了桥梁部分:将99.99999%的仓库迁移至Azure,将写入权限范围限定到单个引用,并通过Azure Blob Storage搭建虚拟副本。

我们还注意到一个关键点:写入操作中极少需要顺序处理。Git对象是不可变的,且通过内容命名,这意味着两个写入者可以同时存储对象而无需达成任何共识。推送操作中需要共识的部分仅限于引用更新,而引用更新的规模相对很小。虚拟副本是围绕这一特性构建的第一步,也是我们将仓库迁移至新架构的方式。

架构的其余部分均基于这一观察。只有引用更新需要共识。这使得原本紧密耦合的Git基础设施组件(数据存储、请求处理和仓库维护)能够独立扩展。对于开发者而言,Git保持原样:他们以相同的方式推送、获取和分支,所有保护机制也保持不变。下文我们将深入探讨该架构设计背后的原理,追踪一个请求在系统中的处理流程,并详细解析其中两个核心组件。

为代理扩展性构建

我们从两个维度思考GitHub平台的扩展性:

  • 水平扩展是指跨多个仓库可以支持多少开发者和代理。GitHub当前在这一维度的许多方法都经过多年的实战验证,并延续到新架构中:包括跨基础设施的租户分区、租户流量的隔离和公平性保障、负载感知路由等。
  • 垂直扩展是指单个仓库可以支持多少开发者和代理。随着代理编写更多代码,我们预计单个仓库的规模和活跃度将显著增加,因此大部分需求都来自垂直扩展维度。

今天,GitHub.com上最繁忙的仓库每秒最多接收数十次写入操作,通常以短时爆发的形式出现。随着越来越多的工作转移到并行运行且全天候运行的代理上,我们预计这些爆发将在未来一年内演变为持续的流量速率。每次推送都会带来更多的读取操作:CI运行、部署系统会拉取新提交,其他代理也会为了上下文信息而获取该提交。这种读取扩散效应会导致整体流量随着写入操作呈倍数增长。

我们已经在曲线顶端的仓库中观察到这一趋势。下图显示了某仓库从3月到5月每天的请求量,其流量增长了约4倍。自那时以来,其增长轨迹一直呈指数级上升。

我们必须为一个由代理驱动的开发持续加速的世界做好准备。与其规划下一个增量,我们设定了至少提升两个数量级的扩展目标。对于单个仓库而言,这意味着新架构需要:

  • 每秒处理数千次写入操作。
  • 支持多个写入节点。原因有两个:我们的经验表明,索引传入的包和检查对象连接性是写入过程中最耗费CPU资源的部分,且该成本会随着写入规模增加而增长,因此单个节点会很快达到计算瓶颈。将这些工作分散到多个节点上可以提高容错性,并在节点故障时缩短恢复时间。
  • 最小化提交延迟。代理在每次推送后会等待,然后再继续下一步操作。
  • 按引用顺序记录变更,并能够快速跨引用线性化。落后的虚拟副本需要通过仅下载满足请求所需的数据来追上进度。
  • 在无变化的常见情况下保持读取成本低廉(考虑到读写比例)。
  • 使每个组件能够独立扩展。

除此之外,系统还需要具备提升整体可靠性和全球扩展能力。

介绍“Project Rubicon”

2026年5月,在首次Azure迁移之后,我们立即开始设计能够满足这些需求的方案。我们尝试了多种方法。在所有方案中,包数据都存储在Azure Blob Storage中,其中一些方案还尝试将引用状态也保留在那里。我们将在下文讨论一些发现的权衡取舍,但最终我们选择了我们认为能在保持正确性、持久性、可用性和延迟保证的同时最大化吞吐量的方案。我们将其命名为“Rubicon”,这是对行业在代理驱动开发中跨越转折点的致敬。

让我们从整体架构逐步了解Rubicon。许多部分保持不变:来自Git客户端、API和网页界面的请求仍然会到达Spokes,Spokes仍然负责路由和负载均衡,并为GitHub其他部分提供用于读写仓库数据的内部API。Spokes还继续担任定位和平衡服务,维护水平扩展所需的分区、隔离和租户公平性。与Spokes下方的数据平面相比,这部分是全新的。

Spokes会将每个请求路由到为该仓库分配的一组小型虚拟副本中的一个。虚拟副本是一个单一的临时计算舰队,可以处理读取、写入或两者。虚拟副本的本地磁盘是一个可以随时重建的缓存。

Spokes 在可以的情况下会将仓库的流量路由到相同的虚拟副本,这有助于保持它们的缓存处于热状态并降低延迟,但请求落在特定虚拟副本上与正确性无关。当某个虚拟副本消失时,仓库的其他虚拟副本会继续提供服务,同时新的虚拟副本会随着流量的到来逐步填充其缓存。

当仓库负载增加时,我们会添加更多的虚拟副本。与此同时,一组压缩节点在后台独立运行,直接针对 Blob Storage 和 Ref-CAS,用于合并每个仓库的包并删除不可达的对象。这些操作不会在虚拟副本上执行,这意味着它们的 CPU 和内存可以几乎完全用于处理请求。

持久化状态存储在两个位置:

  • Git 对象以包和辅助读取加速数据的形式存储在 Azure Blob Storage 中。
  • 引用信息和构成仓库的包列表(即包清单)存储在一个独立的复制日志服务中,我们将其称为 Ref-CAS("Reference Compare-and-Swap" 的缩写)。

让我们从写入和读取的视角来分析架构:

  • 写入:虚拟副本会索引传入的包并将其上传到 Blob Storage,然后向 Ref-CAS 提交一个事务,将该包列入清单并更新引用信息。之后,压缩节点会将该包与其他包合并,以保持读取速度。
  • 读取:虚拟副本会向 Ref-CAS 核对引用版本,从 Blob Storage 中将所需的包拉取到本地缓存,然后从缓存中提供读取服务。

在新的架构中,我们采取了多种不同的方法,从虚拟副本上的软件堆栈,到针对不同使用场景(例如许多小型仓库与一个大型仓库)的流量路由和工作负载调度优化。现在让我们深入探讨架构中的两个组件,并围绕两个设计原则讲述它们的故事。

最小化协调:以高吞吐量更新引用状态

像 Azure Blob Storage 这样的对象存储服务在其擅长的领域表现非常出色,是许多数据密集型应用(如源代码存储)的天然选择。它们具有持久性和复制性,存储容量可满足我们的需求,每字节成本相对较低,并且只要流量分布在大量键上,就能提供极高的聚合吞吐量。因此,新 Git 存储架构中几乎所有持久化数据都存储在 Azure Blob Storage 中,包括:

  • 通过推送、压缩和垃圾回收操作写入的包和索引。
  • 我们维护流程从这些包构建的各种读取加速器(如多包索引、可达性位图和一些自定义格式)。
  • 允许压缩和垃圾回收恢复或审计其工作的记录,以及压缩节点用于声明任务的租约。
  • 频繁重复获取请求的缓存响应。
  • 仓库引用状态的检查点,但不包括用于服务请求的权威状态。

这些数据大多数是一次性写入到特定键中,之后不会更改,这正是它们高度适合 Blob Storage 的原因。

引用的情况正好相反。一个繁忙仓库的引用是相对较小的状态片段,许多写入操作会同时竞争更新。这种访问模式难以通过Blob Storage高效处理,我们的初步原型也验证了这一点。对于这种工作负载,对象存储是一个直观的选择,在一定规模范围内表现良好:条件写入可提供免费的比较并交换操作,数据本身已具备持久性和复制性。然而,我们面临了一系列艰难的选择,以下两种尝试方法即为明证。

我们的第一种方法是为仓库的所有引用创建一个预写日志,该日志由单个blob进行索引。每次更新都对该blob执行条件写入。这种方法在正确性上没有问题,读取操作成本低廉:虚拟副本只需确认blob是否已变更,若未变更则直接完成。但写入操作存在瓶颈。由于同一时间只能有一个条件写入在进行,即使我们将写入期间接收到的所有推送合并到下一个写入中,仓库的写入上限仍为每轮单个blob的写入次数。实际测试显示,这种方案的写入吞吐量每秒仅在数百次左右。

第二种方法将引用分散到多个分段blob中,每个引用通过哈希算法映射到对应的分段。不同分段的引用写入不再相互等待,从而提升了写入上限。但读取操作现在需要检查所有分段才能确认是否有变更,性能取决于最慢的分段。我们用写入延迟换取了读取延迟,而读取操作的数量级远超写入。通过并行展示多个写入和一个读取操作,可以直观体现这种差异:

两种方案都无法满足所有需求,因此我们构建了另一种方案:一个专用于引用的复制日志服务,即Ref-CAS服务。这就是我们在上一篇文章结尾提到的那个(小型)共识组,它仍位于技术栈的某处。以下是其工作原理:

Ref-CAS中的每个引用更新都通过多数派确认的比较并交换操作完成。事务仅锁定涉及的引用,验证每个引用的值是否仍符合写入者的预期,当多数日志副本记录该操作后,更新才会生效。由于代理通常只推送自己的分支,大多数更新不会发生冲突,且可并行复制。当两个推送同时更新同一引用时,只有其中一个会将更新追加到日志中。

每个Ref-CAS提交都会获得一个日志序列号(LSN),仓库的版本号即为其最新提交的LSN。读取操作从版本号检查开始。虚拟副本会发送其当前持有的引用状态版本。如果该版本仍有效,响应将不包含任何引用,未变更的仓库只需一次比较即可完成。如果虚拟副本版本滞后,响应将包含当前引用、版本号,以及指向打包数据和其他必要信息的指针(例如最后一次垃圾回收的时间)。

与我们传统架构中的共识组不同,Ref-CAS从不持有或验证对象数据,它仅存储指向这些数据的指针。虚拟副本在提交引用事务前,会先将推送的打包数据上传到Blob Storage。引用永远只指向已持久化的对象。

这些方法使我们能够以每秒数万次操作的速率仅处理引用状态,并为复制日志上的附加注释提供了基准,这些注释可帮助读取操作决定针对特定请求应下载哪些内容。

但每秒在单个仓库上支持数千次写入会带来二阶问题:保持读写性能。为此,我们需要一种不同的压实方法。

解耦存储与计算:每秒数千次写入的压实

每次推送都会向Blob存储添加一个包。Git可以从多个包中提供仓库,但每个额外的包会使查找操作略微更耗时,因此包数量不能无限制增长。在我们的传统架构中,压实在与流量服务相同的副本上运行,并且必须在每个副本上运行。在新架构中,压实完全在后台的独立节点上运行,直接针对持久化存储,并使用Ref-CAS服务通知虚拟副本状态的变化。

压实节点通过租约声明对仓库数据子集的所有权,这使我们能够水平扩展压实。以下是单个压实节点处理仓库包的一个分片的流程:

节点首先在Blob存储中对分片进行租约获取,因此同一时间只有一个节点可以压实一个分片。同时,虚拟副本持续上传包并在包清单中列出它们。节点读取清单以找到新包,并将它们收集到一个开放段中,这实际上是一个即将被压实的数据暂存区。当段达到基于包数量、大小或年龄的定义限制时,节点会密封该段以阻止新包进入,然后将包合并为更少、更大的包。在压实过程中,分片的下一个段可以开始捕获其输入包。在任何内容发布之前,节点会检查新包是否包含与输入完全相同的对象。然后上传新包并将其添加到清单中。第二个事务会从清单中移除输入内容,之后清理操作会稍后删除它们。

为了横向扩展,这个过程可以同时为多个分片独立运行。每个包的哈希映射到一个分片,因此增加分片和节点会提升压实吞吐量。一个经过压实但仍低于大小限制的包会根据其自身哈希重新进入分片并再次合并,因此包会分层增长直到达到限制。最终结果是在推送量增加时,包数量保持基本稳定:

压实会合并包,但保留每个对象。垃圾回收是移除不再被任何引用的对象的步骤,例如被删除分支和强制推送遗留的对象。在我们的新架构中,垃圾回收通过使用专用工作线程处理计算密集型部分,其工作方式与压实非常相似。它还有一些专门的行为以安全删除数据,这些内容值得单独讨论。

通过这些方法,我们能够在写入量增加时持续展示稳定的延迟曲线,并计划了其他优化措施以提升压实和垃圾回收过程本身的性能。

后续计划

我们最初设定的两个原则使该架构的每个部分都能独立扩展。我们通过添加虚拟副本扩展读写操作,Ref-CAS独立地对每个引用进行排序,而非整个仓库。我们通过添加压实节点来扩展维护操作。我们的目标是让每个仓库——从单个开发者的个人项目到最繁忙的单体仓库——都能在相同的架构上运行,无需对繁忙仓库进行特殊处理。仓库不会一直保持小型。一个个人项目可能在一夜之间获得大量代理,而无需我们进行迁移或特殊处理即可保持同步。统一的架构意味着我们所做的每项改进都能立即惠及所有仓库。

这一基础使我们能够实现更多目标。接下来我们将重点关注三个领域:将架构扩展到超出我们当前可预测的需求范围、提升代理在GitHub上执行任务的效率,以及为代理开发优化Git本身。

改进架构

我们计划进一步扩展这一架构。这些改进使仓库在减少每个请求工作量的同时能够处理更多活动:

  • 仅读取所需内容。从Blob存储中获取单个对象,而非包含它的整个包。
  • 按类型组织包。将提交、树和对象分别归类到各自的包中。许多请求因此需要访问的数据量更少。
  • 部分副本。无需本地磁盘上的每个包即可响应请求。
  • 物化视图。以即用形式保存仓库数据,例如预存的归档、默认分支的近实时浅层副本,以及与每个提交一同存储的代理会话上下文。
  • 全局副本。在任意区域部署副本,靠近使用它们的工作负载。读取操作始终处于最新写入的有限滞后范围内。
  • 动态扩展。根据实际活跃的仓库动态扩展或缩减集群规模。

GitHub上更高效的代理操作

基础设施扩展是支持代理的一方面,另一方面是降低每个代理操作的成本:减少需要获取的字节数、减少往返次数,以及减少对变更的等待时间。这些快速跟进的改进都建立在本文中存储工作的基础上:

  • 无需克隆即可工作。许多代理任务根本不需要完整的文件树。它们只需要少量文件和足够的上下文来完成更改。它们可以直接从服务器读取数据。
  • 原子提交API。通过单次操作提交多文件更改。整个更改提交成功,或在冲突时被干净地拒绝。
  • 事件流。代理及其相关系统最适合使用实时信息。通用事件流使它们能够订阅变更并在发生时立即响应。

为代理改进Git

我们在Git本身中也有改进计划。GitHub一直是Git项目的重要贡献者,这一趋势不会停止。我们期待继续推进的一些工作示例包括:

  • 轻量级克隆。部分克隆、浅层克隆和稀疏检出都允许代理获取更少数据。每种方法都有局限性,且彼此之间兼容性不佳。我们希望改进每个组件并使它们更好地协同工作。
  • 消除隐藏的扩展成本。Git中仍存在一些代码路径,其性能下降速度远快于仓库规模的增长。最近的版本修复了其中多个问题,但我们预计还会发现更多。
  • 更快的历史遍历。像提交图这样的元数据可以在不遍历每个提交的情况下回答合并基础和可达性问题。这对代理的API调用转化为服务器上的Git操作时尤为重要。
  • 避开常见陷阱。代理会像开发者一样犯错。Git 能够检测更多常见问题并在造成损害前发出警告。

代理改变了软件构建的方式。GitHub 和 Git 也随之改变。我们很兴奋地将这一新架构推向起跑线。目前该功能正在有限的私有预览中提供,预计明年年初将全面上线。我们迫不及待想看到团队在代理数量受限于想法时能构建出什么。我们将继续与客户和 Git 社区一起推动 Git 和 GitHub 的发展。

由 AI 驱动的开发者平台,用于构建、扩展和交付安全的软件。

更多文章

关注 270 万粉丝

/$

2026 年 11 月 6 日 晚上 10:17

·

71.6K

浏览量

87

127

850

355