OpenAI来源发布时间

在 OpenAI 扩展 Git

在过去一年中,我们的 monorepo 的规模和需求以惊人的速度增长。很明显,我们需要改造源码控制系统,以扩展到 Codex 所能实现的并发工作水平。

在过去一年中,我们的 monorepo 的规模和需求以惊人的速度增长。很明显,我们需要改造源码控制系统,以扩展到 Codex 所能实现的并发工作水平。

OpenAI monorepo 的季度提交增长——相对于 2024 年第 1 季度,落在 master 分支上的提交数量的增长

我们构建了 Pando——一个面向 Git 仓库的新后端——没有中断任何工程工作,也没有改变团队使用 Git 的方式。Pando 现在为 OpenAI monorepo 中的所有开发提供支持。Pando 旨在实现两个宏大的目标:第一,水平可扩展的写入,针对向多个分支的并发推送进行优化。第二,最大化读取吞吐量,使客户端能够以足够快的速度下载数据,充分占满其可用的网络带宽。

我们在实现这些目标的同时保留了对 Git 协议和客户端的兼容性,使我们的工程师和 agent 都能将 Pando 无缝纳入其现有工作流。在生产环境中,Pando 将推送的中位延迟降低了近 5 倍。它现在每天提供近一个 PB 的 Git 数据。

在本文中,我们将介绍构建新源码控制系统的动机,以及我们为何选择保持与 Git 的兼容性。我们将把我们的系统与扩展 Git 的常见方法进行比较,说明为什么 Pando 能够独特地满足 OpenAI 的可扩展性需求。我们还会讨论接下来的计划,以及我们将如何继续为 Git 做出贡献,以支持 agent 规模的工作。

为什么我们需要一个新的源码控制系统

Codex 改变了 OpenAI 开发的规模和节奏,使工程师能够并行完成多得多的工作。这意味着更多的分支和推送、更多的合并,以及来自开发环境、研究任务和 CI 的更多拉取和克隆。

这些需求会叠加。现在一名工程师可以同时有多个变更等待 CI,而每次推送都可能让多个任务去拉取同一个提交。

大约从去年年底开始,支持我们 monorepo 的系统就难以跟上需求。我们遭遇了缓慢的拉取和推送以及失败的合并,可靠性有时降至 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 和零个或多个父提交,构成仓库的历史。

对象通过其类型和内容的哈希来标识。对象也是不可变的:记录一个已更改的文件会创建一个新的 blob,而未更改的文件和目录可以复用现有的对象。

分支和标签是指向 commit 的引用(或称 “ref”)。在分支上提交会创建一个新的 commit,并更新该分支的 ref 使其指向它。

Git 对象通过各自的 ID 相互引用:commit 指向 tree,tree 指向 tree 和 blob。分支 refs/heads/main 指向 commit C2,C2 的父提交是 C1。C1 指向根 tree T1,其中 parser.py 引用 blob B1。C2 指向根 tree T2,其中 parser.py 引用 blob B2。在两个根 tree 中,docs/ 都指向同一个 tree T3。T3 的 README.md 条目指向 blob B3,usage.md 指向 blob B4。

Git 以“松散”或“打包”状态存储对象。松散对象存储在以其对象 ID 命名的单独文件中。打包对象则一起存储在 packfile 中,使相似的对象能够以相对彼此的增量形式存储。

Git 可以将 blob 存储为单独的文件,或将它们打包在一起,把一个 blob 编码为相对于另一个 blob 的增量。

在 Git 中,如果一个对象可以通过从某个提交沿着其父提交、树和 blob 的链接被 Git 找到,那么该对象从这个提交是 可达的 。在 fetch 过程中,客户端告诉服务器它想要哪些提交以及它已经拥有哪些提交。服务器找出从所需提交可达的对象,排除它知道客户端已有的对象,并以 Git 在磁盘上使用的相同格式将剩余部分作为 packfile 发送。完整克隆开始时没有任何对象,因此需要被克隆历史中可达的所有对象。

push 通过请求服务器更新一个或多个引用来反方向传输对象。客户端利用服务器公布的一组引用来避免发送服务器已有的对象。因此,所发送的包可能依赖于现有对象:仅接收这个包不足以重建新的历史。

复制一个 Git 仓库意味着在多台服务器上保持对象和引用都可用。在一个副本能够提供某个引用的新值之前,它必须能够提供从该值可达的对象。该服务还需要协调相互竞争的 引用更新,即试图将同一分支从同一旧顶端移动到不同新提交的 push。对所有仓库写入施加单一顺序的系统会使对不同分支的 push 相互等待。

