在 OpenAI 扩展 Git 规模
OpenAI 如何构建 Pando——一个为 Codex 所带来的开发规模而设计的 Git 兼容后端。
过去一年中,我们 monorepo 的规模和需求以惊人的速度增长。显然,我们需要彻底改造我们的源代码控制系统,以适应 Codex 所带来的并发工作规模。
我们构建了 Pando——一个全新的 Git 仓库后端,在不中断任何工程工作、也不改变团队使用 Git 方式的情况下完成了迁移。Pando 现在为 OpenAI monorepo 中的所有开发工作提供支持。Pando 的设计旨在实现两个雄心勃勃的目标:第一,水平可扩展的写入,针对向多个分支的并发推送进行优化;第二,最大化读取吞吐量,使客户端能够快速下载数据,从而充分利用其可用的网络带宽。
我们在实现这些目标的同时,保持了与 Git 协议和客户端的兼容性,使我们的工程师和智能体都能将 Pando 无缝融入现有工作流。在生产环境中,Pando 将推送延迟的中位数降低了近 5 倍。它现在每天传输近 1 PB 的 Git 数据。
在本文中,我们将介绍构建新源代码控制系统的动机,以及为何选择保持与 Git 的兼容性。我们将把我们的系统与常见的 Git 扩展方案进行对比,说明 Pando 为何能独特地满足 OpenAI 的可扩展性需求。我们还将讨论未来的计划,以及我们将如何继续为 Git 做出贡献,以支持智能体规模的工作。
为什么我们需要新的源代码控制系统
Codex 改变了 OpenAI 开发工作的规模和节奏,使工程师能够并行完成更多工作。这意味着更多的分支和推送、更多的合并,以及来自开发环境、研究任务和 CI 的更多 fetch 和 clone。
这些需求会相互叠加。现在一位工程师可能同时有多个变更等待 CI 处理,而每次推送都可能触发多个任务拉取同一个提交。
从去年年底左右开始,支撑我们 monorepo 的系统开始难以跟上需求。我们遭遇了缓慢的 fetch 和 push,以及失败的合并,可靠性有时降至 90% 以下。我们是触及了 Git 本身的极限,还是我们服务 monorepo 方式的极限?Git 源自一个通过电子邮件向内核交换补丁的社区。二十多年来,开发者随着软件构建方式的变化,不断调整围绕 Git 的工具。当我们自己的 monorepo 开始感受到摩擦时,我们很看重能够继续在这一成果之上进行构建。
我们找到了一种方法,在重建后端以并行处理对不同分支的推送、并同时为多个读者提供同一历史记录的同时,保留熟悉的工作流。以 Git 作为源代码控制工作流的基础,意味着对客户端的改进可以惠及 OpenAI 之外的用户。我们希望将这些改进从我们的 Git 副本(在新窗口中打开) 贡献给上游,与更广泛的社区共享。
大规模 Git 的工程实践
扩展源代码控制系统有两个基本维度:许多写入者向不同分支推送,以及许多读者拉取同一历史记录。Pando 针对这两方面都进行了优化。
首先,Pando 独立地协调每个分支的更新。对不相关分支的更新可以并行进行,而获取某个分支的读者会得到该修订所需的历史记录。其代价是,查看多个分支的读者可能看到它们处于不同的时间点。
其次,Pando 在多次获取之间复用 packfile(Git 用于将多个对象压缩在一起的格式),而不是为每个客户端生成单独的 packfile。客户端可能会下载一些它已有的对象,但服务器可以用很少的计算量为许多客户端提供相同的字节。
这些选择让我们能够在各分支间扩展写入,并从缓存提供读取服务。要了解它们如何协同工作,让我们先从 Git 如何存储和传输历史开始。
Git 对象存储入门
Git 必须能够保持仓库的两部分同步:对象和引用。
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 将对象存储为“松散”或“打包”状态。松散对象以对象 ID 标识的单个文件存储。打包对象存储在 packfile 中,使得相似的对象可以作为彼此的 delta 存储。
Git 可以将 blob 存储为单独的文件,也可以将它们打包在一起,将一个 blob 编码为相对于另一个 blob 的 delta。
在 Git 中,如果 Git 能够从某个 commit 出发,通过其父 commit、tree 和 blob 的链接找到一个对象,则该对象从该 commit 是可达的。在获取过程中,客户端会告诉服务器它想要哪些 commit 以及它已经拥有哪些 commit。服务器找到从所需 commit 可达的对象,排除它知道客户端已有的对象,并将剩余部分作为 packfile 发送,其格式与 Git 在磁盘上使用的格式相同。完整的克隆开始时没有任何对象,因此它需要被克隆历史中所有可达的对象。
推送通过请求服务器更新一个或多个 ref,以相反的方向传输对象。客户端使用服务器公布的 ref 集合来避免发送服务器已有的对象。因此,正在发送的 pack 可能依赖于现有对象:仅接收该 pack 不足以重建新的历史。
复制 Git 仓库意味着在多台服务器之间保持对象和引用都可用。副本在能够提供引用的新值之前,必须先能提供从该值可达的对象。该服务还需要协调相互竞争的引用更新,即试图将同一分支从同一旧顶端移动到不同新提交的推送。对仓库的所有写入施加单一顺序的系统,可能会让推送到不相关分支的操作彼此等待。
现有的分布式 Git 服务模式
在设计 Pando 时,我们评估了使用 Git 扩展源代码控制的传统方法:一种是使用三阶段提交(3PC,一种在多个节点之间协调提交的协议)在仓库副本之间协调 ref 更新;另一种是通过共享的预写日志(WAL,一种在变更应用之前写入的记录)在 blob 存储中对更新进行排序。
三阶段提交(3PC)
在我们建模的三阶段提交设计中,Git 服务在多个副本上保留仓库的完整副本。当任何服务器收到推送时,传入的 packfile 会被复制并并行发送到所有副本。要发布该推送,必须有多数副本接受 ref 更新。管理此过程的服务器是协调者,它需要在协调者与其他每个副本之间进行多轮通信。
要接受引用更新,每个副本会锁定其 ref 的副本,并检查它是否仍指向客户端所期望的提交。然后协调者向接受更新的副本发送预提交消息。一旦多数副本确认了预提交消息,协调者便通知它们执行更新。之后,任何最新的副本都可以从其本地仓库副本中公布新的分支顶端。
保留完整副本使得本地提供读取成为可能,并保证无论客户端选择哪个副本,都能获得所有引用的一致视图。因此,引用是一次一个地更新的,并且每次推送都需要多次往返才能在副本之间协调 ref 更新。
等待法定人数会将副本磁盘性能和通信延迟置于推送路径上。随着写入量的增长,副本必须同时跟上传入的 pack 以及读取流量。
预写日志(WAL)
在我们建模的基于 WAL 的设计中,Git 服务使用 blob 存储中的共享清单来对整个仓库的更新进行排序。当服务器收到推送时,它会将传入的 packfile 上传到 blob 存储,并在其本地仓库副本中准备 ref 更新。要发布该推送,它会向存储在 blob 存储中的共享清单添加一个条目,该条目记录 ref 更新并指向该 packfile。然后 Git 服务器使用比较并交换(compare-and-swap)操作更新清单。该操作会原子地替换清单,否则就拒绝此次更改。如果在此期间有其他写入者更新了清单,服务器会读取其间的条目,检查其 ref 更新是否仍然有效,并使用已上传的 packfile 进行重试。
写入者可以并行上传其 packfile,但即使推送到不相关的分支也需要更新同一个 manifest。一次成功的比较并交换(compare-and-swap)的往返时间限制了仓库发布新 manifest 的频率。低延迟的 blob 存储使这一步骤变得快速。批处理允许一个 manifest 同时更新多个 ref,代价是需要排队更新并增加延迟。
blob 存储可以并行接收 packfile,但对共享 manifest 的更新仍然会相互竞争。仅仅增加 blob 存储的容量并不能解决这一瓶颈。每个副本仍需从 blob 存储读取并索引 pack,然后才能在本地提供服务。如果传入的写入速度超过这项工作的处理速度,副本就会落后,客户端等待获取新推送的提交的时间也会变长。
设计 Pando
这就引出了 Pando。Pando 是一种用于提供 Git 仓库服务的新架构,其目标是扩展到支持大量客户端同时推送。其工作原理如下:
重新思考约束
在开发 Pando 之前,我们有一种直觉,认为可以在不牺牲 Git 的前提下满足我们的需求,但我们确实需要在大多数 Git 系统视为必需的某些事项上做出妥协:
全仓库一致性: 提供所有 ref 的一致、时间点快照需要跨仓库进行协调。在一个将所有 ref 更新统一排序的系统中,对不相关分支的推送会在单个协调点上产生争用。
为每个客户端生成唯一的 packfile: 如果两个具有不同本地历史记录的客户端获取同一个分支,服务器可能需要为每个客户端生成不同的 pack,以避免发送它们已有的对象。创建这些响应会消耗 CPU 和磁盘 I/O,使得难以扩展到大量读取者。
相反,Pando 提供了一种宽松的一致性模型(因果一致性)来扩展写入,并通过重用 packfile 来扩展读取。
因果一致性(CC)
当客户端向分支推送时,Pando 将传入的 packfile 作为不可变 blob 存储在 blob 存储中。每当分支移动时,Pando 会在元数据存储中记录一个 ref 版本,其中包含新的顶端(tip)、提供它的 pack,以及该 pack 所依赖的任何更早的 ref 版本。blob 存储和元数据存储都是可水平扩展的。
从 Pando 获取提交或引用需要将一组 ref 版本(packfile)重放到客户端,从而重建创建所获取 ref 版本的客户端的状态。这在整个仓库范围内提供了一种较弱的一致性形式,同时为单个 ref 的更新提供可串行化性。只有在同时推送到同一个 ref 时,客户端之间才会发生冲突。读取多个分支的客户端不能假设这些读取描述了整个仓库的单一时间点。
向 trunk 推送会添加一个增量 pack,其中包含一个或多个提交。每次向侧分支推送都会再次包含该主题(topic)的早期提交,因此拥有已通告 trunk 版本的客户端可以在一个 pack 中获取该主题。
这是一种因果一致性。为了获取一个引用,读取方会观察产生它的写入方的历史。在推送过程中,Pando 控制客户端和服务器达成一致的共享历史,确保客户端生成更容易共享的 packfile。
因此,在 Git 操作期间,Pando 不提供所有引用的某一时间点状态,也不提供克隆或获取过程中引用更新的顺序。具体来说,假设分支 foo 和 bar 在完整克隆的协商过程中都被更新。在协商期间,一个客户端可能观察到 foo 处于版本 1、bar 处于版本 2,而另一个客户端观察到 foo 处于版本 2、bar 处于版本 1。实际上,用户和智能体并不关心引用更新的全局顺序,而是依赖于单个引用的更新顺序。
方法比较
在这三种一致性模型的模拟中,我们配置了 1,000 个客户端,每个客户端选择一个不同的分支,然后反复推送唯一的提交,测量总挂钟时长和推送到获取的延迟。后者是工作负载的重要指标,因为它衡量了用户和智能体协作或使用依赖版本控制的其他系统时的往返时间。该模拟对从普通到高端的虚拟机和 blob 存储系统进行了建模。在本次模拟中,我们为三阶段提交放宽了仓库范围的一致性。
模拟 Git 服务器实现
跟踪每个写入方的反复推送,并单独测量独立读取方何时可以检出每个提交。
运行此模拟,将 Pando 与另外两种方法进行比较。可从普通到高端硬件中选择基础设施、客户端数量以及更多设置,以查看它们的比较结果。
所建模的三阶段提交系统在负载下显示出显著更高的写入延迟,因为其磁盘达到饱和;与此同时,预写日志系统由于相同的原因显示出显著更高的读取延迟,因为服务器难以跟上从日志中读取的速度。无论是在我们的 monorepo 上进行的真实世界测试,还是在本次模拟中,Pando 都支持数千个客户端并发推送,且在负载下不会影响性能。
在处理推送时,Pando 会在将传入的 packfile 流式传输到 blob 存储的同时对其进行验证,而无需将其解包到本地仓库中。当客户端开始获取时,Pando 会选择读取方所需的已存储 pack,然后由读取方下载它们。没有任何服务器需要将 packfile 应用到本地仓库,也不需要执行开销巨大的解包和打包操作。由于这些服务器主要执行协商和 packfile 选择,Pando 服务器可以在普通硬件上运行。
这使得 Pando 能够扩展到客户端、客户端与 Pando 之间的网络以及 blob 存储服务的极限。只要有足够的容量,在独立分支上工作的客户端就可以各自以自身连接允许的最快速度传输 pack。为了以该速率服务更多客户端,我们可以添加 Pando 服务器,并同步扩展元数据存储和 blob 存储。
在 Pando 中推送和获取分支
Pando 将写入者的 packfile 存储在 blob 存储中,并在元数据服务中记录 ref 版本。在 fetch 时,Pando 选择读取者所需的 pack,然后由 Express 提供服务。
智能体编辑文件并在本地创建提交,然后运行 git push,它会为这些更改创建一个 packfile 以更新分支。Pando 接收该 pack 并将其流式传输到 blob 存储。在通过单次解码验证传入的推送后,Pando 使用多键事务在我们的元数据存储中记录 ref 版本,并原子地更新 ref 指针。这使 Pando 能够提供“读己之写”一致性。整个写操作需要 次元数据读写。
当另一个客户端 fetch 该分支时,Pando 使用客户端已有的历史记录选择一组已存储的 pack。此操作需要 次元数据读取,其中 是客户端已有内容与客户端所需内容之间 ref 版本的最短距离。服务器返回 packfile URI,客户端通过一个名为 Express 的服务下载它们,该服务将 packfile 存储在区域缓存中。
该设计依赖于 Pando 架构的三个核心服务:推送、fetch 和压缩。
扩展推送
当客户端推送时,传统的 Git 服务器就像一个亮出所有底牌的扑克玩家,向客户端展示它拥有的每个分支。然后客户端收集提交、树和 blob,创建一个 packfile,其中排除它知道服务器已有的对象。
公布更多 ref 可以使推送更小。它也可能使 pack 更难被重用,因为另一个客户端可能缺少写入者省略的对象。
Pando 采取了不同的方法。它不亮出全部手牌,而是只向客户端公布一组精选的分支。这样做有两个原因:
提供每个 ref 的一致视图代价高昂。在拥有数百万分支的情况下,仅 ref 公告就可能超过 100 MiB。提供这一点需要每个服务器要么维护一个当前映射(需要大量的协调开销),要么进行一个随 ref 数量扩展的数据库查询。
通过只向客户端提供 ref 的子集,客户端将生成一个仅依赖于服务器明确允许客户端排除的内容的 pack。跟踪这些依赖关系使 Pando 能够重用客户端在一次推送中生成的 packfile,将其作为后续 fetch 的响应提供服务。
随着 Git 服务器公布更多 ref,推送的客户端就有更多来源可以排除对象,从而能够生成更小的 pack。然而,重用这些 packfile 要求服务器能够证明 fetch 的客户端拥有每个依赖项。否则,packfile 可能会省略客户端需要的对象。
另一方面,如果 Git 服务器一个 ref 都不公布,客户端将生成一个包含仓库完整历史的 packfile。该 packfile 可以被任何其他客户端使用,但代价是使 fetch 任何分支实际上都变成一次完整的克隆。
公布更多 ref 会产生更小的 packfile。大部分的大小缩减可以仅通过公布一个 ref 来实现。
默认情况下,当客户端推送一个新的功能分支时,Pando 仅将指定的主干分支作为基础进行通告。这使得通告的大小不会随着仓库分支的增加而增长,但需要权衡取舍。当工程师要么多次推送到单个分支,要么将一个功能分支堆叠在另一个功能分支之上时,客户端就必须重新发送主干之外的所有内容。
这会导致后续的分支推送变得更大,包含可能冗余的对象,并消耗额外的客户端带宽。在我们的 monorepo 中,这种方式产生的 packfile 中位数小于 20 KiB,99 分位数小于 3 MiB。对我们来说,这是一个合理的权衡,因为已经拥有被通告的主干历史的读取者只需这一个 packfile 就能获取该分支。
在选择要通告给客户端的 ref 集合之后,Pando 接受推送并更新其元数据存储。Pando 将元数据更新分为同步和异步两部分。同步的元数据更新确保客户端能够立即获取新更新的提交。这会写入一个新的 ref 版本,并更新指向该新版本的 ref 指针映射,该版本存储新的 tip、用于协商 packfile 的依赖项,以及指向被推送的 packfile 的链接。
与此同时,异步工作也会在同一事务中被记录,例如调度压缩。对于非常大的推送,只有一部分提交会被同步写入。这使我们无论推送大小如何,都只需要对元数据存储进行常数次的读写操作。
重要的是,Pando 服务器可以独立接受对不相关 ref 的推送。唯一存在争用的键是 ref 指针,我们使用类似原子比较并交换的操作来保护它。我们可以通过添加 Pando 服务器、元数据存储扩展单元或 blob 存储来扩展写入吞吐量。
线速获取与克隆性能
当客户端执行 fetch 时,它会向服务器提供两组提交 ID,在协议中称为 haves 和 wants。在传统的 Git 服务器中,服务器会计算 haves 可达对象与 wants 可达对象之间的集合差,压缩它们,然后将结果发送给客户端。虽然开源 Git 服务器在本地有各种索引来加速此操作,但这些 packfile 的计算成本可能很高,且通常难以缓存。
相反,Pando 使用推送时记录的提交映射来查找客户端的 haves 和 wants,并将它们解析为包含这些提交的一组 ref 版本。每个 ref 版本都链接到 Pando 之前接收到的 packfile。服务器利用这一点来求解客户端所需的内容,选择覆盖客户端所需对象集合的 packfile。
Pando 中的 fetch 规划
Pando 比较客户端已有的内容与其想要的内容,然后选择所需的已存储 packfile——包括任何缺失的分支依赖项。
已持有获取缺失的分支依赖项
获取 27 个主干 pack。
选择一个分支以包含其主干依赖项。
Pando 将 Git 协议的 haves 和 wants 映射为一系列 ref 版本。想要一个侧分支意味着想要其对主干的依赖项。
Git 协议仅允许在克隆或获取操作的 HTTP 响应中发送单个包文件。然而,Git 协议中一个未被充分重视的特性,packfile-uri,允许服务器返回链接以单独下载额外的包。Pando 完全依赖这一特性,并在内联传输一个空包文件。为支持此特性,OpenAI 向 packfile-uri 上游贡献了多项改进。
OpenAI 的系统在许多地理区域运行,因此我们在每个区域都维护了带有本地缓存的 Pando 集群。包分发服务 Express 缓存我们的 blob 存储,另一个独立服务缓存元数据存储。这些缓存使 Pando 能够完全基于该集群中的缓存状态来响应提交 ID 或 ref 特殊读取的请求。我们使用一致性哈希将包文件的请求路由到 Express 服务器,缓存命中率超过 99%。
这套缓存系统提供了极高的吞吐量,足以打满客户端与 Pando 之间的网络带宽限制。借助这些包文件 URI 和本地缓存,我们已验证仅使用标准云服务商虚拟机,克隆和获取操作即可打满 10 Gbps 网络链路。
压缩与首字节时间
对于大多数 Git 服务器,重新打包和垃圾回收是维持不断增长的仓库性能所必需的。这两个步骤都能回收空间,但对计算和 I/O 的消耗很大。
在像 OpenAI 单体仓库这样快速迭代的仓库中,为自上次获取以来的每次推送都发送一个单独的包文件 URI 是不可行的。客户端每天需要下载数万个包文件才能跟上主干的进度。
相反,Pando 在后台合并包以减少这种开销,采用了类似于日志结构合并树(LSM-tree)中压缩的设计。每次推送都会产生一个 Pando 称之为“L0”的包文件,代表 Pando 可传输的最小数据单元。随着时间的推移,后台工作进程会将这些包文件收集到覆盖一系列 ref 版本的“覆盖包(cover)”中,其规模从 L0 开始按几何级数增长。目前,我们的单体仓库使用了最高至 L4 的包文件覆盖包。
这些覆盖包具有双重用途。当一个覆盖包足够满时,压缩工作进程会认领它并生成单个包文件。这通过重新组织包文件内部的对象来提高压缩率。但它还通过充当 ref 版本上的跳表来提高获取协商性能。通过同时扫描每一层的索引,Pando 可以在对数时间内选择一组要发送给客户端的包。这些包文件覆盖包,无论是否经过重新打包,都为获取规划提供了一种更高效的解决方案,降低了 Pando 对吞吐量和延迟的要求。
Pando 中的压缩
小包文件被分组为跨层级的更大覆盖包,从而提高压缩率并加快获取速度。
L2
L1
L0
压缩进行中已发布的覆盖包
压缩在后台合并来自多个 ref 版本的包文件,从而以更少的文件实现更高效的获取。
与许多 Git 服务器不同,packfile 在每一层都会保留,而不会被丢弃或进行垃圾回收。每一层的 pack 都用于持续服务客户端,依据的是它们在 fetch 请求中所请求的 have 和 want。对于将 Pando 作为 API 使用的客户端来说,从 packfile 中读取任意对象,通常需要从 L0 读取的数据比从 L4 读取的更少,因为 L0 中的数据被压缩的可能性更低。
Pando 中的压缩(Compaction)显著减少了发送给客户端的总传输量,并使 pack 规划能够根据客户端落后的程度实现亚线性扩展。这对于大型且快速变化的代码仓库尤为重要,我们预计这将成为软件开发中的新常态。
构建 Pando
我们使用 Codex 构建了 Pando,从启动到上线仅用了 10 周。Pando 的核心由一位工程师设计,由多个智能体团队实现,并由少数工程师部署。Pando 于今年 4 月开始服务生产流量,是 OpenAI 所使用的源代码管理工具体系中的一部分。
在验证 Pando 的设计时,我们采用了多种多样的方法。我们应用了关于 构建更安全的软件基础 的原则。作为一个全新项目,我们选择使用 Rust 实现 Pando,且不依赖任何遗留代码。我们编写了一套一致性测试套件,使智能体能够针对标准 Git 行为测试其更改。我们还开发了确定性模拟测试框架,让我们能够检查系统的不变量和属性,从而发现并缓解协商和 pack 规划中细微的竞态条件和回归问题。
许多模型为这个系统做出了贡献;随着新模型的可用,我们利用它们来提升 Pando 的性能和可靠性。使用 GPT‑6 Astra,智能体编写了 Lean 和 TLA+ 来形式化一个“Pando 理论”,我们用它来发现并修复实现中的 bug,并证明系统的属性。多个持续会话运行了数百小时,以发现并修复 Pando 中的边缘情况。我们还全天候运行智能体,使用具备 Daybreak 能力的模型对 Git 和 Pando 进行红队测试。
部署 Pando
我们最初将 Pando 部署为现有源代码管理系统的读写透传代理。这使我们能够保持 Pando 与旧系统同步,从而可以在不停机的情况下安全地将流量迁移到 Pando。
作为其缓存设计的一部分,Pando 还可以在代理请求之前对请求进行去重和合并。这使我们能够在逐个迁移系统到 Pando 的同时,保持现有工具的正常运行。随着我们将更多工作负载迁移到 Pando,它们的推送填充了 Pando 的缓存,并提高了我们旧后端的可靠性。
为了优化使用 Pando 时的性能,我们开始将公开分发的 Git 部署到开发者的机器以及 ChatGPT 桌面应用中。为了将客户端迁移到 Pando,我们使用了一个围绕 Git CLI 的内部封装器来分阶段迁移客户端。我们还创建了许多其他工具来处理变更请求、代码审查以及用于自动化的 API。我们迁移了数百个依赖于遗留 API、webhook 和仓库下载的集成,与 Git 流量一同迁移。
Codex 本身帮助开发者和团队完成了这一转变。我们创建了技能,用于准备现有的检出,以及引导智能体使用新系统。因此,开发者只需让 Codex 迁移其开发环境和服务即可。
切换到 Pando 提高了合并的可靠性,并将中位推送延迟降低了近 80%。
Pando 仅用少量 CPU 核心即可每天交付一 PB 的 Git 数据。Pando 曾在单小时内提供超过 57 TB 的 Git 包,其中超过 99% 的字节来自本地磁盘缓存。
复用包使许多客户端能够下载相同的历史仓库,而服务节点只需极少的计算。此示例仅展示了一个仓库,但 Pando 的设计旨在支持更多仓库。随着需求增长,我们可以独立扩展数据交付和 Git 协商能力。
回馈 Git 项目
基于 Git 协议和客户端构建 Pando,帮助我们发现了 Bug 以及改进 Git 自身性能的机会。
例如,Pando 直接向客户端提供可复用的包,因此下载和索引这些包成为使系统快速可靠的核心工作。这项工作暴露了并发下载、传输中断后的清理以及恢复已完成下载等方面的边缘情况。我们修复了这些路径,在接受现有包进行获取时增加了验证,并将这项工作回馈给了上游。
我们还需要了解时间和带宽的去向,因此我们为 packfile-uri 下载添加了追踪,并为包生成添加了字节数统计,从而更容易判断缓慢的获取是将时间花在生成包上还是传输包上。
在 OpenAI 的 Git 构建(在新窗口中打开) 中,客户端可以并行下载和索引独立的包。我们引入了“显式 haves”的概念,使服务器能够将分支的当前顶端与推送客户端可能从其包中省略的历史分开通告。在 Pando 上,这使客户端能够使用 --force-with-lease 来检查功能分支是否未移动,并仅针对主干构建其包。任何拥有该主干历史的人都可以使用相同的包获取该分支。
其中几项改进已进入上游 Git,还有许多改进在我们的公共仓库中。我们正在继续与 Git 社区合作,使这些改进在 Pando 之外也能发挥作用。您可以在我们的 公共补丁追踪器(在新窗口中打开) 中关注这项工作。
下一步
通过重新思考如何扩展 Git 仓库的常规做法,Pando 使我们能够在不改变工程师和智能体已使用的 Git 工作流的情况下扩展我们的单体仓库。在我们的单体仓库中,Pando 实现了更高程度的并行性,提高了合并的可靠性,并减少了发布代码更改所需的时间。在单体仓库之外,我们还在探索 Pando 在 OpenAI 内部的其他用途,包括需要大量智能体在共享 Git 仓库上协作的工作负载。
随着工程师和智能体产出越来越多的软件,OpenAI 之外的更多团队将遭遇我们在源代码控制中遇到的极限。我们目前正在探索如何与用户分享使 Pando 成为可能的技术。
Pando 建立在 Git 仓库用户和运维人员二十年经验的基础之上。我们将继续开发 Pando,同时与开源 Git 社区合作,分享任何能让更广泛的 Git 用户受益的改进。