OpenAI

2026年11月2日

エンジニアリング

OpenAI における Git のスケーリング

Git エコシステムへの投資と、エージェントによる大規模開発に対応するソース管理の構築

著者:技術スタッフ Aaron Friel、Taylor Blau、Ted Nyman

この 1 年で、当社のモノレポの規模と、それに求められる処理量は驚異的なペースで増加しました。Codex が可能にする規模の並行作業に対応するには、ソース管理システムの刷新が必要であることは明らかでした。

そこで、開発業務を一切中断せず、チームの Git の使い方も変えることなく、Git リポジトリ向けの新しいバックエンド、Pando を構築しました。現在、Pando は OpenAI のモノレポにおけるすべての開発を支えています。Pando は 2 つの意欲的な目標を達成するために設計されています。1 つ目は、多数のブランチへの同時プッシュに最適化された、水平スケーリング可能な書き込みです。2 つ目は、読み取りスループットを最大化し、クライアントが利用可能なネットワーク帯域を使い切る速度でデータをダウンロードできるようにすることです。

Git プロトコルおよびクライアントとの互換性を維持しながら、これらの目標を達成しました。そのため、エンジニアもエージェントも、既存のワークフローに Pando をシームレスに導入できました。本番環境では、Pando によってプッシュのレイテンシーの中央値がほぼ 5 分の 1 になりました。現在は、1 日あたり 1 ペタバイト近い Git データを配信しています。

本記事では、新しいソース管理システムを構築した背景と、Git との互換性を維持することにした理由を紹介します。一般的な Git のスケーリング手法と当社のシステムを比較し、Pando が OpenAI のスケーラビリティ要件を満たせる独自の理由を説明します。また、今後の展望と、エージェントによる大規模な作業を支えるために Git への貢献をどう続けていくかについても紹介します。

新しいソース管理システムが必要だった理由

Codex は OpenAI の開発の規模とスピードを変え、エンジニアがはるかに多くの作業を並行して進められるようにしました。その結果、ブランチ、プッシュ、マージが増え、開発環境、研究ジョブ、CI からのフェッチやクローンも増加しています。

これらの要求は重なり合って負荷を増大させます。今では 1 人のエンジニアが複数の変更を同時に CI にかけられる状態にし、1 回のプッシュで、複数のジョブが同じコミットをフェッチし始めることがあります。

昨年末ごろから、モノレポを支えるシステムは処理に追い付くのが難しくなっていました。フェッチやプッシュの遅延、マージの失敗が発生し、信頼性が 90% を下回ることもありました。私たちが直面したのは Git 自体の限界だったのでしょうか。それとも、モノレポの提供方式の限界だったのでしょうか。Git は、カーネルのパッチをメールでやり取りするコミュニティから生まれました。20 年以上にわたり、開発者はソフトウェアの作り方の変化に合わせ、Git を中心にツールを適応させてきました。自社のモノレポで不便を感じ始めたとき、そうした積み重ねを引き続き活かせることが、私たちにとって重要でした。

使い慣れたワークフローを維持しつつ、バックエンドを再構築して、異なるブランチへのプッシュを並列処理し、多数の読み取りクライアントに同じ履歴を同時に提供する道筋が見えました。ソース管理ワークフローの基盤を Git に置けば、クライアントの改善を OpenAI 以外でも役立てられます。私たちは、自社の Git コピー⁠(新しいウィンドウで開く)で行った改善をアップストリームに還元し、より広いコミュニティで共有したいと考えていました。

大規模な Git 運用を支える設計

ソース管理システムのスケーリングには、2 つの重要な側面があります。多数の書き込みクライアントが異なるブランチにプッシュすることと、多数の読み取りクライアントが同じ履歴をフェッチすることです。Pando はその両方に最適化されています。

まず、Pando は各ブランチの更新を独立して調整します。無関係なブランチの更新は並行して進められ、ブランチをフェッチする読み取り側は、そのリビジョンに必要な履歴を取得します。その代わり、複数のブランチを参照する読み取り側には、それぞれ異なる時点の状態が見える場合があります。

次に、Pando はクライアントごとに個別のパックファイルを生成するのではなく、パックファイル(複数のオブジェクトをまとめて圧縮する Git の形式)をフェッチ間で再利用します。クライアントは既に持っているオブジェクトの一部をダウンロードする可能性がありますが、サーバーはわずかな計算処理で同じデータを多数のクライアントに提供できます。

こうした選択により、ブランチをまたいで書き込みをスケールさせ、キャッシュから読み取りに応答できます。これらがどう組み合わさるのかを理解するために、まず Git が履歴を保存し、転送する仕組みを見ていきましょう。

Git オブジェクトストレージの基礎

Git は、リポジトリを構成するオブジェクトと参照の 2 つを同期させる必要があります。

Git リポジトリは、ファイルのスナップショットと、それらをつなぐ履歴をオブジェクトのグラフとして記録します。オブジェクトには、blob、ツリー、コミット、タグのいずれかの種類があります。blob は個々のファイルの内容を保持し、ツリーは名前を blob やほかのツリーに対応付けてディレクトリ構造を形成します。コミットはルートツリーと 0 個以上の親コミットを指し、リポジトリの履歴を形成します。