现有的分布式 Git 服务模式

在设计 Pando 时,我们评估了使用 Git 扩展源代码管理的常规方法:一种是在仓库副本之间使用三阶段提交(3PC,一种跨多个节点协调提交的协议)来协调引用更新;另一种是通过 blob 存储中的共享预写日志(WAL,一种在变更被应用之前写入的变更记录)来排序更新。

三阶段提交(3PC)

在我们建模的三阶段提交设计中,Git 服务在多个副本上保存仓库的完整副本。当任何服务器收到 push 时,传入的 packfile 会被复制并并行发送到所有副本。要发布该 push,必须有大多数副本接受该引用更新。管理这一过程的服务器是协调者,它需要在协调者与其他每个副本之间进行多轮通信。

为接受一个引用更新,每个副本会锁定其引用副本,并检查它是否仍指向客户端期望的提交。然后协调者向接受的副本发送预提交消息。一旦大多数副本确认了预提交消息,协调者就告诉它们执行更新。此后,任何最新的副本都可以从其本地仓库副本公布新的分支顶端。

保持完整副本使得本地提供读取服务成为可能,并保证无论客户端选择哪个副本都能获得所有引用的一致视图。其结果是,引用一次只能更新一个,而且每次 push 都需要多次往返以在副本之间协调引用更新。

等待法定人数会将副本的磁盘性能和通信延迟置于 push 路径上。随着写入量的增长,副本必须跟上传入的包以及读取流量。

预写日志(WAL)

在我们建模的基于 WAL 的设计中,Git 服务使用 blob 存储中的共享清单来排序仓库内的更新。当服务器收到 push 时,它将传入的 packfile 上传到 blob 存储,并在其本地仓库副本中准备引用更新。要发布该 push,它会向存储在 blob 存储中的共享清单添加一条条目,记录该引用更新并指向该 packfile。Git 服务器随后使用比较并交换操作更新清单。这会原子性地替换清单,否则拒绝该变更。如果另一个写入者在此期间更新了清单,服务器会读取中间的条目,检查其引用更新是否仍然有效,并使用其已上传的 packfile 重试。

写入者可以并行上传它们的 packfile,但即使是对无关分支的 push 也需要更新同一个清单。一次成功的比较并交换的往返时间限制了仓库发布新清单的频率。低延迟的 blob 存储可以使这一步骤更快。批处理允许一次清单更新同时更新多个引用,代价是更新需要排队并增加延迟。

blob 存储可以并行接收 packfile,但对共享清单的更新仍然相互竞争。仅增加 blob 存储的容量并不能解决这一瓶颈。每个副本仍需要从 blob 存储读取包并建立索引,才能在本地提供它们。如果传入的写入速度超过了这项工作的速度,副本就会落后,客户端就要等待更长时间才能 fetch 新推送的提交。

构建 Pando

现在来说说 Pando。Pando 是一种提供 Git 仓库服务的新架构,其目标是扩展到许多客户端同时 push。以下是它的工作方式:

重新审视约束

在开发 Pando 之前,我们有一种预感:我们可以在不妥协 Git 的前提下满足我们的需求,但我们确实需要在一些大多数 Git 系统视为必要条件的事情上做出妥协:

  • 仓库范围的一致性: 提供所有引用的一致的时间点快照需要跨仓库的协调。在一个将所有引用更新统一排序的系统中,对不同分支的 push 会在单一协调点上竞争。

  • 为每个客户端生成唯一的 packfile: 如果两个拥有不同本地历史的客户端 fetch 同一分支,服务器可能需要为每个客户端生成不同的包,以避免发送它们已有的对象。创建这些响应会消耗 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,其中包含一个或多个提交。每次向侧分支的推送都会再次包含该主题较早的提交,因此拥有所广播的 trunk 版本的客户端可以一次性获取该主题的整个 pack。

这是 因果一致性的一种形式。为了获取一个引用,读取者会观察产生它的写入者的历史。在推送过程中,Pando 控制客户端和服务器就哪些共享历史达成一致,确保客户端生成更容易共享的 packfile。

