继 Anthropic 之后,OpenAI 爆改 Git

作者:AGI Hunt 发布:2026-11-03 10:41 收录:2026-11-03 12:48 2 次阅读 约 8665 字
摘要:继 Anthropic 推倒 git 自研 Scion 之后,OpenAI 保留 Git 只换后端,用 Codex 10 周打造出 Pando,push 延迟降近 80%
推荐理由:本文涵盖「Anthropic」、「OpenAI」、「Claude」等多个主题,重点关注 Anthropic。

刚刚,OpenAI 在工程博客上发了一篇长文,讲他们给自家 monorepo 换了一个新的 Git 后端,名叫 Pando。

现在 OpenAI monorepo 里的全部开发,都已经跑在 Pando 上了。

这事,怎么感觉有点眼熟?

其实。在三周前,Anthropic 也刚刚讲完他们是怎么把整个 monorepo 从 git 上迁走的,见:刚刚,Anthropic 重新发明了 git。

不到一个月,御两家便先后讲起了同一个问题。Claude 和 Codex 让写代码的速度暴涨,结果被那些为人类规模设计的源码控制给拖住了。

只是两家给出的解法,几乎是反着来的。

Anthropic 是把 git 推倒重来,自研了一整套版本控制系统 Scion。

而 OpenAI 则留下了 Git,客户端和协议都保持兼容,工程师和 agent 该怎么用还怎么用,只把服务端整个换掉了。

换完之后,中位 push 延迟降了近 80%,现在每天要分发近 1 PB 的 Git 数据。

源码控制的瓶颈

01

Anthropic 今年 5 月平均每天合并约 2500 次代码变更,到了 9 月,已经超过了每天 3 万次。

仅 main 分支的历史就接近 100 GB,CI 每天要 checkout 约 2500 万次,高峰时得靠几百台内部 git 镜像来扛。

OpenAI 这边,过去一年 monorepo 的体量和需求,也涨得飞快。

有了 Codex,工程师能同时推进的事情多了很多,于是分支更多了,push 和 merge 更多了,开发环境、研究任务和 CI 发起的 fetch、clone 也更多了。

而且这些需求还会叠加,一个工程师手上同时有好几个改动在等 CI,每一次 push 又会让好几个任务去 fetch 同一个 commit。

两家撞上同一堵墙
两家撞上同一堵墙

从去年年底开始,OpenAI 支撑 monorepo 的那套系统就渐渐扛不住了。fetch 和 push 都很慢,merge 也会失败,可靠性一度掉到了 90% 以下……

于是 OpenAI 开始琢磨,到底是 Git 本身到了极限,还是他们服务 monorepo 的方式到了极限呢?

两条路线

02

Anthropic 认为是 git 本身。

git 是为离线、分布式设计的,每一份 clone 都是一个完整的仓库。可当客户端几乎全是永远在线的 agent、仓库又越来越大时,git 最擅长的分布式和离线工作,反倒成了扩展的阻碍。

所以 Scion 从客户端、仓库格式到协议,全都是新的。查历史、做 blame、创建提交、merge 和 rebase 都交给服务端来做,客户端自己几乎不做计算,只维护一个有上限的本地缓存。

换车,还是换发动机
换车,还是换发动机

OpenAI 的答案则是后者。

Git 起源于一个靠邮件交换内核补丁的社区,二十多年来,开发者一直在围绕 Git 调整自己的工具和流程,OpenAI 不想丢掉这些积累。

所以 Pando 只是一个后端,工程师和 agent 手里用的还是原来那个 Git,工作流一点都不用改。

而且建在 Git 之上,客户端上做的改进,OpenAI 以外的人也能用得上。

提交与分支

03

Scion 里,每一次 commit、rebase、merge 都直接发生在服务端,压根没有什么需要 push 的。

它的基本单位叫 changeset,是预写日志里的一条记录,存的是相对第一个父提交改了哪些文件,以及指向文件内容的内容寻址指针。

创建一个 changeset 的开销只跟改动大小有关,跟仓库多大没关系,而且全程无锁。

