OpenAI 用 Pando 承接 Codex 带来的开发规模
OpenAI 介绍了兼容 Git 的后端 Pando,旨在支撑 Codex 所推动的大规模开发活动。文章聚焦 AI 编程背景下版本控制基础设施的扩展问题。
AI 深度解读
Codex 让一个工程师同时推进多个改动,也把更多分支、push 和 CI 拉取请求压向代码仓库。OpenAI 的应对是重建 Git 服务端:Pando 保留 Git 协议和客户端,把不同分支的写入拆开协调,再用可复用的数据包承接大量读取。
作者报告,Pando 已支撑 OpenAI 整个 monorepo 的开发,push 中位延迟改善接近 5 倍,每天分发接近 1 PB 的 Git 数据。值得读的地方是:他们重新判断了哪些一致性和传输要求值得保留,以及放松要求后能换来什么。
为什么 AI 编程会把 Git 服务压到吃紧
原文描述的压力来自并发工作量,而不只是仓库变大。一个工程师同时准备多个改动,每次 push 又可能触发多个 CI 任务,反复 fetch 同一个 commit。开发环境、研究任务和 CI 都在读取,写入增加还会继续放大读取。
作者称,从其所说的「去年年底」左右开始,原有系统出现 fetch 和 push 缓慢、合并失败,可靠性有时低于 90%。原文没有交代这个可靠性指标的统计口径,不能直接理解成全年可用性。
定位问题之前,要先分清 Git 保存的两类东西:
- 对象保存内容与历史,包括 blob、tree、commit 和 tag;对象由类型和内容的哈希标识,创建后不可修改。
- 引用,也就是 refs,记录分支或标签指向哪里;一次提交会创建新对象,再移动分支引用。
未改动的文件和目录可以复用原对象。图中两个 commit 的 parser.py 不同,但共享同一棵 docs/ 目录树。
对象还可以打包进 packfile,相似内容用差量表示。fetch 时,服务端根据客户端想要什么、已经有什么,计算需要补发的对象;push 则反过来传对象并申请更新 refs。
这里有个容易漏掉的约束:push 的包可能依赖服务端已有对象,单独拿到新包未必能还原完整历史。因此,副本公布新分支位置之前,必须保证对应历史也能提供。复制文件和同步分支位置,必须一起设计。
为什么换更快的存储,也未必解决写入排队
作者比较了两种建模方案,重点都在协调成本。
三阶段提交(3PC)方案在多个副本保存完整仓库。收到 push 后,并行复制 packfile,再由协调者组织引用检查、锁定、预提交和更新,获得多数副本确认。
它能从本地副本提供读取,但写入路径要等待多轮通信和副本磁盘处理。在作者建模的设计里,全仓库引用更新的协调还会让无关分支互相等待。
另一种是预写日志(WAL)方案:先把 packfile 上传到 blob storage,再通过共享 manifest 记录引用更新与包的位置。发布 manifest 使用比较并交换(compare-and-swap);被其他写入抢先修改后,就读取新增记录、检查有效性并重试。
包可以并行上传,共享 manifest 仍然是发布入口。提高对象存储吞吐量,无法自动消除这个入口的竞争;批量发布可以摊薄成本,但会增加排队延迟。副本还得读取、索引新包,才能向客户端提供数据。
不同分支能否独立完成更新,比底层能否并行接收文件更关键。
这里也要限定结论:这是作者分析的两种具体设计,不能据此断言所有 3PC 或 WAL 系统都会以同样方式串行化。原文后面的模拟,甚至特意放松了 3PC 的全仓库一致性要求。
Pando 放松了什么,又保住了什么
Pando 的两个选择分别服务于写入和读取:按分支协调更新,跨客户端复用 packfile。
收到 push 后,它把包作为不可变 blob 保存,并在元数据存储中记录一个 ref version:新分支位置、提供它的包,以及这个包依赖的更早引用版本。对象存储和元数据存储都可以水平扩展。
读取某个引用时,服务端向客户端提供相关版本对应的包,重建产生该版本所需的历史。原文称之为因果一致性(causal consistency,CC):读到一次分支更新,也能取得支撑这次更新的依赖历史。
单个 ref 的更新仍然可串行化,客户端同时推送到同一 ref 才会发生写入冲突。但多个分支不再保证属于全仓库的同一时刻。假设 clone 协商期间 foo 和 bar 都发生更新,一个客户端可能看到 foo 的版本 1 和 bar 的版本 2,另一个看到相反组合。
这不是把每条分支的历史读乱了,而是不承诺所有分支组成一张同时拍摄的快照。
读取侧则接受一定重复传输。传统服务端可能为不同客户端计算不同的最小对象集合;Pando 复用包,让服务端反复发送相同字节,减少打包计算,也便于缓存。
原文举例:trunk 的 push 追加增量包,topic 分支的新包则再次包含该主题此前的提交。只要客户端拥有约定的 trunk 版本,就能通过一个包获取 topic。
图中的包依赖也不要和 Git 父提交混淆:新 topic 包依赖 trunk 的 G,但提交 F 的 Git 父提交仍然是 E。
Pando 的读取目标是让客户端下载速度充分利用可用网络带宽。原文没有给出带宽利用率实测,也没量化重复对象增加了多少流量,所以不能把这个目标写成已经证明的结果。
我的判断
我认同它先审视需求、再选择一致性范围的思路。很多开发操作关心的是「这个 commit 能否取到」「这个分支是否正确前进」,未必需要一次读取就获得所有分支的统一快照。对这样的负载,全仓库协调可能付出了收益不高的成本。
但这有前提。如果工具依靠一次读取多个 refs 来判断发布状态,就需要重新检查它对快照的假设。大量写入若集中到同一个主干,按分支拆分也不会让这个热点消失。
复用 packfile 同样是一笔交换:减少服务端计算,可能增加网络传输。原文没有给出 CPU、出口带宽、存储成本的完整对照,普通团队无法只凭吞吐量判断是否划算。
还要把生产结果和演示分开。接近 5 倍的 push 中位延迟改善、每天接近 1 PB 的分发量,是作者报告的生产数据;1,000 个客户端分别向不同分支推送、界面累计 10,000 次 push 的比较,则明确是教学模拟,不是生产基准测试。
模拟中的平均 push 到 fetch 延迟,包含首次尝试、重试,以及独立读取者验证 commit 前的等待,也包含读取槽位排队,但不含构建和测试。提供的原文没有展示模拟最终结果,也没有生产环境的尾延迟与成本明细,证据尚不足以判断其他团队能获得多大收益。
能拿来做什么
- 排查 AI 编程或 CI 导致的仓库拥堵时,把 push 确认耗时、独立任务能够 fetch 到 commit 的耗时,以及构建测试耗时分别记录。push 返回成功,还不能代表下游协作已经畅通。
- 前端或 Node monorepo 若因多个任务反复拉取同一 commit 而变慢,可以先检查现有服务的缓存、镜像和对象复用能力,再判断是否需要更换架构。
- 编写读取多个分支的自动化脚本时,明确任务依赖哪些 commit ID。对确实需要统一发布状态的流程,显式记录这组版本,避免默认多次读取天然属于同一时刻。
- 评估自建 Git 服务前,先统计写入分布。如果大部分更新都集中在同一个 ref,Pando 展示的跨分支并行优势就未必对应自己的主要瓶颈。