因此,Pando 在 Git 操作期间不提供所有引用的某个时间点状态,也不提供 clone 或 fetch 中 ref 更新的顺序。具体来说,假设分支 foo 和 bar 在一次完整 clone 的协商过程中都被更新。在协商期间,一个客户端可能观察到 foo 处于版本 1 而 bar 处于版本 2,而另一个客户端观察到 foo 处于版本 2 而 bar 处于版本 1。在实践中,用户和代理对 ref 更新的全局顺序并不敏感,而是依赖于对单个引用的更新顺序。

比较各种方法

在这个对三种一致性模型的模拟中,我们配置了 1,000 个客户端,每个客户端选择一个不同的分支,然后反复推送唯一的提交,并测量总实际耗时以及从推送到获取的延迟。后者是负载的一个重要指标,因为它衡量用户和代理协作或使用依赖版本控制的其他系统的往返时间。该模拟对从普通到高端虚拟机及 blob 存储系统的硬件进行建模。在此模拟中,我们为三阶段提交放宽了仓库范围的一致性。

模拟 Git 服务器实现

通过反复的推送跟踪每个写入者,并单独测量独立读取者何时可以检出每个提交。

InfrastructurePlaybackWritersSimulate mergeBatch trunk merges

三阶段提交

ReadySimulated time0 sMean push to fetch—Writers done0 / 1,000

每个方块代表一个写入者。所有选定的架构运行相同的负载。当推送被确认后方块变浅,写入者完成时变为蓝色。图表显示随模拟时间推移已完成的写入者;独立读取者在此之后仍可继续获取。使用方向键选择写入者,使用 Home 或 End 跳转,使用 Escape 清除。

0 / 10,000 pushes acknowledged · 0 fetched

选择一个方块以检查该写入者。

对象存储 WAL

ReadySimulated time0 sMean push to fetch—Writers done0 / 1,000

每个方块代表一个写入者。所有选定的架构运行相同的负载。当推送被确认后方块变浅,写入者完成时变为蓝色。图表显示随模拟时间推移已完成的写入者;独立读取者在此之后仍可继续获取。使用方向键选择写入者,使用 Home 或 End 跳转,使用 Escape 清除。

0 / 10,000 pushes acknowledged · 0 fetched

选择一个方块以检查该写入者。

Pando

ReadySimulated time0 sMean push to fetch—Writers done0 / 1,000

每个方块代表一个写入器。所有选中的架构运行相同的工作负载。方块随着推送被确认而变亮,并在写入器完成时变为蓝色。图表显示了在模拟时间内完成的写入器;独立的读取器可以在之后继续获取。使用方向键选择写入器,使用 Home 或 End 键跳转,使用 Escape 键清除。

已确认 0 / 10,000 次推送 · 已获取 0

选择一个方块以查看该写入器。

写入器经过已验证获取的进度

  • 三阶段提交
  • 对象存储 WAL
  • Pando
模拟时间0 s
  • 等待中
  • 推送中
  • 所有推送均已确认
0 秒后

合成教学模型,而非生产环境基准。平均推送至拉取时间涵盖主题推送,从首次尝试到重试,直至独立读取者验证该提交为止。包括等待读取者槽位的时间,不包括构建和测试。在启用合并的情况下,写入者完成包括所需的拉取和主干合并。

结果

播放对比以查看录制的成绩。

合成教学模型,而非生产环境基准。平均推送至拉取时间涵盖主题推送,从首次尝试到重试,直至独立读取者验证该提交为止。包括等待读取者槽位的时间,不包括构建和测试。在启用合并的情况下,写入者完成包括所需的拉取和主干合并。

运行这个将 Pando 与另外两种方法进行比较的模拟。选择从普通到高端的硬件基础设施、客户端数量等更多设置,查看它们的对比表现。

所建模的三阶段提交系统在其磁盘饱和时显示出显著更高的写入延迟;与此同时,预写日志系统出于同样的原因显示出显著更高的读取延迟,因为服务器难以跟上从日志读取的速度。在我们 monorepo 的实际测试和本模拟中,Pando 都支持数千个客户端并发推送,而不会影响负载下的性能。

在处理推送时,Pando 在将传入的 packfile 流式传输到 blob 存储的同时对其进行验证,而不会将其解包到本地仓库。当客户端开始拉取时,Pando 选择读取者所需的已存储 pack,并由读取者下载。没有任何服务器需要将 packfile 应用到本地仓库或执行代价高昂的解包和打包操作。由于这些服务器主要执行协商和 packfile 选择,Pando 服务器可以运行在普通硬件上。