除了 main 和发布分支,Scion 连 ref 都不太鼓励用,移动一个 ref,也只是对 Spanner 里的一行数据做一次 compare-and-swap。

Pando 则还守着 Git 的老规矩,先在本地 commit,再 push 上去。

不同的是,Pando 让每个分支各自独立更新,推到不同分支的 push 可以完全并行,只有同时推同一个分支时才会冲突。

提交在哪里落地
提交在哪里落地

代价是,同时看好几个分支的人,看到的未必是同一时刻的状态。

说起来,两家其实都放弃了「整个仓库在同一时刻保持一致」这件事。Anthropic 是干脆不鼓励用 ref,OpenAI 则让各个分支各管各的。

读取与 packfile

04

Scion 的客户端从不告诉服务端自己已经有什么,只说自己要什么。它从根树开始,查本地缺了哪些 tree 再递归地请求,最后对 blob 重复一遍。

每个请求都对应一个永远不会变的对象,所以 scion-daemon、scion-cache、scion-server 三层缓存都能直接应答。

Scion 里,也干脆没有 packfile 这种东西。

而 Pando 正好相反,它把 packfile 当成了存储和分发的基本单位。

push 上来的 packfile 原样存进 blob 存储;有人来 fetch 时,Pando 从已经存好的 pack 里挑出对方需要的那几个,直接把下载链接发给对方。

按件取,还是拿现成的包
按件取,还是拿现成的包

这样客户端可能会多下载一些本地已经有的对象,但服务端几乎不用计算,同一份数据可以原样发给大量读者。

两家的缓存命中率,也都在 99% 以上。

一家把 packfile 扔了,一家却把 packfile 当成了宝……

性能数据

05

Anthropic 公布的数字更全一些。

Scion 基础设施的 CPU 和内存占用,只有之前 git 镜像方案的 1/200。

开发环境里 status 快了 207 倍,agent 新建一个 worktree,从 7.1 分钟降到了 5.1 秒。

算下来,agent 每提交一个变更请求,花在源码控制上的时间从 34 分钟降到了 3.7 分钟,每天省下约 1.1 万个 agent 小时。

OpenAI 给的则是另一组。

merge 更可靠了,中位 push 延迟降低了近 80%;每天分发近 1 PB 的 Git 数据,用的只是少量 CPU 核。

各自的成绩单
各自的成绩单

它还曾在一小时内分发过超过 57 TB 的 Git pack(摊下来每秒将近 16 GB……),其中 99% 以上来自本地磁盘缓存(两家的口径不一样,这里就不硬比谁更快了)。

开发周期

06

Scion、代码评审工具 Antique,再加上另外六个左右的相关系统,是 Anthropic 一个 7 人团队花了 5 个月做完、并完成迁移的。

团队手写的导入管道原型,交给 Claude Mythos Preview 的一个早期快照重写之后,内存只剩原来的 1/20,速度快了 4 倍。

参与项目的工程师现在都认为 Claude 写代码比自己强,而他们很多时候的工作,就是对 Claude 说「不」。

而 OpenAI 这边,Pando 是用 Codex 造出来的,从立项到上线只用了 10 周。

核心设计只出自一位工程师,实现交给了多个 agent 团队,最后再由几位工程师部署上线。

7 人 5 个月 vs 1 人设计、10 周上线
7 人 5 个月 vs 1 人设计、10 周上线

另外,Pando 其实从今年 4 月就开始承接生产流量了,只是到今天才拿出来讲,OpenAI 的 Thibault Sottiaux 转发时也说,团队终于可以稍微讲一讲了。

所以「继 Anthropic 之后」,说的只是发文的先后。

对外分享

07

Anthropic 明确表示 Scion 不对外提供。

它和 git 差别太大,别家要用就得把整个开发流程都迁过去,还得配套它自己的代码评审、代码搜索和 CI。

不过他们在文末给 git 社区提了几个方向:git 服务端不必和客户端用同样的方式工作,git 平台可以提供全面的服务端 RPC,协商协议也不必是现在这个样子,各种 sparse 和 shallow 功能可以组合得更好。

关起门给建议,还是送补丁
关起门给建议,还是送补丁

OpenAI 走的则是另一条路。

