Git 流量在过去一年呈指数级增长,但 GitHub 上 99.99999% 的仓库同时存在于自家数据中心和 Azure 两个地方。剩下那 0.00001% 是最繁忙的仓库,它们被一个看似微不足道的东西卡住了:一次 25ms 的网络往返。

这 25ms 从哪来?当仓库的一个副本迁移到 Azure 区域后,它离 GitHub 自家数据中心大约 1000 英里,每次网络往返约 25ms。而一次写入需要多次往返。问题在于,GitHub 的 Spokes 存储架构里,quorum(写多数派,即一次写入需要获得多数副本确认才算成功)虽然只需要多数派确认就能提交写入,但会等待所有健康副本响应(直到超时)才继续。这意味着最慢的健康副本决定了每次写入的延迟,而且因为每次写入在提交前持有整个仓库的锁,它也决定了吞吐量上限。

一个已经接近吞吐上限的仓库,无法承受每次串行写入都多出这 25ms 的代价——每次 push 都会让它落后更多。

GitHub 的解法是改变 quorum 的粒度:从整个仓库级别细化为单个引用(reference,即分支、标签等指向某个提交的指针)级别。Git 的一致性要求只针对每个引用单独成立:一旦服务器接受了对某个引用的更新,任何客户端之后都不应看到旧值。但大多数情况下,对不同引用的更新互不依赖,可以独立应用。

这个改动带来的效果很直观:原来仓库级锁把 quorum 变成了一个无序请求队列,同一时间只有一个写入在途,每个写入耗时等于最慢健康副本的耗时。写入速率增长,队列就变长,每个写入等待时间比前一个更长。改为按引用串行化后,只有争用同一引用的写入才需要互相等待。实际上,大多数写入触及的是不同引用——比如 agent 和开发者各自 push 到自己的分支——它们不再互相等待。每个写入仍然经历跨区域延迟,但不同引用的写入可以同时在途,仓库的单一瓶颈被消除了。

内部测试中,在真实流量下,对不同引用的写入持续获得 3-4 倍的吞吐提升,且延迟更低。

解决了写入瓶颈,GitHub 接着处理读扩展。大多数仓库读严重倾斜,读写比在 100:1 到 1000:1 之间。当前架构下读和写命中同一批副本,竞争同一批资源,无法独立扩展。解耦存储与计算(GitHub 下一代存储架构的两大原则之一:最小化协调、存储与计算解耦)改变了这一点。

第一步是让对象存储成为 quorum 的一员,但是一个特殊成员——虚拟副本(virtual replica)。它代表的计算节点是第一个完全无状态的临时节点,充当通往对象存储的桥梁,流式接收写入但不服务读取。因为它是 quorum 成员,写入直到在对象存储中持久化后才被确认。这给每次写入增加了一点延迟,但和跨区域往返一样,按引用串行化让它不会成为吞吐瓶颈。虚拟写入副本还把对不同引用的并发事务合并成对对象存储的一组网络 I/O,繁忙仓库为多次并发写入只付一次往返的代价。

有了虚拟写入副本,就可以按需扩展任意数量的虚拟读副本。每个读副本从 Azure Blob Storage 预热缓存,开始接收读流量而不加入 quorum,以对象存储为数据源,在后台与本地状态对账。内部测试中,分布式负载测试工具在现有架构下总能找到上限,但面对虚拟读副本时,工具先达到了自己的极限。

这个方案也有挑战:虚拟读副本启动时在后台与对象存储对账,适合部分读负载但不适合全部。服务所有类型的读需要把对账过程内联化,只获取副本上次追赶后变更的引用状态和数据。

这个思路能直接用到哪些场景?如果你的系统有跨地域多活需求,并且写入吞吐受限于跨区域同步延迟,把锁粒度从全局细化到独立实体级别(类似从仓库级到引用级)是立竿见影的优化。另一个可借鉴的点是:把存储与计算解耦后,用无状态临时节点作为存储层与计算层之间的桥梁,让读扩展不再受写入 quorum 约束。要注意的坑是:按引用串行化只适用于不同实体间无依赖的场景,如果业务上存在跨实体的强一致需求,这个方案就不适用;虚拟读副本的后台对账模式也不适合所有读负载,需要评估你的读模式是否容忍短暂的不一致。

阅读原文 → 返回 AI 技术文档

内容与图片版权归原作者所有 · 原文: https://github.blog/engineering/architecture-optimization/the-bridge-to-agent-scale-git-infrastructure/