这使得 Pando 可以扩展到客户端、客户端与 Pando 之间的网络以及 blob 存储服务的极限。在容量足够的情况下,在独立分支上工作的客户端都能以其自身连接所允许的速度传输 pack。要以该速度服务更多客户端,我们可以添加 Pando 服务器,并同步扩展元数据存储和 blob 存储。

在 Pando 中推送和拉取分支

Pando 将写入者的 packfile 存储在 blob 存储中,并将 ref 版本记录在 Turtle 中。拉取时,Pando 选择读取者所需的 pack,并由 Express 提供它们。

流式传输推送

  • Packfile 字节
  • 元数据
  • 请求 / 确认
  • Packfile URI

一个智能体在本地编辑文件、创建提交,然后运行 git push,它会创建一个包含这些更改的 packfile 来更新分支。Pando 接收该 pack 并将其流式传输到 blob 存储。通过单次解码验证传入的推送后,Pando 使用多键事务在元数据存储中记录一个 ref 版本,并原子性地更新 ref 指针。这使 Pando 能够提供“读你所写”一致性。整个写操作只需 O(1)O(1) 次元数据读写。

当另一个客户端拉取该分支时,Pando 会根据客户端已有的历史记录选择一组已存储的 pack。此操作需要 O(log⁡Δ)O(\log \Delta) 次元数据读取,其中 Δ\Delta 为客户端已有的内容与其想要的内容之间在 ref 版本上的最短距离。服务器返回 packfile URI,客户端通过一个将 packfile 存储在区域缓存中的服务 Express 下载它们。

该设计依赖于 Pando 架构的三个核心服务:推送、拉取和压缩。

扩展推送

当客户端推送时,传统的 Git 服务器就像一个亮出所有牌的扑克玩家,向客户端展示它拥有的每个分支。然后客户端收集提交、树和 blob,创建一个 排除 它知道服务器已具有的对象的 packfile。

通告更多的 ref 可以使推送更小,但也可能使 pack 更难被复用,因为另一个客户端可能缺少写入者省略掉的对象。

Pando 采取了不同的方法。它并不展示全部底牌,而是只向客户端通告一组精选的分支。这样做有两个原因:

  1. 为每个 ref 提供一致的视图开销很大。当分支数达到数百万时,仅 ref 通告本身就可能超过 100 MiB。提供这些内容需要每台服务器要么维护一份当前映射,需要大量协调开销,要么执行一次随 ref 数量扩展的数据库查询。

  2. 通过只向客户端提供 refs 的一个子集,客户端生成的包将仅依赖于服务器明确允许客户端排除的内容。通过跟踪这些依赖关系,Pando 可以复用客户端在一次推送中生成的 packfile,将其作为后续 fetch 的响应返回。

随着 Git 服务器通告更多 refs,推送的客户端有更多来源可以用于排除对象,从而生成更小的包。然而,复用这些 packfile 要求服务器能够证明执行 fetch 的客户端拥有全部依赖项。否则,packfile 可能遗漏客户端所需的对象。

另一方面,如果 Git 服务器完全不通告任何 refs,客户端将生成一个包含仓库完整历史的 packfile。该 packfile 可以被任何其他客户端使用,但代价非常巨大:抓取任何分支都实际上相当于一次完整克隆。

通告更多 refs 会产生更小的 packfile。大部分体积缩减仅通过通告一个分支即可实现。

默认情况下,当客户端推送一个新的功能分支时,Pando 只通告一个指定的主干(trunk)分支作为基线。这使通告的大小不会随仓库分支增多而增长,但也带来了权衡。当工程师多次推送到同一个分支,或将一个功能分支堆叠在另一个之上时,客户端必须重发主干之外的所有内容。

这会导致连续的分支推送变得更大,包含可能冗余的对象,并消耗额外的客户端带宽。在我们的 monorepo 中,以这种方式生成的 packfile 中位大小低于 20 KiB,第 99 百分位低于 3 MiB。对我们来说,这是一个合理的权衡,因为已经拥有所通告的 trunk 历史的读者可以用那一个 packfile 获取分支。

在选择了要向客户端通告的 ref 集合之后,Pando 接受推送并更新其元数据存储。Pando 将元数据更新拆分为同步和异步两部分。同步元数据更新确保客户端可以立即获取新更新的 commit。这会写入一个新的 ref 版本,并将 ref 指针映射更新到该新版本,存储该新 tip、用于协商 packfile 的依赖项,以及指向被推送 packfile 的链接。