オブジェクトは、その種類と内容のハッシュによって識別されます。また、オブジェクトは不変です。変更されたファイルを記録すると新しい blob が作られますが、変更されていないファイルやディレクトリには既存のオブジェクトを再利用できます。

ブランチとタグは、コミットを指す参照であり、「refs」とも呼ばれます。ブランチ上でコミットすると、新しいコミットが作成され、ブランチの参照がそのコミットを指すよう更新されます。

ブランチ refs/heads/main はコミット C2 を指し、その親は C1 です。C1 はルートツリー T1 を指し、その中の parser.py は blob B1 を参照しています。C2 はルートツリー T2 を指し、その中の parser.py は blob B2 を参照しています。どちらのルートツリーでも、docs/ は同じツリー T3 を参照しています。T3 の README.md エントリは blob B3 を、usage.md は blob B4 を指しています。

Git オブジェクトは ID で互いを参照します。コミットはツリーを、ツリーはほかのツリーや blob を参照します。ブランチ refs/heads/main はコミット C2 を指し、その親は C1 です。C1 はルートツリー T1 を指し、その中の parser.py は blob B1 を参照しています。C2 はルートツリー T2 を指し、その中の parser.py は blob B2 を参照しています。どちらのルートツリーでも、docs/ は同じツリー T3 を参照しています。T3 の README.md エントリは blob B3 を、usage.md は blob B4 を指しています。

Git はオブジェクトを「個別」または「パック済み」の状態で保存します。個別のオブジェクトは、オブジェクト ID で識別される別々のファイルに保存されます。パック済みのオブジェクトはパックファイルにまとめて保存され、似たオブジェクトを互いの差分として保存できます。

$GIT_DIR/objects の下に parser.py の 2 つのバージョンが示されています。個別のオブジェクトでは、blob A に parse() が、blob B に parse() と parse_file() の両方が含まれています。パックファイルは A を完全な形で保存し、B を「A 全体をコピーして parse_file() を追加する」という命令で表現します。付随する *.idx ファイルは、オブジェクト ID によって、パックファイル内の両方の blob の位置を特定します。

Git は blob を個別のファイルとして保存することも、まとめてパックし、ある blob を別の blob との差分として符号化することもできます。

Git では、あるコミットから親、ツリー、blob へのリンクをたどってオブジェクトを見つけられる場合、そのオブジェクトはそのコミットから到達可能であるといいます。フェッチ時、クライアントは必要なコミットと既に持っているコミットをサーバーに伝えます。サーバーは、要求されたコミットから到達可能なオブジェクトを見つけ、クライアントが既に持っていると分かっているものを除外し、残りを Git がディスク上で使うものと同じ形式のパックファイルとして送信します。完全クローンはオブジェクトを何も持たない状態から始まるため、クローン対象の履歴から到達可能なすべてのオブジェクトが必要です。

プッシュは、サーバーに 1 つ以上の参照の更新を要求し、オブジェクトを逆方向に転送します。クライアントは、サーバーが通知した参照の集合を使って、サーバーが既に持っているオブジェクトを送らないようにします。そのため、送信されるパックは既存のオブジェクトに依存する場合があり、パックを受信するだけでは新しい履歴を再構築できません。

Git リポジトリの複製とは、複数のサーバーでオブジェクトと参照の両方を利用可能に保つことです。レプリカが参照の新しい値を提供するには、まずその値から到達可能なオブジェクトを提供できる必要があります。サービスは、競合する参照更新も調整する必要があります。これは、同じブランチを同じ古い先端から、異なる新しいコミットへ移そうとするプッシュです。リポジトリへのすべての書き込みに単一の順序を課すシステムでは、無関係なブランチへのプッシュ同士が待ち合う場合があります。

既存の分散 Git サービスの構成パターン

Pando の設計にあたり、Git を使ったソース管理をスケールさせる従来のアプローチを評価しました。1 つは 3 相コミット(3PC:複数ノード間でコミットを調整するプロトコル)でリポジトリのレプリカ間の参照更新を調整する方式、もう 1 つは blob ストレージ内の共有の先行書き込みログ(WAL:変更を適用前に書き込む記録)で更新を順序付ける方式です。

3 相コミット(3PC)

モデル化した 3 相コミット方式では、Git サービスが複数のレプリカにリポジトリの完全なコピーを保持します。いずれかのサーバーがプッシュを受信すると、受信したパックファイルをコピーし、すべてのレプリカに並列で送信します。プッシュを公開するには、過半数のレプリカが参照更新を受け入れる必要があります。これを管理するサーバーがコーディネーターであり、コーディネーターとほかの各レプリカとの間で複数回の通信が必要です。

参照更新を受け入れる際、各レプリカは自分が持つ参照のコピーをロックし、クライアントが想定するコミットをまだ指していることを確認します。続いて、コーディネーターは受け入れたレプリカに事前コミットメッセージを送ります。過半数が事前コミットメッセージに応答すると、コーディネーターはそれらのレプリカに更新を実行するよう指示します。その後は、最新の状態にあるどのレプリカも、リポジトリのローカルコピーから新しいブランチの先端を通知できます。