他们把客户端上的改进贡献回了上游 Git,自家的 Git 版本也公开在了 GitHub 上,还做了个公开的补丁追踪页,并表示正在探索把 Pando 背后的技术分享给用户。

回头再看 Anthropic 提的这几条建议,「服务端不必和客户端用同样的方式工作」「协商协议不必是现在这个样子」……Pando 做的几乎正是这两件事。

Git 的对象与 ref

08

说回 OpenAI 这篇博客本身。

源码控制的扩展有两个基本方向,一是很多写者往不同的分支 push,二是很多读者 fetch 同一份历史。

Pando 给自己定的目标也是两个,写入要能横向扩展,读取则要快到能把客户端的带宽跑满。前面说的「分支各管各的」和「复用 packfile」,对应的就是这两个方向。

要弄明白这两个选择,得先看看 Git 是怎么存储和传输历史的。

一个 Git 仓库要同步的东西,分为对象和 ref 两种。

对象有四种。blob 存的是单个文件的内容;tree 把名字对应到 blob 或别的 tree,组成目录结构;commit 指向一棵根 tree 和零到多个父 commit,串起仓库的历史;另外还有 tag。

对象用「类型 + 内容」的哈希来标识,而且不可变。改了一个文件就会生成新的 blob,没改的文件和目录则直接复用已有的对象。

分支和 tag 则是指向 commit 的 ref。在分支上提交,就是新建一个 commit,再把分支的 ref 指过去。

Git 对象之间的引用关系
Git 对象之间的引用关系

比如图里,refs/heads/main 指向 C2,C2 的父提交是 C1。

两次提交里的 parser.py 分别指向 B1 和 B2,而 docs/ 目录没变,两棵根 tree 便共用了同一个 T3。

对象在 Git 里有两种存法。loose 是每个对象单独一个文件;packed 则是把对象打包进一个 packfile,相似的对象之间可以存成 delta。

packfile 里的 delta
packfile 里的 delta

像 parser.py 的两个版本,packfile 里完整存下 A,B 只记成「复制 A 的全部内容,再追加 parse_file()」,另有一个 .idx 文件按对象 ID 来定位它们。

fetch 的时候,客户端告诉服务端自己想要哪些 commit、已经有哪些。

服务端顺着父提交、tree 和 blob,找出从「想要的」commit 出发能找到的所有对象,也就是所谓「可达」的对象,去掉客户端已有的,把剩下的打包成一个 packfile 发过去。

全量 clone 一开始什么都没有,所以要拿走全部可达对象。

push 则反过来。客户端会参照服务端宣告的 ref,不再发送服务端已有的对象,所以推上去的 pack 是依赖服务端已有对象的,光凭这个 pack 本身,还原不出新的历史。

要把一个 Git 仓库复制到多台服务器上,对象和 ref 都得同步。副本在对外提供某个 ref 的新值之前,必须先能提供从它可达的所有对象。

系统还要协调相互竞争的 ref 更新,也就是从同一个旧 tip 出发、想把同一个分支推到不同新 commit 的那些 push。

如果给一个仓库的所有写入强行排一个总顺序,那推到无关分支的 push,也只能互相等着。

三阶段提交与预写日志

09

设计 Pando 时,OpenAI 先评估了两种扩展 Git 的常规做法。

第一种是三阶段提交(3PC)。

在他们建模的设计里,几个副本各存一份完整的仓库。任何一台服务器收到 push,都会把 packfile 并行复制给所有副本,必须有过半副本接受这次 ref 更新,push 才算发布。

负责这件事的服务器叫协调者。

每个副本先锁住自己那份 ref,检查它是否还指向客户端预期的 commit;协调者再给接受的副本发 pre-commit 消息,等过半副本确认之后,才通知它们真正执行更新。

之后,任何一个已同步的副本都能对外宣告新的分支 tip。

等过半副本点头
等过半副本点头

保留完整副本的好处,是读请求可以在本地完成,客户端连到哪个副本,看到的 ref 都一致。代价则是 ref 只能一个一个地更新,每次 push 都要来回好几轮。

