# The bridge to agent-scale Git infrastructure
**作者**: GitHub
**日期**: 2026-10-21T18:50:12.000Z
**来源**: [https://x.com/github/status/2112979782652813783](https://x.com/github/status/2112979782652813783)
---

By Eleanor White & Victoria Dye
Over the past year, while Git traffic grew exponentially, 99.99999% of the Git repositories on GitHub began living in two places at once: our own data centers and Azure. The rest are among the busiest on the platform, and they’re held back by just one thing: a 25ms round trip.
In an earlier blog post, we laid out the two principles behind our next-generation storage architecture:
- Minimize coordination
- Decouple storage from compute
In this post, we’ll look at the immediate bridge between our old design and our new one. We’ll talk about how a change to the way our quorum serializes writes gets the busiest repositories into Azure, and how that same step opens the door to replicas that keep no persistent state of their own.
## How we got here
The timing and shape of the move were driven by the agentic shift: rapidly growing traffic, a hard ceiling on the power capacity of our data centers and the surrounding geographical region, and physical supply chain constraints. At the beginning of our migration, the remaining capacity with all possible expansion gave us enough time to migrate safely, but not enough to do it slowly.
While we had prototyped new architectural approaches for scaling Git on cloud infrastructure, changing the storage architecture and the hosting platform at the same time, under that growth, carried too much risk. Given our scale and how critical the service is to the community, we needed to sequence the work carefully: move the data first, then evolve the architecture once it arrived.
Git operations are a Tier 0 service: they’re a core function of GitHub, and nearly everything in GitHub depends on the repository data. That meant the data, services, and every dependency had to move first. The first copy of a repository landed in Azure’s Central US region in August 2025. Today, most repositories have two of their five replicas in Azure, and Azure serves about half of Git traffic. For most of that time, Azure absorbed our growth while our own data centers held steady.
During the migration, traffic follows the data. Most repositories have five replicas to balance durability, availability, and scale. To minimize read latency, Spokes uses preferential routing to direct traffic to a replica in the same region and availability zone as the request. As replicas move to Azure, the share of the traffic that can be served from Azure grows with each one, until the last replica arrives and the old region stops receiving requests entirely. Every repository goes through this process:
<video controls preload="metadata" poster="/doc/8708/image/img_9cb0804ffcc9.jpg" src="/videos/1682.mp4"></video>
## No repository left behind
So, what about the remaining 0.00001% of repositories?
In our current architecture, a repository hosted in more than one region has a lower write throughput ceiling. Spokes’ quorum only needs a majority to commit a write, but it waits for every healthy replica to respond (up to a timeout) before moving on, so slow replicas delay every write rather than falling behind. In practice, that means the slowest healthy replica sets the latency of every write, and because each write holds a repository-wide lock until it commits, it also sets the throughput ceiling. Once a replica lives in the target Azure region, it’s about 1,000 miles from our data centers, making each network round trip around 25ms, and a write requires multiple round trips. A repository already near its throughput ceiling can’t absorb that on every serialized write; it would fall further behind with every push.
This is where our first principle, minimizing coordination, comes into play. Specifically for this case: don’t serialize what doesn’t need to be. Git’s consistency requirements apply to each reference individually. Once the server accepts an update to a reference, no client should see an older value afterward, only that value or a newer one. That holds whether the update is a fast-forward, a force push, or a delete. But in most cases, updates to different references don’t depend on each other and can be applied independently.
So, we’re changing the scope of the quorum from the whole repository to individual references.
Holding a repository-wide lock for the entire reference update, round trips included, turns the quorum into a single, unordered request queue with one write in flight at a time. Each write in that queue takes as long as the slowest healthy replica. As the write rate grows, so does the queue, and each write waits longer than the one before it.
With per-reference serialization, only writes that contend on a shared reference must wait for each other. In practice, most writes to a repository touch different references—for example, agents and developers pushing to their own branches—so they no longer wait on each other. Each write still experiences the cross-region latency but writes to different references can be in flight at the same time. That removes the single bottleneck on the repository. We can visualize how queueing and timeouts build up by comparing the two approaches side by side under similar load:
<video controls preload="metadata" poster="/doc/8708/image/img_08d9c32e7ee4.jpg" src="/videos/1683.mp4"></video>
These changes raise a repository’s write throughput ceiling roughly in proportion to how many references are being written at once, and get the last remaining repositories into Azure. In internal testing with realistic traffic, writes to different references consistently saw 3–4x higher throughput at lower latency.
## Scaling reads on their own
Most repositories are heavily read-skewed, with somewhere between 100 and 1,000 reads for every write. Today, reads and writes hit the same replicas, so they compete for the same resources and can’t be scaled independently.
Decoupling storage from compute changes that. Once repository data is durably stored in Azure Blob Storage and updated with each incoming write, we can begin to decouple read traffic from the write quorum, and independently scale the compute needed for each. For example, reads would benefit from far more in-memory cache than we give them today, but that memory is also needed for compaction. Once they’re separated, each gets its own configuration.
The first step is making object storage a member of the quorum, but a special one that we call a virtual replica. The compute that represents it is our first fully ephemeral node. It serves as a bridge to object storage, streaming incoming writes and serving no reads. Because it’s a member of the quorum, a write isn’t acknowledged until it’s durable in object storage.
That adds a little latency to each write, but just like the cross-region round trip, per-reference serialization keeps it from becoming a throughput bottleneck. The virtual write replica also coalesces transactions on disjoint references into a single set of network I/Os to object storage, so a busy repository pays for one round trip across many concurrent writes instead of one per write. For now, the virtual write replica also handles compaction, and read replicas inherit the compacted state.
With the virtual write replica in place, we can scale out as many virtual read replicas as a repository needs, on demand. Each read replica warms its cache from Azure Blob Storage and starts taking read traffic without joining the quorum, using object storage as the source of truth and reconciling its local state against it in the background.
This step unlocks significant read scale. In internal testing, our distributed load testing harness routinely found ceilings in the current architecture. Against virtual read replicas, the harness hit its own limits first. It also comes with a few challenges that are solved in subsequent steps:
1. Virtual read replicas start out reconciling with object storage in the background, which makes them a good fit for some read workloads but not all of them. Serving every kind of read requires moving that reconciliation in-line, fetching only the reference state and data that changed since the replica last caught up.
2. While we’ve incrementally increased write throughput by shifting to a per-reference model, the quorum remains the bounding factor for increasing further.
How can we push write throughput further?
## What comes next
The GitHub of today was built on the Spokes quorum, and over the years Spokes has become much more than a quorum coordinator. It’s also an intelligent routing and load balancing layer, a unified API that internal clients use to access and transact repository data at well over 100 billion operations per day, and a collection of background systems we call “sweepers” that handle repository placement, repair, and rebalancing between storage nodes.
The vast majority of what lives under the Spokes service umbrella applies just as well to the new architecture as it does to the existing one. But we also recognize that the commit protocol that has taken GitHub to a billion repositories won’t take it to the next billion and beyond.
We’re not done yet. With these steps in place, the next limit is write throughput. Every write still waits on replicas that each hold a full copy of the repository, yet very little of a write actually needs to be ordered. Git objects are immutable and named by their contents, so they can be stored without anyone agreeing on them. What’s left is small, and the hard part is ordering it as fast as possible without moving the bottleneck somewhere else. Designing for that on a whiteboard is one thing. Doing it underneath a billion repositories that developers and agents are writing to around the clock is another. Getting there forced us to rethink the second-order effects too, like where durable reference state lives and how compaction and garbage collection scale.
Agent traffic isn’t slowing down, and each of these steps gives repositories more headroom as it lands, without waiting for the end state.
Coming next week: At GitHub Universe and in the final post in this series, we’ll take a tour through that end state, what it does for write throughput, the second-order problems we had to solve to get there, and why there’s still a (small) consensus group somewhere in the stack.
Thanks to Brian Celenza for their contributions to this post.
## 相关链接
- [GitHub](https://x.com/github)
- [@github](https://x.com/github)
- [143K](https://x.com/github/status/2112979782652813783/analytics)
- [earlier blog post](https://github.blog/engineering/architecture-optimization/building-git-infrastructure-for-agent-scale-development/)
- [Upgrade to Plus](https://x.com/i/premium_sign_up)
- [2:50 AM · Oct 22, 2026](https://x.com/github/status/2112979782652813783)
- [143.3K Views](https://x.com/github/status/2112979782652813783/analytics)
- [View quotes](https://x.com/github/status/2112979782652813783/quotes)
---
*导出时间: 2026/10/23 16:57:29*
---
## 中文翻译
# 迈向代理级 Git 基础设施的桥梁
**作者**: GitHub
**日期**: 2026-10-21T18:50:12.000Z
**来源**: [https://x.com/github/status/2112979782652813783](https://x.com/github/status/2112979782652813783)
**阅读**: 143357
**点赞**: 529
**转发**: 71
**回复**: 64
---

作者:Eleanor White 与 Victoria Dye
过去一年里,随着 Git 流量呈指数级增长,GitHub 上 99.99999% 的 Git 仓库开始同时存在于两个地方:我们自己的数据中心和 Azure。其余的仓库则是平台上最繁忙的一批,而它们只被一件事所限制:25 毫秒的网络往返延迟。
在早前的一篇博文中,我们阐述了下一代存储架构背后的两条原则:
- 最大限度减少协调
- 将存储与计算解耦
在本文中,我们将探讨连接旧设计与新设计之间的直接桥梁。我们会讨论如何通过改变仲裁机制序列化写入的方式,让最繁忙的仓库迁移到 Azure,以及这一步如何为不保留任何自身持久状态的副本铺平道路。
## 我们是如何走到这一步的
迁移的时机和方式是由代理化转型驱动的:流量快速增长、我们数据中心及周边地理区域的电力容量存在硬性上限,以及物理供应链的限制。在迁移开始时,所有可能的扩容手段所剩余的容量给了我们足够的时间安全完成迁移,但不允许我们慢慢来。
虽然我们已经为在云基础设施上扩展 Git 原型化了新的架构方案,但在那样的增长速度下,同时改变存储架构和托管平台风险太大。鉴于我们的规模以及该服务对社区的重要性,我们需要谨慎安排工作顺序:先迁移数据,等数据到位后再演进架构。
Git 操作是 Tier 0 服务:它是 GitHub 的核心功能,GitHub 中几乎所有内容都依赖于仓库数据。这意味着数据、服务以及每一个依赖项都必须先迁移。第一份仓库副本于 2025 年 8 月落地于 Azure 的美国中部区域。如今,大多数仓库的五个副本中有两个位于 Azure,Azure 承载了大约一半的 Git 流量。在那段时间的大部分时间里,Azure 吸收了我们的增长,而我们自己的数据中心则保持稳定。
迁移期间,流量跟随数据。大多数仓库有五个副本,以平衡持久性、可用性和规模。为了最小化读取延迟,Spokes 使用优先路由,将流量引导至与请求处于同一区域和可用性区的副本。随着副本迁移到 Azure,可由 Azure 服务的流量份额随之增长,直到最后一个副本就位,旧区域完全停止接收请求。每个仓库都会经历这个过程:
<video controls preload="metadata" poster="/doc/8708/image/img_9cb0804ffcc9.jpg" src="/videos/1682.mp4"></video>
## 不落下任何一个仓库
那么,剩下那 0.00001% 的仓库怎么办?
在我们当前的架构中,托管在多个区域的仓库具有较低的写入吞吐量上限。Spokes 的仲裁机制只需要多数派即可提交一次写入,但它会等待每个健康副本响应(直至超时)后才继续,因此缓慢的副本会延迟每一次写入,而不是自己落后。实际上,这意味着最慢的健康副本决定了每次写入的延迟,而且由于每次写入在提交之前都会持有一个仓库级别的锁,它也决定了吞吐量上限。一旦副本位于目标 Azure 区域,它距离我们的数据中心约 1,000 英里,每次网络往返约 25 毫秒,而一次写入需要多次往返。一个已经接近吞吐量上限的仓库无法在每次序列化写入时都承受这种开销;否则每次推送都会使它进一步落后。
这就是我们的第一条原则——最大限度减少协调——发挥作用的地方。具体到这个场景:不需要序列化的内容就不要序列化。Git 的一致性要求是针对每个引用单独适用的。一旦服务器接受了对某个引用的更新,之后任何客户端都不应看到更旧的值,只能看到该值或更新的值。无论这次更新是快进推送、强制推送还是删除,这一点都成立。但在大多数情况下,对不同引用的更新彼此不依赖,可以独立应用。
因此,我们将仲裁的范围从整个仓库改为单个引用。
在整个引用更新期间(包括网络往返)持有仓库级别的锁,会把仲裁机制变成一个单一的、无序的请求队列,同一时间只有一次写入在进行。该队列中的每次写入的耗时取决于最慢的健康副本。随着写入速率的增长,队列也在增长,每次写入的等待时间都比前一次更长。
采用按引用序列化后,只有在共享引用上发生竞争的写入才需要相互等待。实际上,对同一仓库的大多数写入都触及不同的引用——例如代理和开发者推送各自的分支——因此它们不再需要相互等待。每次写入仍然会经历跨区域延迟,但针对不同引用的写入可以同时进行。这消除了仓库上的单一瓶颈。通过在类似负载下并排对比这两种方式,我们可以直观地看到排队和超时是如何累积的:
<video controls preload="metadata" poster="/doc/8708/image/img_08d9c32e7ee4.jpg" src="/videos/1683.mp4"></video>
这些改动使仓库的写入吞吐量上限大致按照同时被写入的引用数量成比例提升,并让最后剩下的仓库得以进入 Azure。在内部使用真实流量进行的测试中,对不同引用的写入在更低延迟下始终获得了 3–4 倍的吞吐量。
## 独立扩展读取
大多数仓库的读取远多于写入,每次写入对应大约 100 到 1,000 次读取。目前,读取和写入访问相同的副本,因此它们竞争相同的资源,无法独立扩展。
将存储与计算解耦改变了这一点。一旦仓库数据持久地存储在 Azure Blob Storage 中并随每次传入的写入而更新,我们就可以开始将读取流量从写入仲裁中解耦出来,并独立扩展各自所需的计算资源。例如,读取操作可以受益于远超如今所配置的内存缓存,但那部分内存同时也是压缩操作所需要的。一旦两者分离,各自就可以拥有自己的配置。
第一步是让对象存储成为仲裁成员之一,但是一种特殊的成员,我们称之为虚拟副本。代表它的计算节点是我们第一个完全临时性的节点。它充当通往对象存储的桥梁,流式处理传入的写入,不提供任何读取服务。由于它是仲裁成员,一次写入只有在对象存储中持久化之后才会被确认。
这为每次写入增加了一点延迟,但正如跨区域往返一样,按引用序列化使其不会成为吞吐量瓶颈。虚拟写入副本还会将针对互不相交引用的事务合并为一组对对象存储的网络 I/O,因此繁忙的仓库可以为多次并发写入支付一次往返的成本,而不是每次写入一次往返。目前,虚拟写入副本还负责压缩,而读取副本继承压缩后的状态。
有了虚拟写入副本之后,我们可以根据需要为仓库扩展任意数量的虚拟读取副本。每个读取副本从 Azure Blob Storage 预热其缓存,无需加入仲裁即可开始承接读取流量,以对象存储为事实来源,并在后台将本地状态与其对账。
这一步解锁了巨大的读取扩展能力。在内部测试中,我们的分布式负载测试工具经常能在当前架构中找到上限。而面对虚拟读取副本时,测试工具先达到了自身的极限。这同时也带来了一些挑战,将在后续步骤中解决:
1. 虚拟读取副本最初在后台与对象存储进行对账,这使它们适合某些读取工作负载,但并非全部。要服务所有类型的读取,需要将对账过程改为内联进行,只获取自副本上次追上以来发生变化的引用状态和数据。
2. 虽然我们通过转向按引用模型逐步提高了写入吞吐量,但仲裁机制仍然是进一步提升的限制因素。
我们如何进一步提高写入吞吐量?
## 接下来是什么
今天的 GitHub 构建在 Spokes 仲裁机制之上,多年来 Spokes 已经远远不只是一个仲裁协调器。它还是一个智能路由和负载均衡层,一个内部客户端用于访问和交易仓库数据的统一 API(每日操作量远超 1000 亿次),以及一系列我们称为“清扫器”(sweeper)的后台系统,负责仓库的放置、修复以及在存储节点之间的重新平衡。
Spokes 服务体系下绝大部分内容对新架构和现有架构同样适用。但我们也认识到,将 GitHub 带到十亿仓库规模的提交协议,无法将其带到下一个十亿乃至更远的未来。
我们还没有完成。有了这些步骤,下一个限制就是写入吞吐量。每次写入仍然要等待各自持有完整仓库副本的副本,但实际上写入中需要排序的部分很少。Git 对象是不可变的,并以内容命名,因此无需任何人达成一致就可以存储它们。剩下的部分很小,难点在于以尽可能快的速度对它们排序,同时不把瓶颈转移到别处。在白板上设计是一回事,在十亿个开发者与代理全天候写入的仓库之下实现则是另一回事。为此我们不得不重新思考二阶效应,例如持久化的引用状态应该存放在哪里,以及压缩和垃圾回收如何扩展。
代理流量并未放缓,而每一步的落地都为仓库提供了更多余量,无需等待最终形态的到来。
下周预告:在 GitHub Universe 大会以及本系列的最后一篇文章中,我们将带您一览那个最终形态——它为写入吞吐量带来了什么,我们为达成它而必须解决的二阶问题,以及为什么技术栈中某处仍然存在一个(很小的)共识组。
感谢 Brian Celenza 对本文的贡献。
## 相关链接
- [GitHub](https://x.com/github)
- [@github](https://x.com/github)
- [143K](https://x.com/github/status/2112979782652813783/analytics)
- [earlier blog post](https://github.blog/engineering/architecture-optimization/building-git-infrastructure-for-agent-scale-development/)
- [Upgrade to Plus](https://x.com/i/premium_sign_up)
- [2:50 AM · Oct 22, 2026](https://x.com/github/status/2112979782652813783)
- [143.3K Views](https://x.com/github/status/2112979782652813783/analytics)
- [View quotes](https://x.com/github/status/2112979782652813783/quotes)
---
*导出时间: 2026/10/23 16:57:29*