完全なレプリカを保持することで、読み取りにローカルで応答でき、クライアントがどのレプリカを選んでも、すべての参照について整合した状態を取得できることが保証されます。その結果、参照は 1 つずつ更新され、各プッシュでは、レプリカ間で参照更新を調整するために複数回の往復通信が必要になります。

クォーラムを待つ必要があるため、レプリカのディスク性能や通信レイテンシーがプッシュの処理時間に影響します。書き込み量が増えると、レプリカは読み取りトラフィックに加え、流入するパックの処理にも追い付く必要があります。

先行書き込みログ(WAL)

モデル化した WAL 方式では、Git サービスは blob ストレージ内の共有マニフェストを使い、リポジトリ全体の更新を順序付けます。サーバーはプッシュを受信すると、受信したパックファイルを blob ストアにアップロードし、リポジトリのローカルコピーで参照更新を準備します。プッシュを公開するため、blob ストア内の共有マニフェストに、参照更新を記録し、パックファイルを指すエントリを追加します。次に Git サーバーは、比較交換操作でマニフェストを更新します。この操作はマニフェストをアトミックに置き換えるか、条件が合わなければ変更を拒否します。その間に別の書き込み側が更新していた場合、サーバーは途中で追加されたエントリを読み、自分の参照更新がまだ有効かを確認して、アップロード済みのパックファイルを使って再試行します。

書き込み側はパックファイルを並列にアップロードできますが、無関係なブランチへのプッシュであっても、同じマニフェストを更新する必要があります。比較交換が成功するまでの往復時間によって、リポジトリが新しいマニフェストを公開できる頻度が制限されます。低レイテンシーの blob ストアなら、この処理を高速化できます。バッチ処理では、1 つのマニフェストで複数の参照を同時に更新できますが、更新がキューで待機するため、レイテンシーが増加します。

blob ストアはパックファイルを並列に受信できますが、共有マニフェストの更新は依然として競合します。blob ストアの容量を増やすだけでは、このボトルネックは解消しません。各レプリカは、パックをローカルで提供する前に、blob ストアから読み込んでインデックスを作成する必要があります。流入する書き込みがこの処理を上回ると、レプリカの更新が遅れ、新たにプッシュされたコミットをフェッチするクライアントの待ち時間が長くなります。

Pando のアーキテクチャ設計

そこで登場するのが Pando です。Pando は、多数のクライアントによる同時プッシュに対応するスケーラビリティを目指した、Git リポジトリを提供するための新しいアーキテクチャです。その仕組みを紹介します。

制約の見直し

Pando の開発前から、Git を妥協することなく要件を満たせるのではないかと考えていました。ただし、ほとんどの Git システムが必須と見なす以下の点については、妥協が必要でした。

  • リポジトリ全体の整合性: すべての参照について、ある一時点の整合したスナップショットを提供するには、リポジトリ全体での調整が必要です。すべての参照更新に一律の順序を付けるシステムでは、無関係なブランチへのプッシュも、単一の調整ポイントで競合します。

  • クライアントごとに異なるパックファイル: ローカルの履歴が異なる 2 つのクライアントが同じブランチをフェッチする場合、既に持っているオブジェクトを送らないように、サーバーはそれぞれに異なるパックを生成する必要があります。こうしたレスポンスの生成には CPU とディスク I/O のコストがかかるため、大量の読み取りクライアントに対応するのは困難です。

そこで Pando は、整合性の制約を緩めたモデル(因果整合性)で書き込みをスケールさせ、パックファイルの再利用で読み取りをスケールさせます。

因果整合性(CC)

クライアントがブランチにプッシュすると、Pando は受信したパックファイルを不変の blob として blob ストアに保存します。ブランチが更新されるたびに、Pando は新しい先端、その先端を提供したパック、そのパックが依存する過去の参照バージョンを含む参照バージョンをメタデータストアに記録します。blob ストアとメタデータストアは、どちらも水平スケーリングが可能です。

Pando からコミットや参照をフェッチする際は、一連の参照バージョン(パックファイル)をクライアントに再送し、フェッチ対象の参照バージョンを作成したクライアントの状態を再構築する必要があります。これにより、リポジトリ全体では弱い整合性を提供しつつ、個々の参照の更新については直列化可能性を提供します。クライアント同士が競合するのは、同じ参照に同時にプッシュする場合だけです。複数のブランチを読み取るクライアントは、それらの読み取り結果がリポジトリ全体の同一時点の状態を表すとは想定できません。

左側では、トランクの連続する 3 つのパックに、それぞれコミット A・B・C、D・E、F が含まれています。右側では、トランクとトピックが C で分岐しています。トランクは G に進み、トピックは D、E、F と進みます。最新のトピックパックには D、E、F が含まれています。破線の枠は、D と E が以前のトピックパックにも含まれていることを示しています。青い矢印は、新しいパックが G 時点のトランクに依存していることを示しています。Git 上での F の親は E です。