每次都要等过半副本确认,副本的磁盘性能和通信延迟也就都落在了 push 的关键路径上。写入量一大,副本既要跟上不断涌进来的 pack,还要扛住读流量。

第二种,是在 blob 存储里放一份共享的 manifest,用这份预写日志(WAL)给整个仓库的更新排序。

服务器收到 push 后,先把 packfile 上传到 blob 存储,在本地准备好 ref 更新,然后往 manifest 里追加一条记录,写明这次更新并指向那个 packfile,再用 compare-and-swap 原子地替换整份 manifest。

如果中间有别的写者抢先改了 manifest,就读出新增的条目,确认自己的更新依然有效,再拿已经上传好的 packfile 重试。

大家都挤在一本登记簿前
大家都挤在一本登记簿前

写者可以并行上传 packfile,但哪怕是推到无关分支的 push,也都得去改同一份 manifest。

一次成功的 compare-and-swap 要多少往返时间,就决定了仓库发布新 manifest 的频率上限。

低延迟的 blob 存储能让这一步快一些;批量处理则能让一次更新带上好几个 ref,代价是要排队,延迟更高。但光给 blob 存储扩容,是解决不了这个瓶颈的。

而且每个副本还得先从 blob 存储读取、索引这些 pack,才能在本地对外提供。写入一旦快过这部分处理,副本就会落后,客户端要等更久才能 fetch 到刚推上来的 commit。

两处取舍

10

做 Pando 之前,OpenAI 就觉得 Git 本身不用妥协,但得在两件「大多数 Git 系统都当成必需」的事情上让步。

一是全仓库一致性。要给所有 ref 提供同一时刻的一致快照,就需要整个仓库层面的协调,推到无关分支的 push 也得去争抢同一个协调点。

二是给每个客户端单独生成 packfile。两个本地历史不同的客户端 fetch 同一个分支,服务端可能得给每人生成一个不同的 pack,才能避开对方已有的对象。

这很耗 CPU 和磁盘 I/O,读者一多就很难扩展。

Pando 的办法,是用放宽的一致性模型来扩展写入,用复用 packfile 来扩展读取。

因果一致性

11

客户端 push 一个分支时,Pando 会把传进来的 packfile 作为一个不可变的 blob 存进 blob 存储。

分支每移动一次,Pando 就在元数据存储里记录一个 ref version,里面包括新的 tip、提供它的那个 pack,以及这个 pack 依赖的更早的 ref version。

blob 存储和元数据存储,都可以横向扩展。

从 Pando fetch 一个 commit 或 ref,就是把一组 ref version,也就是一组 packfile 回放给客户端,重建出当初创建这个 ref version 的那个客户端的状态。

整个仓库层面的一致性因此变弱了,但单个 ref 的更新依然是可串行化的,只有同时 push 同一个 ref 的客户端才会冲突。

Pando 里的分支表示
Pando 里的分支表示

往 trunk 的每次 push,都会追加一个增量 pack,装着一个或几个 commit。

往旁支 topic 的每次 push,则会把这个旁支之前的 commit 再带一遍(图里虚线框中的 D 和 E),所以只要客户端有宣告过的那个 trunk 版本,一个 pack 就能把整个 topic fetch 下来。

OpenAI 在推文里配的也是这张图。

推文配图
推文配图
“

如果工程师推的是不同的分支,Pando 可以并行更新它们;如果推的是同一个分支,它仍会按顺序处理。fetch 时你会拿到每个分支 tip 的历史,但两个 tip 可能反映的是不同的时间点。

要 fetch 一个 ref,读者就得看到产出它的那个写者的历史,这便是因果一致性。

而 push 时,由 Pando 来决定客户端和服务端在哪段共享历史上达成一致,好让客户端生成更容易被复用的 packfile。

相应地,Pando 不保证 Git 操作期间所有 ref 处在同一时刻,也不保证一次 clone 或 fetch 里 ref 更新的先后顺序。

比如 foo 和 bar 两个分支,在一次全量 clone 的协商过程中都被更新了。

一个客户端可能看到 foo 在版本 1、bar 在版本 2,另一个看到的却是 foo 在版本 2、bar 在版本 1。

