跳到正文
GitHub Blog · Engineering· Eleanor White·· 2 小时前精选AI 评分60

GitHub 如何为智能体规模的 Git 流量改造存储架构

The bridge to agent-scale Git infrastructure

AI 导读

GitHub 工程团队介绍了把最后 0.00001% 的高负载仓库迁入 Azure 的中间方案:将 Spokes 仲裁的串行化范围从整个仓库缩小到单个引用,使不同引用的写入可以并发进行。内部测试中,对不同引用的写入吞吐提升 3–4 倍且延迟更低。团队还引入不保存持久状态的虚拟写入副本,把对象存储纳入仲裁,并可按需扩展虚拟读副本。

推荐理由

GitHub 工程团队拆解了把最繁忙仓库迁入 Azure 时遇到的写入瓶颈,以及按引用粒度串行化带来的吞吐变化。

正文 · AI 翻译

在过去一年里,随着 Git 流量呈指数级增长,GitHub 上 99.99999% 的 Git 仓库开始同时存在于两个地方:我们自己的数据中心和 Azure。其余的仓库属于平台上最繁忙的一批,而它们只被一件事拖住了脚步:25ms 的往返延迟。

在之前的一篇博客文章中,我们阐述了下一代存储架构背后的两项原则:

  • 最小化协调
  • 将存储与计算解耦

在这篇文章中,我们将探讨旧设计与新设计之间的直接过渡方案。我们将讨论,对我们 quorum 序列化写入方式的改动,如何让最繁忙的仓库进入 Azure,以及这同一步骤如何为不保留自身持久状态的副本打开大门。

我们是如何走到这一步的

这次迁移的时机和形态由智能体化转变所驱动:流量快速增长、我们数据中心及周边地理区域的电力容量存在硬性上限,以及物理供应链的约束。在迁移开始时,即便算上所有可能的扩展,剩余容量也只够我们安全地完成迁移,而不够我们慢慢来做。

虽然我们已经为在云基础设施上扩展 Git 原型化了新的架构方案,但在那样的增长态势下,同时更改存储架构和托管平台会带来太大风险。考虑到我们的规模以及该服务对社区的重要性,我们需要谨慎地安排工作顺序:先迁移数据,等数据到位后再演进架构。

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

在迁移期间,流量跟随数据。大多数仓库有五个副本,以平衡持久性、可用性和规模。为最小化读取延迟,Spokes 使用优先路由将流量引导至与请求处于同一区域和可用区的副本。随着副本迁移到 Azure,可从 Azure 提供服务的流量份额随每个副本的增加而增长,直到最后一个副本到达,旧区域便完全停止接收请求。每个仓库都要经历这一过程:

不落下任何一个仓库

那么,剩下的 0.00001% 仓库呢?

在我们当前的架构中,托管在多个区域的仓库具有较低的写入吞吐量上限。Spokes 的 quorum 只需多数派即可提交写入,但它在继续之前会等待每个健康副本响应(直到超时),因此慢副本会拖慢每一次写入,而不是落后。在实践中,这意味着最慢的健康副本决定了每次写入的延迟,并且由于每次写入在提交前都持有仓库级锁,它也决定了吞吐量上限。一旦某个副本位于目标 Azure 区域,它距离我们的数据中心约 1,000 英里,使得每次网络往返约为 25ms,而一次写入需要多次往返。一个已经接近吞吐量上限的仓库无法在每次序列化写入时承受这一开销;它会在每次推送时进一步落后。

这正是我们的首要原则——最小化协调——发挥作用的地方。具体到本例:不要将不需要串行化的操作串行化。Git 的一致性要求分别适用于每个引用。一旦服务器接受了对某个引用的更新,客户端之后就不应再看到旧值,只能看到该值或更新的值。无论更新是快进、强制推送还是删除,这一点都成立。但在大多数情况下,对不同引用的更新互不依赖,可以独立应用。

因此,我们正在将法定人数的范围从整个仓库改为单个引用。

在整个引用更新过程中(包括往返通信)持有仓库级锁,会把法定人数变成单一的、无序的请求队列,一次只有一个写入在进行中。该队列中的每个写入耗时与最慢的健康副本一样长。随着写入速率增长,队列也会增长,每个写入等待的时间都比前一个更长。

