跳到正文
原文
Hacker News· Klaster_1·· 4 小时前精选AI 评分70

GitHub 如何以按引用仲裁和对象存储扩展 Git 基础设施

The bridge to agent-scale Git infrastructure

AI 导读

GitHub 已把 99.99999% 仓库的一份副本迁至 Azure,正通过按引用仲裁和对象存储改造 Spokes,以迁移剩余仓库并扩展 Git 读写。将仲裁范围从整个仓库缩小到单个引用后,不同引用的写入可以并行,内部测试显示吞吐提高 3–4x 且延迟更低。读取侧由不加入仲裁的虚拟副本按需扩展,缓存从 Azure Blob Storage 预热,并以对象存储作为事实来源。

推荐理由

GitHub 以按引用仲裁和对象存储解耦读写,解释迁移剩余仓库并独立扩展读写的架构路径与实现机制。

正文 · AI 翻译

我们已将 GitHub 中 99.99999% 的仓库副本迁移到 Azure。以下将介绍逐引用仲裁机制如何迁移其余仓库,以及对象存储如何让读取独立扩展。

October 21, 2026

|

8 分钟

  • 分享:

过去一年里,尽管 Git 流量呈指数级增长,GitHub 上 99.99999% 的 Git 仓库开始同时存在于两个位置:我们自己的数据中心和 Azure。其余仓库是平台上最繁忙的仓库之一,而阻碍它们迁移的只有一件事:25ms 的往返延迟。

在此前发布的博客文章中,我们概述了下一代存储架构背后的两项原则:

  • 尽量减少协调
  • 将存储与计算解耦

本文将介绍旧设计与新设计之间的过渡方案。我们会讨论,改变仲裁机制序列化写入的方式如何让最繁忙的仓库迁入 Azure,以及同一步骤如何为自身不保留任何持久状态的副本创造条件。

迁移历程

此次迁移的时机和方式受到智能体化趋势的推动:流量迅速增长、我们数据中心及周边地理区域的供电容量存在硬性上限,以及实体供应链方面的制约。迁移开始时,即便进行一切可能的扩容,剩余容量也只够让我们安全迁移,但不足以让我们缓慢推进。

虽然我们已经为在云基础设施上扩展 Git 设计了新的架构原型,但在这种增长势头下同时更改存储架构和托管平台,风险过高。鉴于我们的规模,以及该服务对社区至关重要的程度,我们需要谨慎安排工作顺序:先迁移数据,待数据到位后再演进架构。

Git 操作是一项 Tier 0 服务:它是 GitHub 的核心功能,GitHub 上几乎所有内容都依赖仓库数据。这意味着,数据、服务和每项依赖都必须先行迁移。第一个仓库副本于 August 2025 落地 Azure 的 Central US 区域。如今,大多数仓库的五个副本中有两个位于 Azure,Azure 承载着约一半的 Git 流量。在此期间的大部分时间里,Azure 承接了我们的增长,而自有数据中心保持稳定。

迁移期间,流量跟随数据。大多数仓库有五个副本,用于平衡持久性、可用性和规模。为尽量降低读取延迟,Spokes 使用优选路由,将流量导向与请求位于同一区域和可用区的副本。随着副本迁入 Azure,每迁入一个副本,可由 Azure 服务的流量占比都会提高,直到最后一个副本迁入,旧区域便完全停止接收请求。每个仓库都会经历这一过程:

不落下任何仓库

那么,剩下 0.00001% 的仓库怎么办?

在我们当前的架构中,托管在多个区域的仓库具有较低的写入吞吐量上限。Spokes 的仲裁机制只需多数副本即可提交写入,但它会等待每个健康副本作出响应(直至超时)后才继续,因此慢副本会阻塞每次写入,而不是被允许落后。实际上,这意味着最慢的健康副本决定了每次写入的延迟;而每次写入在提交前都会持有覆盖整个仓库的锁,因此它也决定了吞吐量上限。当副本位于目标 Azure 区域后,它距离我们的数据中心约 1,000 英里,使每次网络往返耗时约 25ms,而一次写入需要多次网络往返。一个吞吐量已接近上限的仓库无法在每次串行写入时都承受这种延迟;其落后程度会随着每次推送进一步扩大。

这正是我们的首要原则——尽量减少协调——发挥作用的地方。具体来说,在这个场景中,不要串行化不需要串行化的操作。Git 的一致性要求分别适用于每个引用。服务器一旦接受对某个引用的更新,之后任何客户端都不应再看到较旧的值,只能看到该值或更新的值。无论此次更新是快速前移、强制推送还是删除,这一点都成立。但在大多数情况下,对不同引用的更新互不依赖,可以独立应用。

因此,我们正在将仲裁机制的作用范围从整个仓库调整为单个引用。

在包括网络往返在内的整个引用更新期间持有覆盖整个仓库的锁,会把仲裁机制变成一个单一的无序请求队列,每次只能有一次写入在途。队列中的每次写入耗时都与最慢的健康副本相同。随着写入速率提高,队列也会增长,而每次写入的等待时间都会比前一次更长。

