OpenAI 如何扩展 Git规模

OpenAI Blog 工具

摘要

OpenAI 为其 monorepo 构建了 Pando,一个全新的水平可扩展 Git 后端,以应对大规模的 agent 驱动开发,将中位推送延迟降低了近 5 倍,每天服务接近 1PB 的 Git 数据,同时保持与 Git 协议的兼容性。

OpenAI 如何构建 Pando——一个为 Codex 所带来的开发规模而设计的 Git 兼容后端。
查看原文
查看缓存全文

缓存时间: 2026/11/03 00:40

# 在 OpenAI 扩展 Git 的规模 来源:https://openai.com/index/scaling-git-at-openai/ 过去一年里,我们 monorepo 的规模和需求以惊人的速度增长。显然,我们需要改造源代码控制系统,使其能够支撑 Codex 所带来的大规模并发工作。 我们在不中断任何工程工作、也不改变团队使用 Git 方式的情况下,构建了 Pando——一个全新的 Git 仓库后端。Pando 现已驱动 OpenAI monorepo 中的所有开发工作。Pando 的设计旨在实现两个雄心勃勃的目标:第一,水平可扩展的写入,针对向大量分支的并发推送进行优化;第二,最大化读取吞吐量,让客户端能够足够快地下载数据,从而充分压满其可用网络带宽。 我们在保持与 Git 协议及客户端兼容的前提下实现了这些目标,使工程师和智能体都能无缝地将 Pando 融入现有工作流。在生产环境中,Pando 将推送延迟的中位数降低了近 5 倍。如今,它每天处理近 1 PB 的 Git 数据。 本文将介绍我们构建新源代码控制系统的动机,以及为何选择保留与 Git 的兼容性。我们会将我们的系统与常见的 Git 扩展方案进行对比,说明 Pando 为何能够独特地满足 OpenAI 的可扩展性需求。我们还会讨论下一步计划,以及如何继续为 Git 做出贡献,以支持智能体规模的工作负载。 ## 为什么我们需要新的源代码控制系统 Codex 改变了 OpenAI 的开发规模和节奏,使工程师能够并行完成多得多的工作。这意味着更多的分支和推送、更多的合并,以及来自开发环境、研究任务和 CI 的更多 fetch 和 clone。 这些需求会相互叠加。如今一位工程师可能同时有多个改动等待 CI 处理,而每次推送都可能触发多个作业去拉取同一个 commit。 大约从去年年底开始,支撑我们 monorepo 的系统开始力不从心。fetch 和推送变得缓慢,合并时有失败,可靠性一度跌到 90% 以下。我们到达的,究竟是 Git 本身的极限,还是我们服务 monorepo 方式的极限?Git 诞生于一个通过邮件向内核发送补丁的社区。二十多年来,随着开发者构建软件的方式不断演变,他们一直围绕 Git 来调整自己的工具。当我们自己的 monorepo 开始感到摩擦时,能够继续在前人的这项工作之上构建,对我们来说很重要。 我们看到了一条路:在保留熟悉工作流的同时重建后端,使其能够并行处理对不同分支的推送,并让多个读者并发地获取同一份历史。以 Git 作为源代码控制工作流的基石,意味着对客户端的改进可以惠及 OpenAI 之外的用户。我们希望将这些改进从我们的 Git 副本(opens in a new window)(https://github.com/openai/git)贡献给上游,与更广泛的社区共享。 ### 为大规模 Git 而工程 扩展源代码控制系统有两个基本维度:许多写入者向不同分支推送,以及许多读者拉取同一份历史。Pando 对两者都进行了优化。 首先,Pando 对每个分支的更新独立进行协调。对不相关分支的更新可以并行推进,拉取某个分支的读者也能获得该版本所需的历史。代价在于,同时查看多个分支的读者可能看到它们处于不同的时间点。 其次,Pando 在多次 fetch 之间复用 packfile(Git 将多个对象压缩在一起的格式),而不是为每个客户端生成单独的 packfile。客户端可能会下载一些它已经拥有的对象,但服务器可以用很少的计算量,向许多客户端提供相同的字节。 这些选择使我们能够在分支之间横向扩展写入,并通过缓存来服务读取。要理解它们如何协同工作,我们先从 Git 如何存储和传输历史讲起。 ### Git 对象存储入门 Git 必须能够保持仓库的两个部分同步:*对象(objects)* 和 *引用(references)*。 Git 仓库将文件的快照以及连接它们的历史记录,记录为一张对象图。一个对象可以是 blob、tree、commit 或 tag。blob 保存单个文件的内容,tree 将名称与 blob 或其他 tree 关联起来,形成目录结构。commit 指向一个根 tree 和零个或多个父 commit,构成仓库的历史。 对象由其类型和内容的哈希值标识。对象也是不可变的:记录一个被修改的文件会创建新的 blob,而未被修改的文件和目录则可以复用现有对象。 分支和标签是指向 commit 的引用,即 "refs"。在某个分支上提交会创建一个新 commit,并将该分支的 ref 更新为指向它。 Git 对象通过 ID 互相引用:commit 指向 tree,tree 指向 tree 和 blob。分支 `refs/heads/main` 指向 commit C2,其父 commit 是 C1。C1 指向根 tree T1,其中 `parser.py` 指向 blob B1。C2 指向根 tree T2,其中 `parser.py` 指向 blob B2。在两个根 tree 中,`docs/` 都指向同一个 tree T3。它的 `README.md` 条目指向 blob B3,`usage.md` 指向 blob B4。 Git 以 "松散(loose)" 或 "打包(packed)" 两种状态存储对象。松散对象以对象 ID 命名,各自存放在单独的文件中。打包对象则一起存放在一个 packfile 中,使相似的对象能够以增量(delta)形式相对于彼此存储。 Git 可以将 blob 存为单独的文件,也可以将它们打包在一起,把一个 blob 编码为相对于另一个 blob 的增量。 在 Git 中,如果 Git 能够从某个 commit 出发,沿着其父 commit、tree 和 blob 的链接找到某个对象,那么该对象就从这个 commit *可达(reachable)*。在 fetch 过程中,客户端会告诉服务器它想要哪些 commit,以及它已经拥有哪些 commit。服务器找出从所请求 commit 可达的对象,排除它知道客户端已有的部分,然后将其余部分以 Git 在磁盘上使用的同一种格式,作为一个 packfile 发送过去。完整的 clone 起始时没有任何对象,因此它需要被克隆历史中所有可达的对象。 push 则以相反的方向传输对象,通过请求服务器更新一个或多个 ref 来实现。客户端利用服务器广播的 ref 集合,避免发送服务器已经拥有的对象。因此,发送的 pack 可能依赖于已有对象:仅凭接收到的 pack 不足以重建新的历史。 复制 Git 仓库意味着在多个服务器上保持对象和引用都可用。在副本能够提供某个引用的新值之前,它必须先能提供从该值可达的对象。服务还需要协调相互冲突的*引用更新*,即试图把同一个分支从同一个旧的顶端移动到不同新 commit 的 push。对仓库的所有写入强加单一顺序的系统,可能会让对不相关分支的 push 互相等待。 ## 现有的分布式 Git 服务模式 在设计 Pando 期间,我们评估了使用 Git 扩展源代码控制的两种传统方案:一种是使用三阶段提交(3PC,一种跨多个节点协调提交的协议)在仓库副本之间协调 ref 更新;另一种是通过共享的预写日志(WAL,记录在应用之前先写下的变更日志)来对更新排序,日志存放在 blob 存储中。 ### 三阶段提交(3PC) 在我们建模的三阶段提交设计中,Git 服务在若干副本上保存仓库的完整副本。当任一服务器收到 push 时,传入的 packfile 会被复制并并行发送到所有副本。要发布这次 push,必须有多数副本接受该 ref 更新。负责此事的服务器是协调者(coordinator),它需要与每个其他副本进行多轮通信。 为了接受一个引用更新,每个副本会锁定自己那份 ref 的副本,并检查它是否仍指向客户端所期望的 commit。然后协调者向那些已接受的副本发送 pre-commit 消息。一旦多数副本确认了 pre-commit 消息,协调者就通知它们执行更新。之后,任何持有最新数据的副本都可以从本地仓库副本中广播新的分支顶端。 保留完整副本使得本地服务读取成为可能,并保证无论客户端选择哪个副本,都能获得所有引用的一致视图。其结果是引用只能一次更新一个,且每次 push 都需要多轮往返来在副本之间协调 ref 更新。 等待法定人数(quorum)会把副本的磁盘性能和通信延迟放到 push 的关键路径上。随着写入量增长,副本既要跟上不断到来的 pack,又要应对读取流量。 ### 预写日志(WAL) 在我们建模的 WAL 方案中,Git 服务使用 blob 存储中的共享清单(manifest)来为整个仓库的更新排序。当服务器收到 push 时,它把传入的 packfile 上传到 blob 存储,并在本地仓库副本中准备 ref 更新。要发布这次 push,它会在 blob 存储中共享清单里添加一条记录,该记录包含 ref 更新并指向 packfile。随后 Git 服务器使用比较并交换(compare-and-swap)操作来更新清单。这会原子性地替换清单,否则就拒绝该变更。如果在此期间另一个写入者已经更新了清单,服务器会读取中间的条目,检查自己的 ref 更新是否仍然有效,并使用已上传的 packfile 进行重试。 写入者可以并行上传各自的 packfile,但即使是对不相关分支的 push,也需要更新同一个清单。一次成功的比较并交换的往返时间,限制了仓库发布新清单的频率。低延迟的 blob 存储能让这一步更快。批量处理可以让一次清单更新同时更新多个 ref,代价是排队更新并增加延迟。 blob 存储可以并行接收 packfile,但对共享清单的更新仍然会互相竞争。仅仅提升 blob 存储的容量并不能解决这个瓶颈。每个副本在本地提供服务之前,仍然需要从 blob 存储读取并索引这些 pack。如果到来的写入速度超过了这项工作的处理速度,副本就会落后,客户端等待拉取新推送 commit 的时间也会变长。 ## Pando 的架构 这就引出了 Pando。Pando 是一种服务 Git 仓库的新架构,目标是扩展到支持大量客户端同时推送。它的工作方式如下: ### 重新审视约束 在开发 Pando 之前,我们就有一种直觉:可以在不牺牲 Git 的前提下满足需求,但确实需要在大多数 Git 系统视为必需的一些事情上做出妥协: - **仓库级一致性**:提供所有 ref 的一致、时点快照需要跨仓库的协调。在一个将所有 ref 更新统一排序的系统中,对不相关分支的 push 会在同一个协调点上发生竞争。 - **为每个客户端生成唯一 packfile**:如果两个本地历史不同的客户端拉取同一个分支,服务器可能需要为每个客户端生成不同的 pack,以避免发送它们已有的对象。生成这些响应会消耗 CPU 和磁盘 I/O,使得难以扩展到海量读者。 作为替代,Pando 提供一种宽松的一致性模型(因果一致性)来扩展写入,并通过复用 packfile 来扩展读取。 ### 因果一致性(CC) 当客户端向某个分支推送时,Pando 会把传入的 packfile 作为不可变的 blob 存入 blob 存储。每当分支发生移动,Pando 就会记录一个 *ref 版本(ref version)*,其中包含新的顶端、提供该顶端的 pack,以及该 pack 所依赖的更早的 ref 版本,这些都记录在元数据存储中。blob 存储和元数据存储都是水平可扩展的。 从 Pando fetch 一个 commit 或引用,需要向客户端重放一组 ref 版本(packfile),从而重建创建该 ref 版本的客户端的状态。这在整个仓库范围内提供了较弱的一致性形式,同时对单个 ref 的更新提供可串行化(serializability)。客户端只有在同时向同一个 ref 推送时才会冲突。读取多个分支的客户端不能假设这些读取描述的是仓库中同一个时间点的状态。 向主干(trunk)的 push 会追加一个增量 pack,其中包含一个或多个 commit。每次向侧分支的 push 都会再次包含该主题分支(topic)更早的 commit,这样拥有已广播主干版本的客户端只需一个 pack 就能拉取该主题分支。 这就是一种 *因果一致性(causal consistency)*。为了拉取一个引用,读者需要观察到产生它的写入者的历史。在 push 过程中,Pando 控制着客户端与服务器所共识的共享历史,从而确保客户端生成更容易共享的 packfile。 其结果是,Pando 在 Git 操作期间不提供所有引用的时点状态,也不在 clone 或 fetch 内部对 ref 更新排序。具体来说,假设在一次完整 clone 的协商过程中,分支 `foo` 和 `bar` 都被更新了。在协商期间,一个客户端可能观察到 `foo` 处于版本 1、`bar` 处于版本 2,而另一个客户端观察到 `foo` 处于版本 2、`bar` 处于版本 1。在实践中,用户和智能体并不在意 ref 更新的全局顺序,而是依赖对单个引用的更新顺序。 ### 方案对比 在这项针对三种一致性模型的仿真中,我们配置 1,000 个客户端,每个客户端选择一个不同的分支,然后反复推送唯一的 commit,测量总墙上时钟耗时以及 push 到 fetch 的延迟。后者对工作负载来说是一项重要指标,因为它衡量了用户和智能体进行协作,或使用依赖版本控制的其他系统时的往返时间。仿真建模了从普通到高端的虚拟机以及 blob 存储系统。在本次仿真中,我们放宽了三阶段提交方案的仓库级一致性。 ## 仿真 Git 服务器实现 跟踪每个写入者经历的反复推送,并单独测量独立读者何时能够检出每个 commit。 运行此仿真,将 Pando 与另外两种方案进行对比。选择从普通到高端的硬件、客户端数量及更多设置,查看它们的对比结果。 所建模的三阶段提交系统在负载下显示出明显更高的写入延迟,因为它们的磁盘会达到饱和;与此同时,预写日志系统出于同样的原因显示出明显更高的读取延迟,因为服务器难以跟上从日志中读取的速度。在我们 monorepo 的真实测试和本仿真中,Pando 都支持数千个客户端并发推送,且在负载下不影响性能。 在处理 push 时,Pando 会在把传入的 packfile 流式写入 blob 存储的同时对其进行校验,而无需将其解包到本地仓库中。当客户端开始 fetch 时,Pando 会挑选读者所需存储的 pack,然后由读者下载。没有任何服务器需要把 packfile 应用到本地仓库,也不必执行昂贵的解包和打包操作。由于这些服务器主要负责协商和 packfile 选择,Pando 服务器可以运行在普通商用硬件上。 这让 Pando 可以扩展到客户端、客户端与 Pando 之间的网络以及 blob 存储服务的极限。只要有足够的容量,各自在独立分支上工作的客户端都能以自身连接所允许的最快速度传输 pack。要以同样的速率服务更多客户端,我们可以增加 Pando 服务器的数量,并同步扩展元数据存储和 blob 存储。 ## 在 Pando 中推送和拉取分支 Pando 将写入者的 packfile 存储在 blob 存储中,并在元数据中记录 ref 版本……

相似文章

Agora:Git 作为集体自动研究的共享内存

Hugging Face Daily Papers

Agora 是一个用于自主 AI 研究代理的共享内存系统,它使用 Git 将研究记录为仅追加的有向无环图 (DAG),从而实现协作发现。在一次 12 天的实验中,13 个代理将模型性能提升了 62%,接近训练基线。

@github:2025年9月至2026年8月期间,Git 活动量增长超过一倍,达到每月 4733 亿次事件。我们正在重建…

X AI KOLs Following

GitHub 报告称,Git 活动量同比增长超过一倍,达到每月 4733 亿次事件,同时 GitHub 正在重建其 Git 基础设施,以支持面向智能体(agent)、智能体规模的软件开发。文章详细介绍了面临的架构挑战,包括提交延迟瓶颈、写入吞吐量激增、引用(ref)争用,以及来自 CI 和代码扫描的读放大(read fan-out)问题。