トランクへのプッシュでは、1 つ以上のコミットを含む差分パックが追加されます。派生ブランチへの各プッシュには、そのトピックの過去のコミットも再び含まれます。そのため、通知されたバージョンのトランクを持つクライアントは、1 つのパックでトピックをフェッチできます。

これは因果整合性の一形態です。参照をフェッチする際、読み取り側はその参照を生成した書き込み側の履歴を観測します。プッシュ時には、クライアントとサーバーが共通と見なす履歴を Pando が制御し、クライアントが共有しやすいパックファイルを生成するようにします。

そのため Pando は、Git 操作中にすべての参照の一時点の状態を提供することも、クローンやフェッチ内で参照更新の順序を保証することもしません。具体例として、完全クローンのネゴシエーション中に、ブランチ foo と bar が両方とも更新されるとします。ネゴシエーション中に、あるクライアントは foo のバージョン 1 と bar のバージョン 2 を観測し、別のクライアントは foo のバージョン 2 と bar のバージョン 1 を観測する可能性があります。実際には、ユーザーやエージェントは参照更新の全体的な順序には依存せず、単一の参照の更新順序に依存します。

アプローチの比較

この 3 つの整合性モデルのシミュレーションでは、1,000 のクライアントがそれぞれ別のブランチを選び、固有のコミットを繰り返しプッシュするよう設定します。そして、全体の実経過時間と、プッシュからフェッチまでのレイテンシーを測定します。後者は、ユーザーやエージェントが共同作業したり、バージョン管理に依存するほかのシステムを利用したりする際の往復時間を測るため、ワークロードにとって重要な指標です。このシミュレーションでは、汎用から高性能までの仮想マシンと blob ストレージシステムをモデル化しています。このシミュレーションでは、3 相コミットのリポジトリ全体の整合性制約を緩和しています。

Git サーバー実装のシミュレーション

各ライターによる繰り返しのプッシュを追跡し、それとは別に、独立したリーダーが各コミットをチェックアウトできるまでの時間を測定します。

3 フェーズコミット

準備完了
シミュレーション時間
0 s
プッシュからフェッチまでの平均時間
—
完了したライター
0 / 1,000

各四角は 1 つのライターを表します。選択したすべてのアーキテクチャで、同じワークロードを実行します。プッシュの確認応答が返るにつれて四角の色が明るくなり、ライターの処理が完了すると青色に変わります。グラフはシミュレーション時間に沿って完了したライター数を示します。独立したリーダーは、その後もフェッチを続けられます。 矢印キーでライターを選択し、Home または End キーで先頭または末尾に移動し、Escape キーで選択を解除できます。

0 / 10,000 確認応答済みのプッシュ · 0 フェッチ済み

四角を選択すると、対応するライターの詳細を確認できます。

オブジェクトストア WAL

準備完了
シミュレーション時間
0 s
プッシュからフェッチまでの平均時間
—
完了したライター
0 / 1,000

各四角は 1 つのライターを表します。選択したすべてのアーキテクチャで、同じワークロードを実行します。プッシュの確認応答が返るにつれて四角の色が明るくなり、ライターの処理が完了すると青色に変わります。グラフはシミュレーション時間に沿って完了したライター数を示します。独立したリーダーは、その後もフェッチを続けられます。 矢印キーでライターを選択し、Home または End キーで先頭または末尾に移動し、Escape キーで選択を解除できます。

0 / 10,000 確認応答済みのプッシュ · 0 フェッチ済み

四角を選択すると、対応するライターの詳細を確認できます。

Pando

準備完了
シミュレーション時間
0 s
プッシュからフェッチまでの平均時間
—
完了したライター
0 / 1,000

各四角は 1 つのライターを表します。選択したすべてのアーキテクチャで、同じワークロードを実行します。プッシュの確認応答が返るにつれて四角の色が明るくなり、ライターの処理が完了すると青色に変わります。グラフはシミュレーション時間に沿って完了したライター数を示します。独立したリーダーは、その後もフェッチを続けられます。 矢印キーでライターを選択し、Home または End キーで先頭または末尾に移動し、Escape キーで選択を解除できます。

0 / 10,000 確認応答済みのプッシュ · 0 フェッチ済み

四角を選択すると、対応するライターの詳細を確認できます。

フェッチの検証完了までのライターの進捗

  • 3 フェーズコミット
  • オブジェクトストア WAL
  • Pando
05001,00000 s
  • 待機中
  • プッシュ中
  • 全プッシュの確認応答完了
0 s後

説明用の模擬モデルであり、本番環境のベンチマークではありません。プッシュからフェッチまでの平均時間は、トピックブランチへのプッシュを対象に、最初の試行から再試行を経て、独立したリーダーがそのコミットを検証するまでを測定したものです。リーダーの実行枠が空くまでの待機時間を含み、ビルドとテストは含みません。マージが有効な場合、ライターの完了時間には必要なフェッチとトランクへのマージも含まれます。

実験設定

シミュレーションを準備中…

結果

比較を再生して、記録された結果を確認できます。