与此同时,异步工作被记录在同一事务中,例如调度压缩(compaction)。对于非常大的推送,只有一部分 commit 会被同步写入。这使得无论推送规模多大,我们只需要对元数据存储进行常数次数的读取和写入。

重要的是,Pando 服务器可以独立地接受对不相关 ref 的推送。唯一有争用的键是 ref 指针,我们使用一种类似原子比较并交换的操作来保护它。我们可以通过增加 Pando 服务器、元数据存储扩展单元或 blob 存储来扩展写入吞吐量。

线速的 fetch 和 clone 性能

当客户端执行 fetch 时,它向服务器提供两组 commit ID,在协议中分别称为 haves 和 wants。在传统的 Git 服务器中,服务器计算 haves 可达对象与 wants 可达对象之间的集合差,将其压缩,并把结果发送给客户端。虽然开源 Git 服务器在本地拥有多种索引来加速这一操作,但这些 packfile 的计算开销可能很高,而且通常难以缓存。

相反,Pando 使用推送时记录的 commit 映射来查找客户端的 haves 和 wants,将它们解析为包含这些 commit 的一组 ref 版本。这些 ref 版本中的每一个都链接到一个 Pando 此前接收过的 packfile。服务器利用这一点来求解客户端所需的内容,选择覆盖客户端所需对象集合的 packfile。

在 Pando 中规划 fetch

Pando 将客户端已拥有的内容与其想要的内容进行比较,然后选择所需的已存储 packfile——包括任何缺失的分支依赖项。

已拥有Fetch缺失的分支依赖项

获取 27 个 trunk 包。

选择一个分支以包含其 trunk 依赖项。

Pando 将 Git 协议的 haves 和 wants 映射到一系列 ref 版本。想要一个侧分支意味着想要它在 trunk 上直至其依赖项的所有内容。

Git 协议只允许在 clone 或 fetch 的 HTTP 响应中发送单个 packfile。然而,Git 协议中一个被低估的特性,即 packfile-uri,允许服务器返回链接以单独下载额外的包。Pando 专门使用这一特性,并在线传输一个空的 packfile。为支持这一特性,OpenAI 向 packfile-uri 上游贡献了许多改进。

OpenAI 系统运行在多个地理区域,因此我们在每个区域维护带有本地缓存的 Pando 集群。一个名为 Express 的包分发服务缓存我们的 blob 存储,另一个单独的服务缓存元数据存储。这些缓存使 Pando 能够完全从该集群的缓存状态响应针对某个 commit ID 的请求或对 ref 的特殊读取。我们使用一致性哈希将针对 packfile 的请求路由到 Express 服务器,缓存命中率超过 99%。

这套缓存系统提供了极高的吞吐量,能够打满客户端与 Pando 之间的网络上限。通过使用这些 packfile URI 和本地缓存,我们已验证,仅使用标准的云提供商虚拟机,clone 和 fetch 操作就能打满 10 Gbps 的网络链路。

压缩与首字节时间

对于大多数 Git 服务器而言,重新打包和垃圾回收是维护不断增长的仓库性能所必需的。这两个步骤都能回收空间,但计算和 I/O 密集。

在像 OpenAI 的 monorepo 这样快速变动的仓库中,为自上次 fetch 以来发生的每一次推送都发送一个单独的 packfile URI 是不可行的。每天,客户端都需要下载数以万计的 packfile 才能追上其 trunk。

相反,Pando 在后台合并包以减少这种开销,采用了类似于日志结构合并树(log-structured merge-trees)中 compaction 的设计。每次推送都会产生一个 Pando 称为 “L0” 的 packfile,它代表 Pando 可传输的最小数据单元。随着时间的推移,后台工作进程将这些 packfile 收集为跨越一系列 ref 版本的 cover,从 L0 开始按几何级数扩展。目前,我们的 monorepo 最多使用到 L4 packfile cover。

这些 cover 具有双重用途。当一个 cover 足够满时,compaction 工作进程会认领它并生成单个 packfile。这通过重新组织 packfile 内部的对象来改善压缩比。但它还作为 ref 版本之上的跳表(skip list),改善了 fetch 协商性能。通过同时扫描每一层的索引,Pando 可以在对数时间内选出要发送给客户端的一组包。这些 packfile cover,无论是否经过重新打包,都为 fetch 规划提供了更高效的解决方案,降低了 Pando 的吞吐量和延迟要求。

Pando 中的 compaction