同一时刻,两种快照
同一时刻,两种快照

听着是不是有点乱?

但在实际使用中,用户和 agent 并不关心 ref 更新的全局顺序,他们依赖的只是单个 ref 内部的更新顺序。

说句题外话,Pando 这个名字,应该是取自美国犹他州的一片颤杨林,地面上是四万多棵树,地底下却连着同一套根系。放在一个分支无数、又共用同一份历史的仓库上,倒也挺贴切。

三种方案的模拟

12

博客里还做了一个可交互的模拟器来对比这三种方案。1000 个客户端各选一个不同的分支,反复 push 新的 commit,测整体耗时,以及从 push 到可以被 fetch 的延迟。

后一个指标很重要,它衡量的是人和 agent 互相协作、或者调用依赖版本控制的其他系统时的往返时间。

模拟里的硬件可以从普通到高端的虚拟机和 blob 存储里选,写者数量从 100 到 1000 可调,还能打开模拟 merge 和批量合并 trunk。

为了公平,三阶段提交在模拟里也放宽了全仓库一致性。

OpenAI 特意注明,这是用来讲解的合成模型,不是生产环境的基准测试。

push 到 fetch 的平均延迟这一项,只统计旁支上的 push,从第一次尝试算起,中间的重试也算在内,一直到独立的读者验证到这个 commit 为止,等读者空位的时间也包含在里面,构建和测试则不算。打开 merge 之后,写者的完成时间里还要加上必要的 fetch 和合并进 trunk 的时间。

结果是,三阶段提交在负载下磁盘饱和,写延迟明显升高;预写日志则因为服务器跟不上读日志,读延迟明显升高。

而不管是在模拟里,还是在 monorepo 上的真实测试中,Pando 都能支撑成千上万个客户端同时 push,负载下性能不受影响。

因为 Pando 的服务器要做的事其实很少。

处理 push 时,Pando 一边把 packfile 流式写进 blob 存储一边校验,不解包到本地仓库。

客户端来 fetch 时,Pando 挑出读者需要的已存 pack,让读者自己去下载。

没有哪台服务器需要把 packfile 应用到本地仓库,也不用做昂贵的解包和重新打包。它们主要只负责协商和挑选 packfile,所以用普通硬件就能跑。

Pando 的上限,便只取决于客户端、客户端到 Pando 之间的网络,以及 blob 存储服务。

容量够的话,在独立分支上工作的客户端,每个都能按自己连接的最快速度传输 pack;要服务更多这样的客户端,加 Pando 服务器,再一起扩容元数据存储和 blob 存储就行。

push 与 fetch 流程

13

Pando 的元数据存储叫 Turtle,负责分发 pack 的服务叫 Express。

push 与 fetch 的流程
push 与 fetch 的流程

一个 agent 在本地改完文件、创建 commit,运行 git push,生成一个包含这些改动的 packfile 来更新分支。

Pando 收到这个 pack,把它流式写进 blob 存储,单遍解码完成校验。

然后,它会用一个多键事务在元数据存储里记下 ref version,并原子地更新 ref 指针,保证「读得到自己刚写的内容」。整个写操作只需要 O(1) 次元数据读写。

另一个客户端来 fetch 这个分支时,Pando 根据它已有的历史挑出一组已存的 pack,只需要 O(log Δ) 次元数据读取。

这里的 Δ,指的是客户端已有版本和想要版本之间,以 ref version 计的最短距离。

服务端返回的是 packfile 的 URI,客户端再通过 Express 去下载,Express 会把这些 packfile 缓存在各个区域。

push、fetch 和 compaction,便是撑起 Pando 的三个核心服务。

只宣告 trunk 分支

14

OpenAI 在博客里打了个比方,说客户端 push 时,传统的 Git 服务器就像一个亮出全部底牌的扑克玩家,把自己所有的分支都告诉客户端。

客户端再据此收集 commit、tree 和 blob,生成一个排除了服务端已有对象的 packfile。

宣告的 ref 越多,push 就越小,但这个 pack 也越难复用,因为别的客户端可能恰好缺了写者省掉的那些对象。