説明用の模擬モデルであり、本番環境のベンチマークではありません。プッシュからフェッチまでの平均時間は、トピックブランチへのプッシュを対象に、最初の試行から再試行を経て、独立したリーダーがそのコミットを検証するまでを測定したものです。リーダーの実行枠が空くまでの待機時間を含み、ビルドとテストは含みません。マージが有効な場合、ライターの完了時間には必要なフェッチとトランクへのマージも含まれます。

Pando とほかの 2 つのアプローチを比較するシミュレーションを実行してみてください。汎用から高性能までのハードウェア構成やクライアント数などを設定し、結果を比較できます。

モデル化した 3 相コミットシステムでは、負荷がかかるとディスクが飽和し、書き込みレイテンシーが大幅に増加します。一方、先行書き込みログ方式でも同じ理由でサーバーによるジャーナルの読み取りが追い付かず、読み取りレイテンシーが大幅に増加します。当社のモノレポでの実環境テストでも、このシミュレーションでも、Pando は数千ものクライアントによる同時プッシュに対応し、負荷がかかってもパフォーマンスを維持します。

プッシュの処理時、Pando は受信したパックファイルをローカルリポジトリに展開せず、blob ストレージにストリーミングしながら検証します。クライアントがフェッチを開始すると、Pando が保存済みのパックから読み取り側に必要なものを選び、読み取り側がそれらをダウンロードします。どのサーバーも、パックファイルをローカルリポジトリに適用したり、コストのかかる展開やパック処理を行ったりする必要はありません。Pando サーバーの主な処理はネゴシエーションとパックファイルの選択なので、汎用ハードウェアで稼働できます。

そのため Pando は、クライアント、クライアントと Pando 間のネットワーク、blob ストレージサービスの限界までスケールできます。十分な容量があれば、独立したブランチで作業するクライアントは、それぞれの接続で可能な最大速度でパックを転送できます。その速度でさらに多くのクライアントに対応するには、Pando サーバーを追加し、それに合わせてメタデータストアと blob ストアを拡張できます。

Pando でのブランチのプッシュとフェッチ

Pando はライターのパックファイルをブロブストレージに保存し、ref のバージョンをメタデータサービスに記録します。フェッチ時には、Pando がリーダーに必要なパックを選択し、Express が配信します。

プッシュのストリーミング

  • パックファイルのバイトデータ
  • メタデータ
  • リクエスト / 確認応答
  • パックファイルの URI

エージェントがローカルでファイルを編集してコミットを作成し、git push を実行すると、ブランチを更新するために、その変更を含むパックファイルが作成されます。Pando はパックを受信し、blob ストレージにストリーミングします。受信したプッシュを 1 回のデコードで検証した後、Pando は複数キーにまたがるトランザクションを使い、メタデータストアに参照バージョンを記録するとともに、参照ポインターをアトミックに更新します。これにより Pando は、「自分が書き込んだ内容を読み取れる」という整合性を提供できます。この書き込み操作全体に必要なメタデータの読み書きは O(1)O(1) 回です。

別のクライアントがそのブランチをフェッチすると、Pando はそのクライアントが既に持っている履歴を使い、保存済みパックから必要なものを選びます。この操作には O(log⁡Δ)O(\log \Delta) 回のメタデータ読み取りが必要です。ここで Δ\Delta は、クライアントが持つ状態と求める状態の間の、参照バージョン数で測った最短距離です。サーバーはパックファイルの URI を返し、クライアントは、パックファイルをリージョンごとのキャッシュに保存する Express というサービスを通じてダウンロードします。

この設計は、プッシュ、フェッチ、コンパクションという、Pando のアーキテクチャの中核となる 3 つのサービスに支えられています。

プッシュのスケーリング

クライアントがプッシュするとき、従来の Git サーバーは、手札をすべて見せるポーカープレイヤーのように、自分が持つすべてのブランチをクライアントに開示します。クライアントは次にコミット、ツリー、blob を集め、サーバーが既に持っていると分かっているオブジェクトを除外したパックファイルを作成します。

より多くの参照を通知すると、プッシュのサイズを小さくできます。一方、書き込み側が除外したオブジェクトを別のクライアントが持っていない場合があるため、パックを再利用しにくくなります。

Pando は別のアプローチを取ります。手札をすべて見せるのではなく、選んだ一部のブランチだけをクライアントに通知します。これには 2 つの理由があります。

  1. すべての参照について整合した状態を提供するには、大きなコストがかかります。ブランチが数百万あると、参照の通知だけで 100 MiB を超えることがあります。これを提供するには、各サーバーが多大な調整コストをかけて最新の対応関係を維持するか、参照数に比例する規模のデータベースクエリを実行する必要があります。

  2. 参照の一部だけをクライアントに通知すれば、クライアントが生成するパックの依存先は、サーバーが明示的に除外を許可したものだけになります。この依存関係を追跡することで、Pando は、あるプッシュでクライアントが生成したパックファイルを、その後のフェッチへのレスポンスとして再利用できます。