采用按引用串行化后,只有争用同一引用的写入才必须彼此等待。实际操作中,仓库的大多数写入都会触及不同引用——例如,代理和开发者分别推送到自己的分支——因此它们不再相互等待。每次写入仍会经历跨区域延迟,但针对不同引用的写入可以同时在途。这消除了仓库中的单一瓶颈。通过在相似负载下并排比较这两种方式,我们可以直观地看到排队和超时是如何累积的:

这些更改会将仓库的写入吞吐量上限提升至大致与同时写入的引用数量成正比的水平,并使最后剩余的仓库进入 Azure。在使用真实流量进行的内部测试中,对不同引用的写入始终实现了 3–4x 的吞吐量,同时延迟更低。

独立扩展读取能力

大多数仓库都呈现严重的读取偏斜,每有一次写入就有 100 到 1,000 次读取。目前,读取和写入会访问相同的副本,因此它们会争用相同的资源,无法独立扩展。

将存储与计算解耦改变了这一点。一旦仓库数据持久存储在 Azure Blob Storage 中,并在每次写入到达时随之更新,我们就可以开始将读取流量与写入仲裁机制解耦,并分别扩展二者所需的计算资源。例如,与目前相比,读取操作可以受益于大得多的内存缓存,但这些内存同时也用于压实。一旦二者分离,各自就会采用独立的配置。

第一步是让对象存储成为仲裁集的一员,但要以一种特殊形式加入,我们称之为虚拟副本。代表它的计算实例是我们的第一个完全临时节点。它充当通往对象存储的桥梁,流式传输传入的写入,并且不提供读取。由于它是仲裁集的一员,写入只有在对象存储中持久化后才会被确认。

这会给每次写入增加一点延迟,但与跨区域往返一样,按引用串行化仍可防止它成为吞吐量瓶颈。虚拟写入副本还会将针对不同引用的交易合并为发往对象存储的一组网络 I/O,因此繁忙的代码仓库只需为大量并发写入进行一次网络往返,而不必每次写入都往返一次。目前,虚拟写入副本还负责压实,而读取副本会继承压实后的状态。

有了虚拟写入副本,我们便可以根据代码仓库的需求,按需横向扩展任意数量的虚拟读取副本。每个读取副本都会从 Azure Blob Storage 预热缓存,并在不加入仲裁集的情况下开始处理读取流量;它以对象存储为真实数据来源,并在后台据此协调本地状态。

这一步可以显著扩展读取能力。在内部测试中,我们的分布式负载测试框架经常触及当前架构的上限。改用虚拟读取副本后,测试框架反而先触及了自身的极限。这也会带来一些需要后续步骤解决的挑战:

  1. 虚拟读取副本开始时会在后台与对象存储进行协调,因此适合某些读取工作负载,但并非所有工作负载。要处理各种类型的读取,就需要把这种协调转移到读取路径中,只获取该副本上次追平后发生变化的状态和数据。
  2. 虽然我们通过转向按引用的模型逐步提高了写入吞吐量,但仲裁集仍然是进一步提高写入吞吐量的制约因素。

我们如何进一步提升写入吞吐量?

接下来是什么

如今的 GitHub 建立在 Spokes 仲裁机制之上,多年来,Spokes 已经远远不只是仲裁协调器。它还是一层智能路由和负载均衡层,是内部客户端用于以每天超过 1000 亿次操作访问和处理仓库数据的统一 API,也是一组我们称为“清理器”的后台系统,负责仓库放置、修复以及存储节点之间的再平衡。

Spokes 服务范围内的绝大多数能力同样适用于新架构和现有架构。但我们也认识到,正是这套提交协议帮助 GitHub 达到了 10 亿个仓库,但它无法帮助 GitHub 迈向下一个 10 亿乃至更多仓库。

我们还没有完成。有了这些步骤,下一个限制因素就是写入吞吐量。每次写入仍要等待副本,而这些副本各自保存着整个仓库的完整副本,但一次写入实际上只有很小一部分需要定序。Git 对象是不可变的,并且以其内容命名,因此无需任何人先对它们达成一致即可存储。剩余工作量不大,难点在于尽可能快地为其排序,同时又不将瓶颈转移到其他地方。在白板上为此进行设计是一回事,在全天候有开发者和智能体向其写入的十亿个仓库底层实现它,则完全是另一回事。要实现这一目标,我们还不得不重新思考二阶效应,例如持久化引用状态应存放在哪里,以及压实和垃圾回收如何实现规模化。

智能体流量并未放缓。随着这些步骤逐步落地,每一步都会为仓库提供更大的容量余量,无需等到最终状态才见到成效。

下周预告:在 GitHub Universe 以及本系列的最后篇文章中,我们将深入解读这一最终状态、它将如何提升写入吞吐量、为了实现这一目标我们必须解决的二阶问题,以及为什么技术栈中的某处仍然存在一个(小型)共识组。

感谢 Brian Celenza 对本文的贡献。

撰写者

Eleanor White

高级软件工程师

Victoria Dye

资深软件工程师

相关文章

我们也制作新闻简报

在我们的开发者双周刊新闻简报中,了解实用技巧、技术指南和最佳实践。

来源:Hacker News · github.blog