Pando 则不亮全部底牌,只向客户端宣告挑选过的几个分支。

Pando 只亮一张牌
Pando 只亮一张牌

原因有两个。

一是给每个 ref 都提供一致的视图太贵了。分支数到了百万级,光是 ref 宣告就可能超过 100 MiB……

每台服务器要么维护一份实时的映射,协调开销很大;要么每次都去查一遍随 ref 数量增长的数据库。

二是只给客户端一部分 ref,它生成的 pack 就只会依赖服务端明确允许它省掉的那些对象。

把这些依赖追踪好,Pando 就能把这次 push 收到的 packfile,直接当作之后某次 fetch 的响应。

当然,宣告得越多,push 的客户端能排除的对象就越多,pack 也越小。

但要复用这些 pack,服务端就得能证明来 fetch 的客户端手里有全部依赖,否则 pack 里可能会缺对方要的对象。

那如果一个 ref 都不宣告呢?

那客户端就得把仓库的全部历史都打包进 packfile。这样的 pack 谁都能用,但代价是 fetch 任何一个分支,都等于做了一次全量 clone……

宣告的 ref 与 packfile 大小
宣告的 ref 与 packfile 大小

而大部分的体积缩减,其实只要宣告一个分支就能拿到。

所以默认情况下,客户端 push 一个新的功能分支时,Pando 只宣告一个指定的 trunk 分支作为基准,宣告的大小也就不会随分支数增长了。

代价是,工程师对同一个分支反复 push,或者把一个功能分支叠在另一个功能分支上时,trunk 以外的东西都得重新发一遍,后续的 push 会更大,可能带着冗余对象,也更费客户端的带宽。

不过在 OpenAI 的 monorepo 里,这样生成的 packfile 中位数不到 20 KiB,P99 也不到 3 MiB。

只要读者已经有宣告过的 trunk 历史,一个 packfile 就能把分支 fetch 下来,这笔账对他们来说是划算的。

选好宣告哪些 ref 之后,Pando 接受 push 并更新元数据存储,更新分成同步和异步两部分。

同步部分会写入一个新的 ref version,记下新的 tip、协商 packfile 时用到的依赖和指向这个 packfile 的链接,再把 ref 指针更新过去,保证客户端马上就能 fetch 到新 commit。

安排 compaction 之类的异步工作,也记在同一个事务里。

特别大的 push,只有一部分 commit 会同步写入,所以不管 push 有多大,对元数据存储的读写次数都是常数。

更关键的是,Pando 的服务器可以各自独立地接受推往无关 ref 的 push。唯一有争用的键是 ref 指针,用一个类似 compare-and-swap 的原子操作来保护。

想提升写吞吐,加 Pando 服务器、元数据存储的扩展单元或者 blob 存储都可以。

fetch 与区域缓存

15

客户端 fetch 时,会给服务端两组 commit ID,协议里分别叫 have 和 want。

传统的 Git 服务器要算出从 want 可达、但从 have 不可达的对象,压缩后再发给客户端。

开源 Git 服务端在本地有各种索引来加速这一步,但这样生成的 packfile 计算代价依然很高,而且基本没法缓存。

Pando 则利用 push 时记下的 commit 映射,把客户端的 have 和 want 解析成包含这些 commit 的一组 ref version。

每个 ref version 都链接着一个之前收到过的 packfile,服务端据此挑出能覆盖客户端所需对象的那几个。

想要一个旁支,也就意味着还得拿到它在 trunk 上依赖的那部分。

推文里的另一段动图,演示的正是这个过程。

fetch 时挑选 packfile
fetch 时挑选 packfile
“

Git 客户端告诉 Pando 它有哪些 commit、想要哪些,Pando 返回能补齐这段差距的已存 packfile 的链接,包括缺失的分支依赖。这些 packfile 里可能有客户端已经有的对象,但靠在大量读者之间复用,Pando 做到了类似 CDN 的性能。

可 Git 协议规定,clone 或 fetch 的 HTTP 响应里只能带一个 packfile,那好几个 pack 要怎么发呢?