Git サーバーが通知する参照が増えるほど、プッシュするクライアントは除外対象のオブジェクトを見つける手掛かりが増え、より小さいパックを生成できます。ただし、こうしたパックファイルを再利用するには、フェッチするクライアントが依存先をすべて持っていることを、サーバーが確認できる必要があります。そうでなければ、クライアントに必要なオブジェクトがパックファイルから抜け落ちている可能性があります。

一方、Git サーバーが参照をまったく通知しない場合、クライアントはリポジトリの全履歴を含むパックファイルを生成します。そのパックファイルはほかのどのクライアントでも利用できますが、どのブランチのフェッチも実質的に完全クローンになるという、大きなコストが生じます。

通知する参照がパックファイルのサイズに与える影響を示す曲線です。参照を通知しない場合は全履歴が含まれ、トランクを通知するとサイズが縮小し、すべての参照を通知すると新しいオブジェクトのみになります。

通知する参照が多いほど、パックファイルは小さくなります。参照を 1 つ通知するだけでも、サイズ削減効果の大半を得られます。

デフォルトでは、クライアントが新しい機能ブランチをプッシュする際、Pando は指定されたトランクブランチだけをベースとして通知します。これにより、リポジトリのブランチが増えても通知サイズは一定に保たれますが、トレードオフもあります。エンジニアが同じブランチに何度もプッシュしたり、ある機能ブランチの上に別の機能ブランチを積み重ねたりする場合、クライアントはトランクに含まれないものをすべて再送する必要があります。

その結果、ブランチへのプッシュを重ねるにつれてサイズが大きくなり、重複の可能性があるオブジェクトを含むため、クライアントの帯域も余分に消費します。当社のモノレポでは、この方法で生成されたパックファイルのサイズは中央値で 20 KiB 未満、99 パーセンタイルで 3 MiB 未満でした。通知されたトランクの履歴を既に持つ読み取り側は、その 1 つのパックファイルでブランチをフェッチできるため、私たちにとっては合理的なトレードオフでした。

クライアントに通知する参照を選んだ後、Pando はプッシュを受け付け、メタデータストアを更新します。Pando はメタデータの更新を、同期処理と非同期処理に分けています。同期的なメタデータ更新により、クライアントは新たに更新されたコミットをすぐにフェッチできます。この処理では、新しい先端、パックファイルのネゴシエーションに使われた依存関係、プッシュされたパックファイルへのリンクを格納した新しい参照バージョンを書き込み、参照ポインターがそのバージョンを指すように更新します。

同時に、コンパクションのスケジュール設定などの非同期処理も、同じトランザクション内に記録します。非常に大きなプッシュでは、コミットの一部だけを同期的に書き込みます。これにより、プッシュのサイズにかかわらず、メタデータストアへの読み書き回数を一定にできます。

重要なのは、Pando サーバーが無関係な参照へのプッシュを独立して受け付けられることです。競合するキーは参照ポインターだけであり、比較交換に類似したアトミックな操作で保護しています。Pando サーバー、メタデータストアのスケーリングユニット、または blob ストアを追加することで、書き込みスループットを拡張できます。

回線速度を使い切るフェッチとクローンの性能

クライアントはフェッチの際、プロトコル上で have と want と呼ばれる、2 組のコミット ID をサーバーに渡します。従来の Git サーバーは、have から到達可能なオブジェクトと want から到達可能なオブジェクトの差集合を計算し、圧縮してクライアントに送信します。オープンソースの Git サーバーには、この処理を高速化する各種のローカルインデックスがありますが、こうしたパックファイルの生成には大きな計算コストがかかる場合があり、一般にキャッシュもしにくいものです。

これに対し Pando は、プッシュ時に記録したコミットの対応関係を使ってクライアントの have と want を検索し、それらのコミットを含む参照バージョンの集合に解決します。各参照バージョンは、Pando が以前に受信したパックファイルにリンクしています。サーバーはこれを使ってクライアントに必要なものを割り出し、必要なオブジェクトの集合を網羅するパックファイルを選びます。

Pando でのフェッチ計画

Pando はクライアントが保持するデータと要求するデータを比較し、不足しているブランチの依存データも含め、必要なパックファイルを保存済みのものから選択します。

取得済みフェッチ不足しているブランチの依存データ

トランクパック 27 個をフェッチ。

ブランチを選択すると、そのブランチが依存するトランクのデータも含まれます。

Pando は、Git プロトコルの have と want を一連の参照バージョンに対応付けます。派生ブランチを要求することは、そのブランチが依存するトランク上の履歴まで要求することを意味します。

Git プロトコルでは、クローンやフェッチの HTTP レスポンスで送信できるパックファイルは 1 つだけです。しかし、Git プロトコルのあまり注目されていない機能である packfile-uri を使うと、サーバーは追加のパックを別途ダウンロードするためのリンクを返せます。Pando はこの方式だけを使い、インラインでは空のパックファイルを送信します。この機能を支えるため、OpenAI は packfile-uri に関する多くの改善をアップストリームに提供しました。

