在线AI教程 / AI入门
[OpenAI] 在 OpenAI 规模化 Git
在 OpenAI 规模化 Git 2026 年 11 月 2 日 工程 在投入 Git 生态的同时,构建 agent 级规模的源代码管理。 作者:Aaron Friel、Taylo
在 OpenAI 规模化 Git
2026 年 11 月 2 日
工程
在投入 Git 生态的同时,构建 agent 级规模的源代码管理。
作者:Aaron Friel、Taylor Blau 与 Ted Nyman,Members of Technical Staff
过去一年里,我们 monorepo 的规模以及它所承受的需求,都以惊人的速度增长。很显然,我们需要改造自己的源代码管理系统,让它能扩展到 Codex 所开启的那种并发工作水平。
我们构建了 Pando——一个面向 Git 仓库的新后端,期间既没有打断任何工程工作,也没有改变团队使用 Git 的方式。如今 OpenAI monorepo 的全部开发都由 Pando 支撑。Pando 的设计要达成两个颇有雄心的目标:第一,可横向扩展的写入,并针对向众多分支并发推送做了优化;第二,把读取吞吐最大化,让客户端下载数据的速度足以完全跑满其可用的网络带宽。
我们在保留与 Git 协议和 Git 客户端兼容的前提下达成了这些目标,让工程师和 agent 都能无缝地把 Pando 纳入自己现有的工作流。在生产环境中,Pando 把推送延迟的中位数降低了近 5 倍,如今每天交付近一个 PB 的 Git 数据。
在本文里,我们会讲到构建一套新源代码管理系统的动机,以及为什么我们选择保留与 Git 的兼容。我们会把自己的系统与常见的 Git 扩容思路做对比,说明为什么 Pando 独一无二地能够满足 OpenAI 的扩展性需求。我们也会谈下一步做什么,以及我们将如何继续为 Git 做贡献,以支撑 agent 规模的工作。
为什么我们需要新的源代码管理系统
Codex 改变了 OpenAI 开发的规模与节奏,让工程师能够并行完成多得多的工作。这意味着更多的分支和推送、更多的合并,以及来自开发环境、研究任务和 CI 的更多 fetch 与 clone。
这些需求会相互叠加。如今一名工程师会同时把好几个变更准备好送进 CI,而每一次推送都可能触发多个任务去 fetch 同一个 commit。
大约从去年年底开始,支撑我们 monorepo 的那些系统就有些跟不上了。我们饱受缓慢的 fetch 与推送之苦,合并也时常失败,可靠性一度跌破 90%。是我们触及了 Git 本身的极限,还是触及了我们服务 monorepo 那套方式的极限?Git 诞生于一个通过邮件交换内核补丁的社区。二十多年来,开发者们一直围绕着 Git 调整自己的工具,以适应构建软件方式的变化。当我们开始在自己的 monorepo 里感到阻力时,能够继续建立在这些工作之上,对我们而言很重要。
我们看到了一条路:保留那套熟悉的工作流,同时重建后端,让它并行处理推向不同分支的请求,并向众多读取方并发提供同一份历史。把 Git 作为源代码管理工作流的地基,意味着客户端层面的改进在 OpenAI 之外同样有用。我们希望把这些改进从我们的 Git 副本贡献回上游,与更广泛的社区共享。
为规模化的 Git 做工程
扩展一个源代码管理系统有两个本质维度:众多写入方推向不同分支,以及众多读取方 fetch 同一份历史。Pando 对这两者都做了优化。
第一,Pando 独立地协调每个分支的更新。互不相关分支的更新可以并行推进,而 fetch 某个分支的读取方会得到该 revision 所需的历史。代价是,同时查看多个分支的读取方看到的可能是不同时点上的状态。
第二,Pando 在多次 fetch 之间复用 packfile(Git 把多个对象一起压缩的格式),而不是为每个客户端单独生成一个 packfile。客户端可能会下载一些它已经拥有的对象,但服务端几乎不需要计算,就能把同样的字节提供给许多客户端。
这些选择让我们能够跨分支扩展写入,并从缓存提供服务端读取。要弄清它们如何衔接,我们先从 Git 如何存储和传输历史说起。
Git 对象存储入门
Git 必须能让仓库的两部分保持同步:对象(objects)与引用(references)。
Git 仓库把文件的快照,以及把这些快照连接起来的历史,记录成一张对象图。一个对象可以是 blob、tree、commit 或 tag。blob 保存单个文件的内容;tree 把名字关联到 blob 或其他 tree,从而构成目录结构。commit 指向一个根 tree 以及零个或多个父 commit,由此形成你仓库的历史。
对象由其类型与内容的哈希来标识。对象同时也是不可变的:记录一个被修改的文件会创建新的 blob,而未变动的文件和目录可以复用已有的对象。
分支和 tag 都是指向 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 以「松散」(loose)或「打包」(packed)两种状态存储对象。松散对象存放在以对象 ID 标识的独立文件里。打包对象则一起存放在一个 packfile 中,使相似的对象能够互为 delta 存储。
Git 既可以把 blob 存成独立文件,也可以把它们打包在一起,把一个 blob 编码成相对另一个 blob 的 delta。
在 Git 里,如果 Git 能顺着某个 commit 经过其父提交、tree 和 blob 的链接找到某个对象,那么这个对象对该 commit 而言是可达的。fetch 期间,客户端会告诉服务端它想要哪些 commit、已经拥有哪些。服务端找出从想要的那些 commit 可达的对象,排除它知道客户端已有的部分,再把剩下的以一个 packfile 发送出去,格式与 Git 在磁盘上使用的相同。完整 clone 一开始没有任何对象,因此它需要被 clone 的那段历史所可达的全部对象。
推送则以相反的方向传输对象:它请求服务端更新一个或多个 ref。客户端利用服务端公布(advertise)的那组 ref,避免发送服务端已经拥有的对象。因此被发送的 pack 可以依赖于已存在的对象:只收到这个 pack 并不足以重建出新历史。
复制一个 Git 仓库,意味着在多台服务器上同时保证对象和引用都可用。在一个副本能够提供某个引用的新值之前,它必须能够给出从该值可达的那些对象。这个服务还需要协调相互竞争的引用更新,也就是那些试图把同一分支从同一个旧 tip 移到不同新 commit 的推送。对仓库的所有写入强制施加单一顺序的系统,会让推向互不相关分支的推送彼此等待。
现有的分布式 Git 服务范式
在设计 Pando 时,我们评估了用 Git 扩展源代码管理的常规做法:一种是用三阶段提交(3PC,一个在多个节点之间协调提交的协议)在各仓库副本之间协调 ref 更新;另一种是通过 blob 存储里一份共享的预写日志(WAL,在变更生效前先写下的变更记录)为更新排序。
三阶段提交(3PC)
在我们建模的三阶段提交设计里,Git 服务在若干副本上各保存仓库的一份完整拷贝。任何一台服务器收到推送时,进来的 packfile 会被复制并并行发送到所有副本。要发布这次推送,必须有多数副本接受该 ref 更新。负责管理这件事的服务器就是协调者,而协调者与其他每个副本之间需要多轮通信。
要接受一次引用更新,每个副本会锁住自己那份 ref,并检查它是否仍指向客户端预期的那个 commit。随后协调者向已接受的副本发送 pre-commit 消息。一旦多数副本确认了 pre-commit 消息,协调者就通知它们执行更新。此后,任何一个最新的副本都能从自己本地的仓库拷贝中公布新的分支 tip。
保存完整的副本使得本地服务读取成为可能,并保证客户端无论选择哪个副本,都能得到所有引用的一致视图。其结果是,引用只能一次更新一个,而每次推送都需要多个往返,以便在各副本之间协调这次 ref 更新。
等待法定人数(quorum)会把副本的磁盘性能和通信延迟放到推送路径上。随着写入量增长,副本不仅要跟上涌入的 pack,还要跟上读取流量。
预写日志(WAL)
在我们建模的基于 WAL 的设计里,Git 服务使用 blob 存储中一份共享的清单(manifest)来为整个仓库的更新排序。服务器收到推送时,会把进来的 packfile 上传到 blob 存储,并在自己本地的仓库拷贝中准备好这次 ref 更新。要发布这次推送,它会在存于 blob 存储的共享清单里追加一条记录该 ref 更新、并指向那个 packfile 的条目。随后 Git 服务器用 compare-and-swap 操作更新清单:要么原子地替换整份清单,否则就拒绝这次改动。如果在此期间有别的写入方更新了它,服务器会读出中间插入的那些条目,检查自己的 ref 更新是否仍然有效,并用自己已经上传的那个 packfile 重试。
写入方可以并行上传各自的 packfile,但即便是推向互不相关分支,也需要更新同一份清单。一次成功的 compare-and-swap 的往返时间,限制了仓库能多频繁地发布新清单。低延迟的 blob 存储能让这一步很快。批量处理则让一次清单更新能同时改动多个 ref,代价是更新需要排队、延迟上升。
blob 存储可以并行接收 packfile,但共享清单的更新仍然彼此竞争。单纯扩大 blob 存储的容量并不能解决那个瓶颈。每个副本仍需先从 blob 存储读取 pack 并为它们建立索引,才能在本地提供服务。如果涌入的写入快过这部分工作,副本就会落后,客户端 fetch 新推送的 commit 也要等更久。
Pando 的架构设计
这就说到了 Pando。Pando 是一套用于服务 Git 仓库的新架构,目标是扩展到大量客户端同时推送。它是这样工作的:
重新思考约束
在开发 Pando 之前,我们有一种预感:可以在不牺牲 Git 的前提下满足自己的需求,但确实需要在一些事情上做出让步——而大多数 Git 系统把这些事情当成硬性要求:
全仓库一致性:要提供所有 ref 的一致、时点快照,就需要在整个仓库范围内协调。在一个把所有 ref 更新放在一起排序的系统里,推向互不相关分支会争抢同一个协调点。 每个客户端都要有一份唯一的 packfile:如果两个本地历史不同的客户端 fetch 同一个分支,服务端可能需要为每个客户端生成不同的 pack,以免发送它们已经拥有的对象。构造这些响应要消耗 CPU 和磁盘 I/O,很难扩展到大量读取方。
作为替代,Pando 提供一个放宽的一致性模型(因果一致性)来扩展写入,并复用 packfile 来扩展读取。
因果一致性(CC)
当客户端向某个分支推送时,Pando 把进来的 packfile 作为不可变 blob 存入 blob 存储。分支每移动一次,Pando 就在元数据存储里记录一个 ref 版本,其中包含新的 tip、提供这个 tip 的那个 pack,以及该 pack 所依赖的更早的 ref 版本。blob 存储和元数据存储都能横向扩展。
从 Pando fetch 一个 commit 或引用,需要把一组 ref 版本(packfile)回放给客户端,从而重建出创建被 fetch 的那个 ref 版本的客户端当时的状态。这在整个仓库范围内提供了一种较弱形式的一致性,同时为单个 ref 的更新提供了可串行化。只有当多个客户端同时推向同一个 ref 时才会冲突。一个读取多个分支的客户端,不能假设这些读取描述的是全仓库的同一个时间点。
推向 trunk 会增加一个增量 pack,其中包含一个或多个 commit。而每次推向旁支分支都会把该 topic 早先的 commit 再次带上,因此持有已公布 trunk 版本的客户端可以用一个 pack 就 fetch 到整个 topic。
这就是因果一致性的一种形式。要 fetch 一个引用,读取方会观察到产生它的那个写入方的历史。在推送期间,Pando 控制客户端与服务端就哪一段共享历史达成一致,从而确保客户端产出更容易共享的 packfile。
因此,Pando 不提供 Git 操作期间所有引用的时点状态,也不提供一次 clone 或 fetch 之内 ref 更新的顺序。具体来说,假设分支 foo 和 bar 在一次完整 clone 的协商过程中都被更新了。在协商期间,一个客户端可能看到 foo 处于版本 1、bar 处于版本 2,而另一个客户端看到 foo 处于版本 2、bar 处于版本 1。实践中,用户和 agent 并不关心 ref 更新的全局顺序,它们依赖的是对单个引用的更新顺序。
各方案对比
在这份对三种一致性模型的模拟中,我们配置 1,000 个客户端,每个各自选定一个不同的分支,然后反复推送独有的 commit,并测量总墙钟时长与「推送到可 fetch」的延迟。后者是工作负载的一项重要指标,因为它衡量的是用户和 agent 相互协作、或使用其他依赖版本控制的系统所需的往返时间。该模拟对硬件的建模范围从普通到高端虚拟机,也包括 blob 存储系统。在这次模拟中,我们为三阶段提交放宽了全仓库一致性。
模拟 Git 服务器实现
跟随每个写入方经历反复推送,并另外测量独立的读取方何时能够 check out 每个 commit。
基础设施
标准快速极速
回放
1×5×10×30×60×
写入方
1002004006001,000
模拟合并
批量处理 trunk 合并
三阶段提交
就绪
模拟时间
0 s
平均推送到可 fetch
—
写入方完成
1,000 / 1,0000 / 1,000
每个方块代表一个写入方。所有被选中的架构都跑同一份工作负载。随着推送被确认,方块会变浅;某个写入方完成时它变成蓝色。图表按模拟时间展示已完成的写入方数量;在此之后独立读取方仍可以继续 fetch。用方向键选择一个写入方,用 Home 或 End 跳转,用 Escape 清除选择。
0 / 10,000 次推送已确认 · 0 次已 fetch
选择一个方块以查看那个写入方。
对象存储 WAL
就绪
模拟时间
0 s
平均推送到可 fetch
—
写入方完成
1,000 / 1,0000 / 1,000
每个方块代表一个写入方。所有被选中的架构都跑同一份工作负载。随着推送被确认,方块会变浅;某个写入方完成时它变成蓝色。图表按模拟时间展示已完成的写入方数量;在此之后独立读取方仍可以继续 fetch。用方向键选择一个写入方,用 Home 或 End 跳转,用 Escape 清除选择。
0 / 10,000 次推送已确认 · 0 次已 fetch
选择一个方块以查看那个写入方。
Pando
就绪
模拟时间
0 s
平均推送到可 fetch
—
写入方完成
1,000 / 1,0000 / 1,000
每个方块代表一个写入方。所有被选中的架构都跑同一份工作负载。随着推送被确认,方块会变浅;某个写入方完成时它变成蓝色。图表按模拟时间展示已完成的写入方数量;在此之后独立读取方仍可以继续 fetch。用方向键选择一个写入方,用 Home 或 End 跳转,用 Escape 清除选择。
0 / 10,000 次推送已确认 · 0 次已 fetch
选择一个方块以查看那个写入方。
写入方通过已验证 fetch 取得的进度
三阶段提交 对象存储 WAL Pando
模拟时间0 s
等待中 推送中 全部推送已确认
0 s之后
这些是用于教学的合成模型,不是生产基准。平均「推送到可 fetch」覆盖 topic 推送,从第一次尝试起、经过重试,直到某个独立读取方验证该 commit 为止。它包含等待读取方名额的时间,不含构建和测试。启用合并时,写入方的完成还要算上必需的 fetch 与合并进 trunk。
结果
播放这段对比,即可看到录制下来的结果。
这些是用于教学的合成模型,不是生产基准。平均「推送到可 fetch」覆盖 topic 推送,从第一次尝试起、经过重试,直到某个独立读取方验证该 commit 为止。它包含等待读取方名额的时间,不含构建和测试。启用合并时,写入方的完成还要算上必需的 fetch 与合并进 trunk。
运行这个把 Pando 与另外两种方案做对比的模拟。你可以选择从普通到高端的硬件基础设施、客户端数量以及更多设置,看看它们相比如何。
我们建模的三阶段提交系统在负载下表现出明显更高的写入延迟,因为它们的磁盘被打满;与此同时,预写日志系统由于同样的原因表现出明显更高的读取延迟——服务器难以跟上从日志中读取的速度。无论是在我们 monorepo 上的真实测试,还是在这次模拟中,Pando 都能支持数千个客户端并发推送,且在负载下不影响性能。
在处理推送时,Pando 一边把进来的 packfile 流式写入 blob 存储,一边对其进行校验,而不必把它解包到本地仓库。当客户端开始 fetch 时,Pando 选出读取方需要的那些已存储的 pack,由读取方自行下载。没有任何服务器需要把 packfile 应用到本地仓库,也不需要执行昂贵的解包与打包操作。由于这些服务器主要做的是协商和 packfile 选择,Pando 服务器可以跑在普通硬件上。
这让 Pando 的扩展上限取决于客户端本身、客户端与 Pando 之间的网络,以及 blob 存储服务。只要有足够的容量,在各自独立分支上工作的客户端都能以自己的连接允许的最快速度传输 pack。要在这样的速率下服务更多客户端,我们可以增加 Pando 服务器,并同时扩展元数据存储与 blob 存储。
在 Pando 中推送与 fetch 一个分支
Pando 把写入方的 packfile 存在 blob 存储里,并在元数据服务中记录 ref 版本。fetch 时,Pando 选出读取方需要的 pack,由 Express 把它们提供出去。
流式推送fetch 该分支
packfile 字节 元数据 请求 / 确认 packfile URI
agent 编辑文件并在本地创建 commit,然后运行 git push,它把这些改动生成一个 packfile 用来更新分支。Pando 收到这个 pack 并把它流式写入 blob 存储。在用一趟解码校验完进来的推送之后,Pando 用一个多键事务在元数据存储里记录一个 ref 版本,并原子地更新 ref 指针。这让 Pando 能够提供「读己之写」一致性。这整个写入操作只需要 $O(1)$ 次元数据读写。
当另一个客户端 fetch 这个分支时,Pando 依据该客户端已有的历史选出一组已存储的 pack。这一操作需要 $O(\log \Delta)$ 次元数据读取,其中 $\Delta$ 是客户端已拥有的与客户端想要的之间,以 ref 版本度量的最短距离。服务端返回 packfile URI,客户端通过一个名为 Express 的服务下载它们,该服务把 packfile 存放在各区域的缓存里。
这套设计依赖 Pando 架构的三个核心服务:推送、fetch 与压实(compaction)。
扩展推送
客户端推送时,传统的 Git 服务器就像一个把全部底牌亮出来的扑克玩家,把自己拥有的每个分支都告诉客户端。客户端随后收集 commit、tree 和 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 任何分支实际上都变成了一次完整 clone。
公布更多 ref 会带来更小的 packfile。而体积上的大部分缩减,只公布一个 ref 就能拿到。
默认情况下,当客户端推送一个新的 feature 分支时,Pando 只把一个指定的 trunk 分支作为基准公布出来。这样仓库不断新增分支时,公布内容的大小也不变,但也有代价。当工程师反复推同一个分支,或者把一个 feature 分支叠在另一个之上时,客户端就必须把 trunk 之外的内容重新发送一遍。
这使得连续的分支推送变得更大,其中包含可能冗余的对象,并额外消耗客户端带宽。在我们的 monorepo 里,这样产出的 packfile 中位数不到 20 KiB,99 分位不到 3 MiB。对我们来说这是一个合理的取舍,因为已经拥有公布的那份 trunk 历史的读取方,用这一个 packfile 就能 fetch 到该分支。
在选定要公布给客户端的那组 ref 之后,Pando 接受这次推送并更新自己的元数据存储。Pando 把元数据更新拆成同步和异步两部分。同步的那部分元数据更新确保客户端能立刻 fetch 到新更新的 commit:它写入一个新的 ref 版本,并把 ref 指针更新为指向这个新版本,其中存放着新 tip、用于协商这个 packfile 的那些依赖,以及指向被推送的那个 packfile 的链接。
与此同时,异步工作(比如安排压实)也在同一个事务里被记录下来。对于非常大的推送,只有一部分 commit 是同步写入的。这让我们无论推送大小如何,都只需要对元数据存储执行常数次的读写。
重要的是,Pando 服务器可以各自独立地接受推向互不相关 ref 的推送。唯一存在争用的键是 ref 指针,我们用一种原子的、类 compare-and-swap 的操作来保护它。我们可以通过增加 Pando 服务器、元数据存储的扩展单元或 blob 存储来提升写入吞吐。
达到线速的 fetch 与 clone 性能
客户端执行 fetch 时,会向服务端提供两组 commit ID,协议里称为 have 与 want。在传统的 Git 服务器中,服务端会计算这些 have 可达的对象与 want 可达的对象之间的差集,把它们压缩后发给客户端。虽然开源的 Git 服务器在本地有各种索引来加速这一操作,但这些 packfile 的计算可能十分昂贵,而且通常难以缓存。
Pando 则不同:它利用推送时记录的 commit 映射来查找客户端的 have 与 want,把它们解析为一组包含这些 commit 的 ref 版本。每个 ref 版本都指向一个 Pando 此前收到的 packfile。服务端据此求解出客户端需要什么,选出能覆盖客户端所需对象集合的那些 packfile。
Pando 中的 fetch 规划
Pando 把客户端已有的与想要的加以比较,然后选出所需的、已存储的 packfile——包括任何缺失的分支依赖。
已持有fetch缺失分支依赖
fetch 27 个 trunk pack。
选择一个分支,以把它的 trunk 依赖包含进来。
Pando 把 Git 协议的 have 与 want 映射成一串 ref 版本。想要一个旁支分支,就意味着想要它直到 trunk 的那些依赖。
Git 协议只允许在 clone 或 fetch 的 HTTP 响应里发送单个 packfile。但 Git 协议中一个被低估的特性 packfile-uri 允许服务端返回若干链接,用来另行下载额外的 pack。Pando 完全采用这一方式,并就地发送一个空的 packfile。为了支持这个特性,OpenAI 已就 packfile-uri 向上游贡献了多项改进。
OpenAI 的系统运行在许多地理区域,因此我们在每个区域都维护带有本地缓存的 Pando 集群。一个 pack 分发服务 Express 为 blob 存储做缓存,另有独立的服务为元数据存储做缓存。这些缓存让 Pando 能够完全依靠该集群中的缓存状态,来响应针对某个 commit ID 或某次特殊 ref 读取的请求。我们用一致性哈希把对某个 packfile 的请求路由到 Express 服务器,缓存命中率超过 99%。
这套缓存体系提供了极高的吞吐,足以把客户端与 Pando 之间的网络上限打满。借助这些 packfile URI 和本地缓存,我们已经验证:仅使用云厂商的标准虚拟机,clone 与 fetch 操作就能把 10 Gbps 的网络链路跑满。
压实与首字节时间
对大多数 Git 服务器而言,repack(重新打包)与垃圾回收是维持一个不断增长的仓库性能的必要操作。这两步都能回收空间,但都是计算密集和 I/O 密集的。
在像 OpenAI monorepo 这样高速运转的仓库里,为自上次 fetch 以来发生的每一次推送都单独发送一个 packfile URI 是不可行的。那样的话,客户端每天需要下载数万个 packfile 才能让自己的 trunk 追平。
Pando 的做法是在后台合并 pack 以降低这种开销,采用的设计与 log-structured merge-tree 中的压实相似。每次推送产出的就是 Pando 所称的「L0」packfile,它代表 Pando 能够传输的最小数据单元。随着时间推移,后台 worker 会把这些 packfile 收集成覆盖一串 ref 版本的 cover,并从 L0 起按几何级数增长。目前我们的 monorepo 使用到 L4 的 packfile cover。
这些 cover 有双重用途。当一个 cover 足够填满之后,压实 worker 会认领它并产出一个单独的 packfile。这通过重新组织 packfile 内部的对象提升了压缩率;同时也因为它充当了 ref 版本之上的一层跳表,而改善了 fetch 协商性能。通过同时扫描每个层级的索引,Pando 能够以对数时间为客户端选出一组要发送的 pack。这些 packfile cover,无论是否重新打包过,都为 fetch 规划提供了更高效的解法,降低了 Pando 对吞吐和延迟两方面的要求。
Pando 中的压实
小的 packfile 在各层级上被归集成更大的 cover,从而改善压缩并加快 fetch。
在已有 pack 之上发布一个 cover
L2
L1
L0
压实进行中已发布 cover
压实会在后台把来自许多 ref 版本的 packfile 合并起来,从而用更少的文件实现更高效的 fetch。
与许多 Git 服务器不同,Pando 在每个层级上都保留 packfile,既不丢弃也不做垃圾回收。每个层级的 pack 都持续用于服务客户端,依据是它们在 fetch 中请求的 have 与 want。而对于把 Pando 当作 API 使用的客户端,从 packfile 里读取任意一个对象时,从 L0 读取的数据量往往比从 L4 更少,因为它更不可能被压缩过。
Pando 中的压实显著降低了发送给客户端的总传输量,并让 pack 规划的开销随客户端落后的距离呈次线性增长。这对庞大而快速变化的仓库尤其重要,而我们预计这类仓库会成为软件开发中的新常态。
构建 Pando
我们用 Codex 构建了 Pando,从立项到上线只用了 10 周。Pando 的核心由一名工程师设计、由若干 agent 团队实现、并由少数几位工程师部署。Pando 从今年 4 月起开始服务生产流量,它是 OpenAI 所使用的、更大的源代码管理工具体系中的一环。
在验证 Pando 的设计时我们用上了各种各样的方法。我们贯彻了自己构建更安全的软件根基的原则。作为一个全新项目,我们选择用 Rust 实现 Pando,且不引入遗留依赖。我们编写了一套 conformance 测试套件,让 agent 能拿标准的 Git 行为来检验自己的改动。我们还开发了确定性模拟测试框架,使我们能够检查系统的不变量与性质,从而发现并缓解协商和 pack 规划中那些细微的竞态条件与回归。
许多模型都为这套系统做出了贡献;随着新模型可用,我们把它们用于提升 Pando 的性能与可靠性。借助 GPT‑6 Astra,agent 写下 Lean 和 TLA+,把「Pando 理论」形式化,我们用这套理论找出并修掉实现中的 bug,并证明系统的若干性质。多个不间断运行的会话跑了数百个小时,在 Pando 中发现并修复边界情况。我们还全天候运行 agent,用具备 Daybreak 能力的模型对 Git 和 Pando 做红队演练。
部署 Pando
Pando 最初的部署形态,是我们现有源代码管理系统的一个读写透传代理。这让 Pando 与之前的系统保持同步,从而让我们可以在不停机的前提下把流量安全地迁移到 Pando 上。
作为其缓存设计的一部分,Pando 还能在代理转发之前对请求做去重与合并。这让我们在把各个系统逐一迁到 Pando 的过程中,现有工具依然保持健康。随着更多工作负载迁到 Pando,它们的推送填满了 Pando 的缓存,也提升了我们旧后端的可靠性。
为了在使用 Pando 时把性能优化到位,我们开始把公开的 Git 发行版分发到开发者机器上,并内置进 ChatGPT 桌面应用。为了把客户端迁到 Pando,我们用一个包在 Git CLI 外部的内部封装分阶段迁移客户端。我们还创建了许多其他工具,用于处理变更请求、代码评审,并为自动化提供 API。我们迁移了数百个依赖遗留 API、webhook 与仓库下载的集成,与 Git 流量一起完成切换。
Codex 本身也帮助开发者和团队完成了这次迁移。我们创建了若干 skill,用于准备已有的 checkout,以及指导 agent 使用新系统。结果是,开发者只要让 Codex 迁移自己的开发环境和服务即可。
切换到 Pando 后,合并可靠性得到提升,推送延迟的中位数降低了近 80%。
Pando 仅用少数几个 CPU 核就能每天交付一个 PB 的 Git 数据。Pando 曾在单个小时内服务超过 57 TB 的 Git pack,其中超过 99% 的字节来自本地磁盘缓存。
复用 pack 让许多客户端可以下载同一份仓库历史,而服务节点上几乎不需要计算。这个例子展示的是单个仓库,但 Pando 的设计支持更多仓库。随着需求增长,我们可以独立地扩展数据交付与 Git 协商。
回馈 Git 项目
把 Pando 构建在 Git 协议和 Git 客户端之上,帮助我们发现了 bug,也发现了改进 Git 自身性能的机会。
举例来说,Pando 直接把可复用的 pack 提供给客户端,因此下载这些 pack 并为它们建立索引,成了让整个系统又快又稳的关键环节。这项工作暴露出并发下载、传输中断后的清理,以及恢复那些其实已经下载完成的下载等路径上的边界情况。我们修好了这些路径,在为 fetch 接受一个已存在的 pack 时加上了校验,并把这部分工作贡献回了上游。
我们还需要看清时间和带宽都花在了哪里,于是为 packfile-uri 下载加入了 tracing,并为 pack 生成加上了字节计数,这样就更容易判断一次缓慢的 fetch 究竟是在生成 pack 上耗时间,还是在传输 pack 上耗时间。
在 OpenAI 的 Git 构建中,客户端可以并行下载并为彼此独立的 pack 建立索引。我们加入了「explicit haves」这个概念,让服务端能够把某个分支当前的 tip 单独公布出来,与推送方可以从自己的 pack 中略去的历史分开。在 Pando 上,这让客户端可以用 --force-with-lease 检查某个 feature 分支是否已经移动,并仅以 trunk 为基础构建自己的 pack。任何拥有那份 trunk 历史的客户端,随后都能用同一个 pack fetch 到这个分支。
这些改进里已有若干合入了 upstream Git,还有许多在我们公开的仓库中。我们会继续与 Git 社区合作,让这些改进在 Pando 之外也有用。你可以在我们的公开补丁跟踪页上关注这项工作。
下一步
通过重新思考「如何扩展 Git 仓库」的常规观念,Pando 让我们能在不改动工程师和 agent 已在使用的 Git 工作流的前提下扩展我们的 monorepo。在我们的 monorepo 里,Pando 释放出了更高程度的并行性,改善了合并可靠性,并缩短了发布代码变更所需的时间。在 monorepo 之外,我们也在 OpenAI 内部探索 Pando 的其他用途,其中包括大量 agent 需要在一个共享的 Git 仓库上协作的工作负载。
当工程师和 agent 产出更多软件,OpenAI 之外的更多团队会碰到我们在源代码管理上撞到的那些极限。我们目前正在探索一些方式,把让 Pando 成为可能的技术与我们的用户共享。
Pando 建立在 Git 仓库的用户和运维者二十年积累的经验之上。我们会继续开发 Pando,并与开源 Git 社区合作,分享那些能让更多 Git 用户受益的改进。
作者