好在协议里有个不太为人所知的特性叫 packfile-uri,允许服务端返回额外 pack 的下载链接,让客户端另外去下。

Pando 只用这种方式,响应里内联的,干脆就是一个空的 packfile……

为了用好这个特性,OpenAI 还给上游贡献了不少 packfile-uri 的改进。

OpenAI 的系统分布在很多地区,所以每个地区都部署了带本地缓存的 Pando 集群,Express 缓存 blob 存储,另一个服务缓存元数据存储。

按 commit ID 或者以特定方式读取 ref 的请求,可以完全由该集群的缓存来应答。

packfile 请求用一致性哈希路由到 Express 服务器,缓存命中率在 99% 以上。

这套缓存的吞吐极高,能把客户端和 Pando 之间的网络跑满。只用标准的云厂商虚拟机,clone 和 fetch 就能跑满 10 Gbps 的网络链路。

Compaction

16

对大多数 Git 服务器来说,仓库越长越大,就得靠 repack 和垃圾回收来维持性能。这两步都能回收空间,但都很耗计算和 I/O。

而在 OpenAI monorepo 这种变化极快的仓库里,给上次 fetch 以来的每一次 push 都发一个 packfile URI 是不现实的,客户端每天得下载几万个 packfile 才能追上 trunk……

所以 Pando 会在后台合并 pack,设计上类似 LSM 树里的 compaction。

每次 push 产生一个「L0」packfile,这是 Pando 能传输的最小数据单元。

随着时间推移,后台 worker 会把这些 packfile 收进一个个覆盖连续 ref version 的 cover 里,从 L0 开始按几何级数逐级变大。目前 OpenAI 的 monorepo 已经用到了 L4。

cover 有两个作用。

一个 cover 足够满之后,compaction worker 会认领它并合成一个 packfile,在里面重新组织对象,提高压缩率。

同时,cover 也相当于 ref version 上的一张跳表,同时扫描各层的索引,Pando 就能在对数时间内挑出要发给客户端的那组 pack。

不管有没有被重新打包,这些 cover 都让 fetch 的规划更高效,降低了 Pando 对吞吐和延迟的要求。

和很多 Git 服务器不同,Pando 每一层的 packfile 都会保留,不丢弃,也不做垃圾回收。

各层的 pack 都会根据客户端 fetch 时的 have 和 want,持续拿来服务。

而对把 Pando 当 API 用的客户端来说,从 L0 里读一个任意对象,往往比从 L4 里读要少读一些数据,因为 L0 压缩得没那么深。

compaction 大幅减少了发给客户端的总数据量,也让 pack 规划的开销随客户端落后的程度呈亚线性增长。

这对大型、快速变化的仓库格外重要,而 OpenAI 认为,这样的仓库会成为软件开发的新常态。

形式化验证与红队测试

17

10 周造出来的一套源码控制后端,OpenAI 是怎么验证的呢?

作为一个全新项目,Pando 用 Rust 实现,没有任何遗留依赖,也遵循了 OpenAI 自家「构建更安全的软件基础」的那套原则。

他们写了一套一致性测试,让 agent 能拿标准 Git 的行为来检验自己的改动;又开发了确定性模拟测试框架,用来检查系统的不变量和性质,找出并修复协商和 pack 规划里那些隐蔽的竞态条件和回归。

前前后后有很多模型参与了 Pando 的开发,每有新模型可用,团队就会拿它来提升 Pando 的性能和可靠性。

其中借助 GPT-6 Astra,agent 用 Lean 和 TLA+ 把一套「Pando 理论」形式化了,团队再用它来找出并修复实现里的 bug,证明系统的性质。

agent 写证明来抓 bug
agent 写证明来抓 bug

还有多个连续会话跑了几百个小时,专门发现和修复 Pando 里的边界情况;另外还有 agent 用具备 Daybreak 能力的模型,全天候对 Git 和 Pando 做红队测试。

零停机迁移

18

Pando 最初是作为现有源码控制系统前面的一层读写直通代理上线的。这样它和旧系统能保持同步,流量便可以安全地迁到 Pando 上,全程没有停机。

Pando 先站到旧系统门口
Pando 先站到旧系统门口