OpenAI のシステムは多くの地理的リージョンで稼働しているため、各リージョンでローカルキャッシュを備えた Pando クラスターを運用しています。パック配信サービスの Express が blob ストアをキャッシュし、別のサービスがメタデータストアをキャッシュします。これらのキャッシュにより、Pando はコミット ID へのリクエストや参照の特殊な読み取りを、そのクラスターのキャッシュだけで処理できます。コンシステントハッシュ法を使ってパックファイルへのリクエストを Express サーバーに振り分けており、キャッシュヒット率は 99% を超えています。

このキャッシュシステムは極めて高いスループットを提供し、クライアントと Pando 間のネットワーク帯域を使い切ることができます。パックファイルの URI とローカルキャッシュを使うことで、クラウドプロバイダーの標準的な仮想マシンだけでも、クローンとフェッチが 10 Gbps のネットワーク回線を使い切れることを確認しました。

コンパクションと最初のバイトを受信するまでの時間

ほとんどの Git サーバーでは、拡大するリポジトリの性能を維持するために、再パックとガベージコレクションが必要です。どちらも空き容量を回復する処理ですが、多くの計算資源と I/O を必要とします。

OpenAI のモノレポのように更新の速いリポジトリでは、前回のフェッチ以降に発生したすべてのプッシュについて、個別のパックファイル URI を送るのは現実的ではありません。クライアントがトランクを最新に保つには、毎日数万個のパックファイルをダウンロードする必要が生じます。

そこで Pando は、ログ構造マージツリーのコンパクションに似た設計を採用し、バックグラウンドでパックを結合して、このオーバーヘッドを減らします。各プッシュは、Pando が「L0」と呼ぶパックファイルを生成します。これは Pando が転送できるデータの最小単位です。バックグラウンドワーカーは、これらのパックファイルを順次、連続する参照バージョンをまとめたカバーに集約します。カバーの規模は L0 から等比級数的に大きくなります。現在、当社のモノレポでは L4 までのパックファイルカバーを使っています。

これらのカバーには 2 つの役割があります。カバーが十分に埋まると、コンパクションワーカーがその処理を引き受け、単一のパックファイルを生成します。これにより、パックファイル内のオブジェクトが再編成され、圧縮率が向上します。また、参照バージョン上のスキップリストとしても機能し、フェッチのネゴシエーション性能を向上させます。各レベルのインデックスを同時に走査することで、Pando はクライアントに送るパックの集合を対数時間で選択できます。これらのパックファイルカバーは、再パック済みかどうかにかかわらず、フェッチ計画を効率化し、Pando に求められるスループットとレイテンシーの要件をともに緩和します。

Pando のコンパクション

小さなパックファイルをレベルごとに大きなカバーへとまとめることで、圧縮効率を高め、フェッチを高速化します。

コンパクション実行中公開済みカバー

コンパクションは、多数の参照バージョンのパックファイルをバックグラウンドで結合し、より少ないファイルで効率よくフェッチできるようにします。

多くの Git サーバーとは異なり、パックファイルはすべてのレベルで保持され、破棄やガベージコレクションは行いません。各レベルのパックは、クライアントがフェッチ時に指定する have と want に応じて、継続的に配信に使われます。また、Pando を API として使うクライアントがパックファイルから任意のオブジェクトを読み取る場合、L0 は L4 より圧縮されている可能性が低いため、読み取るデータ量が少なくなる傾向があります。

Pando のコンパクションは、クライアントへの総転送量を大幅に削減するとともに、クライアントの履歴の遅れに対するパック計画の処理量の増加を、線形未満に抑えます。これは、大規模で更新頻度の高いリポジトリにとって特に重要です。私たちは、こうしたリポジトリがソフトウェア開発の新たな標準になると考えています。

Pando の構築

私たちは Codex を使って Pando を構築し、着手から本番稼働まで、わずか 10 週間で実現しました。Pando の中核は、1 人のエンジニアが設計し、エージェントのチームが実装し、少人数のエンジニアがデプロイしました。Pando は今年 4 月に本番トラフィックの処理を開始しました。OpenAI で使われている、より大きなソース管理ツール群の一部を担っています。

Pando の設計検証には、さまざまなアプローチを採用しました。私たちは、より安全なソフトウェア基盤の構築⁠に関する原則を適用しました。新規プロジェクトであることを活かし、Pando は Rust で実装し、レガシーな依存関係を持たせないことにしました。エージェントが変更内容を標準の Git の動作と照らし合わせてテストできるよう、適合性テストスイートを作成しました。また、システムの不変条件や性質を確認できる決定論的シミュレーションテスト用ハーネスを開発し、ネゴシエーションやパック計画に潜む微妙な競合状態やリグレッションを発見して軽減できるようにしました。

このシステムには多くのモデルが貢献しています。新しいモデルが利用可能になるたびに、それらを活用して Pando の性能と信頼性を高めました。GPT‑6 Astra を使ったエージェントが Lean と TLA+ を記述して「Pando の理論」を形式化し、私たちはそれを使って実装上のバグを発見・修正し、システムの性質を証明しました。複数の継続セッションを数百時間にわたって実行し、Pando のエッジケースを発見・修正しました。また、Daybreak 対応モデルを使うエージェントを 24 時間稼働させ、Git と Pando のレッドチームテストを行っています。