小的 packfile 被分组为跨层级的更大 cover,从而改善压缩并加速 fetch。

L2L1L0压缩进行中已发布的封面

后台压缩将来自多个引用版本的 packfile 合并,从而以更少的文件实现更高效的抓取。

与许多 Git 服务器不同,packfile 在每一层都会保留,不会被丢弃或垃圾回收。每一层的 pack 都会被持续用于服务客户端,依据客户端在抓取中请求的 L have0 和 L want4。对于使用 Pando 作为 API 的客户端,从 packfile 中读取任意对象时,从 L0 读取的数据量通常少于从 L4 读取的数据量,因为 L0 中的对象更不容易被压缩。

Pando 中的压缩显著减少了发送给客户端的总传输大小,并使 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 时的性能,我们开始向开发者机器以及 ChatGPT 桌面应用内置交付我们的 Git 公共发行版。为了将客户端迁移到 Pando,我们使用了 Git CLI 的内部包装器,分阶段迁移客户端。我们还创建了许多其他工具,用于处理变更请求、代码审查,以及供自动化使用的 API。我们在迁移 Git 流量的同时,迁移了数百个依赖遗留 API、webhook 和仓库下载的集成。

Codex 本身也帮助开发者和团队完成迁移。我们创建了用于准备现有检出以及引导智能体使用新系统的技能。因此,开发者只需让 Codex 迁移他们的开发环境和服务即可。

切换到 Pando 提升了合并的可靠性,并将中位数推送的延迟降低了近 80%。

Pando 仅用少数几个 CPU 核心就能每天交付 1 PB 的 Git 数据。Pando 曾在一小时内服务了超过 57 TB 的 Git pack,其中超过 99% 的字节来自本地磁盘缓存。

复用 pack 使许多客户端能够在服务节点上只需极少计算即可下载相同的仓库历史。这个例子展示的是单个仓库,但 Pando 的设计支持多得多的仓库。随着需求增长,我们可以独立扩展数据交付和 Git 协商。

回馈 Git 项目

在 Git 协议和客户端之上构建 Pando 帮助我们发现了 bug 以及改进 Git 自身性能的机会。

例如,Pando 直接向客户端提供可复用的 pack,因此下载和索引这些 pack 成为使系统快速且可靠的核心环节。这项工作暴露了并发下载、中断传输后的清理,以及恢复已完成下载等方面的边界情况。我们修复了这些路径,在接收现有 pack 用于抓取时增加了校验,并将这些工作回馈到上游。

我们还需要了解时间和带宽的去向,因此我们为 packfile-uri 下载和 pack 生成的字节数添加了追踪,使我们更容易判断一次缓慢的抓取究竟是把时间花在生成 pack 还是传输 pack 上。

在 OpenAI 的 Git 构建⁠(opens in a new window)中,客户端可以并行下载和索引独立的 pack。我们添加了“显式 haves”的概念,使服务器能够将分支的当前顶端与推送客户端可能未包含在其 pack 中的历史分开通告。在 Pando 上,这使客户端能够使用 --force-with-lease 来检查功能分支是否未移动,并仅针对主干构建其 pack。任何拥有该主干历史的人随后都可以使用同一个 pack 抓取该分支。

其中几项改进已经进入上游 Git,还有许多改进已收录在我们的公开仓库中。我们将继续与 Git 社区合作,让这些改进在 Pando 之外也能发挥作用。你可以在我们的 公开补丁追踪器⁠(在新窗口中打开).

接下来做什么

通过重新思考关于如何扩展 Git 仓库的传统观念,Pando 使我们能够在不改变工程师和智能体已在使用的 Git 工作流的情况下扩展我们的单体仓库。在我们的单体仓库中,Pando 带来了更高程度的并行性、更高的合并可靠性,并缩短了发布代码变更所需的时间。除了单体仓库,我们还在探索 Pando 在 OpenAI 内部的其他用途,包括需要大量智能体在共享 Git 仓库上协作的工作负载。

随着工程师和智能体产出更多软件,更多 OpenAI 之外的团队将遇到我们在源码控制中碰到的限制。我们目前正在探索向用户分享使 Pando 成为可能的这项技术的方式。

Pando 建立在 Git 仓库用户和运营者二十年经验的基础之上。我们将继续开发 Pando,同时与开源 Git 社区合作,分享任何能让更广泛的 Git 用户受益的改进。

原始出处

OpenAI

内容说明

原始发布及相关权利归来源方。

机器翻译 · 请以原文为准