作为缓存设计的一部分,Pando 还能在转发之前对请求去重、合并,所以在把系统一个个迁过来的过程中,现有工具也能保持健康。

迁过来的工作负载越多,它们的 push 就把 Pando 的缓存填得越满,反过来还让旧后端更可靠了。

为了让大家用 Pando 时性能最好,OpenAI 开始把自家公开发行的 Git 装到开发者的机器上,也放进了 ChatGPT 桌面 App 里。

客户端的迁移,则是靠一个包装了 Git CLI 的内部工具分阶段完成的。

他们还做了很多别的工具,用来处理变更请求、代码评审,以及供自动化调用的 API。几百个依赖旧 API、webhook 和仓库下载的集成,也跟着 Git 流量一起迁了过来。

迁移本身,同样用上了 Codex。

团队写了用来准备现有 checkout、以及指导 agent 使用新系统的 skill,开发者只要让 Codex 去迁移自己的开发环境和服务就行了。

这里举的虽然只是单个仓库的例子,但 Pando 的设计可以支持多得多的仓库;需求增长时,数据分发和 Git 协商两部分也可以各自独立扩展。

回馈上游 Git

19

建在 Git 协议和客户端之上,也让 OpenAI 找到了 Git 自身的一些 bug,以及性能上可以改进的地方。

比如 Pando 直接把可复用的 pack 发给客户端,下载和索引这些 pack,就成了系统快不快、稳不稳的关键。

这部分工作暴露出了并发下载、传输中断后的清理,以及续传一个其实早已下载完的文件这几类边界情况。

他们修好了这些路径,在接受一个已有 pack 用于 fetch 时加了校验,并把这些都贡献回了上游。

为了看清时间和带宽到底花在了哪里,他们还给 packfile-uri 的下载加了追踪,给 pack 的生成加了字节计数,好分辨一次慢 fetch 究竟是慢在生成 pack 上,还是慢在传输上。

在 OpenAI 自己的 Git 版本里,客户端可以并行下载和索引互相独立的 pack。

他们还引入了「explicit haves」的概念,让服务端可以把一个分支当前的 tip,和 push 的客户端可以从 pack 里省掉的那段历史分开宣告。

放到 Pando 上,客户端就可以用 --force-with-lease 确认功能分支没被别人动过,同时只基于 trunk 来打包。之后任何有这段 trunk 历史的人,都能用同一个 pack 来 fetch 这个分支。

其中几项改进已经进入了上游 Git,更多的则在他们的公开仓库里,推文的最后一条说的也是这件事。

“

我们也给 Git 贡献了修复,比如 packfile 下载中断后的清理。

接下来

20

在 OpenAI 的 monorepo 里,Pando 带来了更高的并行度、更可靠的 merge 和更快的代码发布,而工程师和 agent 已经在用的 Git 工作流,一点都没变。

OpenAI 也在探索 Pando 在内部的其他用途,包括让大量 agent 在同一个共享 Git 仓库上协作。

他们认为,随着工程师和 agent 产出的软件越来越多,OpenAI 之外也会有更多团队撞上同样的源码控制极限,所以正在探索把 Pando 背后的技术分享给用户。

而 Pando 本身,也会一边继续开发,一边和开源 Git 社区合作,把对更多 Git 用户有益的改进分享出去。

作者之一 Ted Nyman 在转发时表示:

“

我们保持了 git 兼容,也会继续把通用的改进贡献给上游。这只是开始。

OpenAI 的义父 Thibault Sottiaux 则说:

“

我们还是懂点 Git 功夫的。

我怎么感觉这话,多少也有点说给三周前的 A 社听的意思?

而 Anthropic 三周前的预判是,未来 6 到 12 个月里,这类问题会变得越来越普遍。

现在看来,这个时间可能还说长了……

◇ ◆ ◇

转载声明:本文转载自原发布平台 (作者:AGI Hunt), 原文标题《继 Anthropic 之后,OpenAI 爆改 Git》, 查看原文。 版权归原作者及原发布平台所有,本站仅作收录与展示,未对正文内容作实质性修改; 若涉及侵权请联系本站处理。