Pando の導入

当初、Pando は既存のソース管理システムに対するリードスルー・ライトスループロキシとして導入しました。これにより Pando と従来のシステムの同期を保ち、ダウンタイムなしでトラフィックを安全に Pando へ移せました。

Pando はキャッシュ設計の一環として、転送前にリクエストの重複を排除し、まとめることもできます。そのため、システムを 1 つずつ Pando に移す間も、既存のツールを安定して稼働させられました。Pando に移行するワークロードが増えるにつれ、そのプッシュによって Pando のキャッシュが充実し、従来のバックエンドの信頼性も向上しました。

Pando 利用時の性能を最適化するため、公開している当社の Git ディストリビューションを開発者のマシンと ChatGPT デスクトップアプリ内に配布し始めました。クライアントの Pando への移行には、Git CLI を包む社内ラッパーを使い、段階的に進めました。変更リクエストやコードレビューを扱うツール、自動化のための API など、ほかにも多くのツールを作成しました。Git トラフィックと並行して、従来の API、Webhook、リポジトリのダウンロードに依存する数百の連携も移行しました。

Codex 自体も、開発者やチームの移行を支援しました。既存のチェックアウトを準備するスキルや、エージェントに新しいシステムの使い方を案内するスキルを作成しました。その結果、開発者は Codex に依頼するだけで、開発環境やサービスを移行できるようになりました。

Pando への切り替えにより、マージの信頼性が向上し、プッシュのレイテンシーの中央値は約 80% 減少しました。

Pando は、わずか数個の CPU コアで、1 日あたり 1 ペタバイトの Git データを配信できます。Pando は 1 時間で 57 テラバイトを超える Git パックを配信した実績があり、そのデータの 99% 以上はローカルディスクキャッシュから提供されました。

パックの再利用により、配信ノードでの計算処理をほとんど必要とせず、多数のクライアントが同じリポジトリの履歴をダウンロードできます。これは単一リポジトリの事例ですが、Pando はさらに多くのリポジトリを支えられるように設計されています。需要の増加に合わせ、データ配信と Git のネゴシエーションを独立してスケールできます。

Git プロジェクトへの還元

Git プロトコルとクライアントを基盤に Pando を構築したことで、Git 自体のバグや性能改善の余地を見つけられました。

たとえば、Pando は再利用可能なパックをクライアントに直接提供するため、パックのダウンロードとインデックス作成が、システムの高速化と信頼性向上の鍵になりました。その取り組みで、同時ダウンロード、中断した転送の後処理、完了済みダウンロードの再開に関するエッジケースが明らかになりました。それらの処理経路を修正し、フェッチで既存のパックを受け入れる際の検証を追加して、アップストリームに還元しました。

また、時間と帯域がどこで使われているかを把握するため、packfile-uri のダウンロードのトレースと、パック生成時のバイト数計測を追加しました。これにより、遅いフェッチがパックの生成と転送のどちらに時間を費やしているのか、判別しやすくなりました。

OpenAI の Git ビルド⁠(新しいウィンドウで開く)では、クライアントは独立したパックのダウンロードとインデックス作成を並列に実行できます。「明示的な have」という概念を追加し、サーバーがブランチの現在の先端を、プッシュするクライアントがパックから除外してよい履歴とは別に通知できるようにしました。Pando ではこれにより、クライアントが --force-with-lease を使って機能ブランチが更新されていないことを確認しつつ、トランクだけをベースにパックを作成できます。そのトランクの履歴を持つクライアントなら、同じパックを使ってそのブランチをフェッチできます。

これらの改善の一部は既にアップストリームの Git に取り込まれており、ほかの多くの改善も当社の公開リポジトリで利用できます。私たちは、これらの改善が Pando 以外でも役立つよう、Git コミュニティとの協力を続けています。その取り組みは、当社の公開パッチトラッカー⁠(新しいウィンドウで開く)で確認できます。

今後の展望

Git リポジトリのスケーリングに関する常識を見直すことで、Pando はエンジニアやエージェントが使い慣れた Git ワークフローを変えずに、モノレポをスケールさせることを可能にしました。当社のモノレポでは、Pando によって並列処理の幅が広がり、マージの信頼性が向上し、コードの変更を公開するまでの時間が短縮されました。モノレポ以外でも、多数のエージェントが共有の Git リポジトリで共同作業するワークロードなど、OpenAI 内で Pando を活用するほかの用途を探っています。

エンジニアやエージェントがより多くのソフトウェアを生み出すにつれ、OpenAI 以外でも、私たちが直面したソース管理の限界に突き当たるチームが増えるでしょう。現在、Pando を支える技術をユーザーの皆さまに共有する方法を検討しています。

Pando は、Git リポジトリのユーザーや運用者が 20 年にわたって積み重ねた経験を基盤としています。今後も Pando の開発を続けながら、オープンソースの Git コミュニティと協力し、幅広い Git ユーザーに役立つ改善を共有していきます。

著者

OpenAI