采用按引用串行化后,只有争用同一引用的写入才需要相互等待。在实践中,对仓库的大多数写入涉及不同的引用——例如,代理和开发者推送到各自的分支——因此它们不再相互等待。每个写入仍会经历跨区域延迟,但对不同引用的写入可以同时进行。这消除了仓库上的单一瓶颈。我们可以通过在相似负载下并排比较两种方法,来直观地了解排队和超时是如何累积的:

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

独立扩展读取

大多数仓库严重偏向读取,每 1 次写入对应大约 100 到 1,000 次读取。目前,读取和写入命中相同的副本,因此它们竞争相同的资源,无法独立扩展。

将存储与计算解耦改变了这一点。一旦仓库数据持久存储在 Azure Blob Storage 中并在每次传入写入时更新,我们就可以开始将读取流量与写入法定人数解耦,并独立扩展各自所需的计算资源。例如,读取将受益于比目前提供给它们的内存缓存多得多的缓存,但这些内存也是压缩所需的。一旦分离,各自获得自己的配置。

第一步是让对象存储成为法定人数的一员,但这是一个特殊的成员,我们称之为虚拟副本。代表它的计算资源是我们的第一个完全临时性节点。它充当通往对象存储的桥梁,流式处理传入写入,不提供任何读取服务。由于它是法定人数的一员,写入只有在对象存储中持久化后才会被确认。

这会给每个写入增加一点延迟,但就像跨区域往返一样,按引用串行化可防止它成为吞吐量瓶颈。虚拟写入副本还会将不相交引用上的事务合并为对对象存储的单组网络 I/O,因此繁忙的仓库只需为许多并发写入支付一次往返,而不是每次写入支付一次。目前,虚拟写入副本还负责压缩,读取副本继承压缩后的状态。

有了虚拟写副本,我们就可以按需为一个仓库扩展出任意数量的虚拟读副本。每个读副本从 Azure Blob Storage 预热缓存,并在不加入法定人数的情况下开始承接读流量,以对象存储作为事实来源,并在后台将本地状态与其对账。

这一步释放了显著的读扩展能力。在内部测试中,我们的分布式负载测试工具经常在当前架构中触及上限。而面对虚拟读副本时,该工具反而先触及了自身的极限。它也带来了一些挑战,这些挑战将在后续步骤中解决:

  1. 虚拟读副本一开始会在后台与对象存储进行对账,这使它们适合某些读工作负载,但并非全部。要服务所有类型的读请求,就需要把这种对账移到在线路径上,只获取自副本上次追上以来发生变化的引用状态和数据。
  2. 虽然我们通过转向按引用模型逐步提高了写吞吐量,但法定人数仍然是进一步提高的制约因素。

我们如何进一步推高写吞吐量?

接下来是什么

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

Spokes 服务伞下绝大多数内容对新架构的适用性与对现有架构一样好。但我们也认识到,将 GitHub 带到十亿仓库的提交协议,无法将它带到下一个十亿乃至更远。

我们还没有完成。随着这些步骤就位,下一个限制是写吞吐量。每次写入仍然要等待各自持有仓库完整副本的副本,而一次写入中真正需要排序的部分其实很少。Git 对象是不可变的,并以其内容命名,因此可以在无人对其达成一致的情况下存储。剩下的部分很小,而难点在于尽可能快地对其排序,同时不把瓶颈转移到别处。在白板上为此做设计是一回事。在开发者与智能体全天候写入的十亿仓库之下付诸实践则是另一回事。要达到那里,也迫使我们重新思考二阶效应,比如持久引用状态存放在哪里,以及压实和垃圾回收如何扩展。

智能体流量并未放缓,而上述每一步在落地时都会为仓库带来更多余量,无需等待最终状态。

下周预告:在 GitHub Universe 以及本系列的最后一篇文章中,我们将带您一览那个最终状态、它对写吞吐量的影响、我们为达到那里必须解决的二阶问题,以及为什么技术栈中某处仍然存在一个(小型的)共识组。

感谢 Brian Celenza 对本文的贡献。

文章 通往智能体规模 Git 基础设施的桥梁 首次出现在 The GitHub Blog。

来源:GitHub Blog · Engineering · github.blog