あらゆる規模のGit
大規模なGitリポジトリのホスティングは悪夢です。Linus Torvaldsが_地獄から来た情報マネージャー_の最初のバージョン (実際、これがGitのキャッチフレーズです。確認してみてください) を設計したとき、彼の念頭にあったユースケースは非常に具体的でした。それは自分自身のためのものでした。彼は、Linux Kernelの開発に使われていた分散バージョン管理システムであるBitKeeperを置き換えようとしていました。もちろん、その代替も分散型でなければなりません。Kernelは特殊なソフトウェアプロジェクトです。数多くのサブシステムごとに異なるメンテナーが存在し、極めて分散化されています。分散バージョン管理システムは、このワークフローに自然に適しています。
20年後、Gitは業界標準になりました。しかし実際には、その分散型という性質は利点よりも足かせになっています。一般的なオープンソースソフトウェアプロジェクトは、分散型のワークフローで運用されていません。一般的な企業なら、なおさらです。オフラインで作業できる、pushを遅らせられるといった分散モデルの多くの利点を活用しつつも、中央集約型のホストに大きく依存しています。そしてGitリポジトリのホスティングは、実のところ、途方もなく難しいのです。
Git の難しい点とは?
Git リポジトリを大規模にホストする際の課題は、Git 自体の設計に起因します。Git は_分散型_バージョン管理システムであるため、リポジトリのすべてのインスタンスは同一です。Git サーバー上のリポジトリだけに、開発者のノートパソコン上のリポジトリにはない特別な性質があるわけではありません。一見すると、Git リポジトリのホスティングは簡単に思えるかもしれません (ディスク上のリポジトリの前に HTTP デーモンを置くだけで、Git サーバーが動き出します!) 。しかし実際には、スケーラビリティと信頼性に関する難しい課題が数多くあり、状況はまったく逆です。
通常の Git リポジトリでは、コードとメタデータ (ファイル、コミット、ツリー) は圧縮され、packfile に保存されます。packfile は単純なバイナリ直列化形式で、ローカルマシンでは扱いやすい一方、サーバー上で大規模に管理するには適していません。packfile は、Git ストレージと Git ネットワークの基本的な構成要素です。リポジトリにデータを push または fetch すると、データは packfile として転送されます。
これは Git の設計どおりの動作ですが、必ずしもそうである必要はないと考えるのももっともです。結局のところ、Git クライアントは制御できません (少なくとも、ユーザーを煩わせて大きな摩擦を生まずにはできません) 。しかし、自分のサーバー内であれば、望む_あらゆること_ができます。packfile の使用に縛られる必要はありません。Linus がやって来て確認することもありません。唯一の制約は、すべての Git 操作でネットワーク経由で packfile を受信・送信する必要があることです。
長年にわたり、Git リポジトリの大規模ホスティングに取り組んできた企業は、この packfile ベースの設計が、可用性とスケーラビリティの両方における大きな制約であることに気付きました。packfile は大きなバイナリファイルであり、Git がアクセスするにはファイルシステム上に存在している必要があります。ディスク上のリポジトリの前に HTTP サーバーを置くだけの単純なアプローチには、すぐに限界が来ます。理想的には、リポジトリを複数のディスクと複数のマシンに配置したいところです (これにより、多数の Git 操作を並列に実行でき、サーバーがクラッシュしてもリポジトリを利用可能な状態に保てます) 。しかし、それをどう実現すればよいのでしょうか?
これを実現するアプローチは、大きく3つあります。複雑度の低い順に、ファイルシステムを分散する、packfile を分散する、Git 自体を分散する、という方法です。
packfile を使わない Git
Git はコンテンツアドレス可能なデータストアです。Git リポジトリ内のすべてのオブジェクト (blob、tree、commit など) は、その内容の SHA-1 をキーとして識別されます。これは分散キーバリューストアに非常によく対応します (キーは SHA-1、値は実際のオブジェクトです) 。リポジトリのストレージをスケールアウトする、すっきりした方法にもなり得ます。しかし実際には、これはうまく機能しません。
問題は、Git リポジトリの実際の構造が有向非巡回グラフ (DAG) であることです。SHA を使って任意のオブジェクトを取得できますが、リポジトリ内でごく単純な操作を行うだけでも、DAG を一歩ずつたどる必要があります。
リポジトリの最近の変更を一覧表示するような操作を行うには、commit を処理する必要があります。commit を処理すると、その tree のルートへのポインタが得られます。その tree からは、各ファイルや各サブツリーへのポインタが得られます。元の commit からは、その親、つまり履歴上で直前の commit へのポインタが得られます。重要なのは、この走査の各ステップで、前の値を fetch するまで次のポインタの値がわからないことです。fetch のたびに分散ストアとのラウンドトリップが必要になると、すぐにコストが非常に大きくなります。
オブジェクトレベルで Git を分散するこのアプローチは、これまで何度も試みられてきましたが、大規模に運用すると失敗しがちです。最も有望だった実装は、私の元メンターである Shawn Pearce が Google のバージョン管理システムチームで働いていた際に試みたものでした。彼のアプローチは、オブジェクトを分散ハッシュテーブルに保存するというものでした。これは、Java で実装されたカスタム Git 実装である JGit があったからこそ可能でした。昔ながらの優れた Java ライブラリらしく、JGit は通常の Git リポジトリに関するあらゆる詳細を抽象化できるだけのインターフェース、ファクトリ、インターフェースファクトリを備えており、ディスク上の packfile を DHT に置き換えることも可能です。システムは動作し、通常の Git 操作には十分な結果が得られましたが、Git プロトコルの制約、つまりサーバーでデータをどのように保存していてもネットワーク経由で packfile を送信する必要があるという制約により、git clone のパフォーマンスが悪くなり、この設計は最終的に断念されました。
GitHub とファイルシステム
Git が Linux Kernel の枠を超え始めて数年後、サンフランシスコで気骨あるスタートアップが誕生しました。GitHub は 2008 年、「Git リポジトリのホスティング:もう苦痛ではない」という非常に先見の明のあるタグラインを掲げるソーシャルコーディングプラットフォームとして設立されました。冗談ではありません。実際に確認してみてください。2008 年当時すでに、Git の分散型デザインにもかかわらず、あるいはむしろそれゆえに、Git リポジトリを使いやすくするには中央集約的にホストする仕組みが必要であり、それを実現するのは非常に大変だという認識が広く共有されていました。GitHub はそれを変えようとしていました。
そのプラットフォームは Rails モノリスとして始まり、現在も大部分はそうです。最初のバージョンは、Ruby サーバーと、その横のディスクに置かれたリポジトリの複製物を備えた、強力ではあるものの 1 台のマシンで稼働していました。Rails アプリのスケーリングは簡単です。インスタンスを追加でデプロイすればよいのです。しかしこのケースでは Git が関わるため、すぐにここで解決しようとしている繰り返し現れる問題に直面しました。Rails アプリがディスク上の Git リポジトリにアクセスする必要がある場合、その複製物をさらにデプロイするにはどうすればよいのでしょうか。
倹約家の寄せ集めだった GitHub の初期システムエンジニアは、スケーリングの問題を解決できそうな最もシンプルなアプローチを試しました。ファイルシステム (packfile や Git 自体ではなく) の分散に注力すれば、Rails アプリを変更せずに済み、Git に奇妙なことをさせる代わりに、増え続けるユーザーベース向けの機能提供に時間を使える、という考えでした。非常に実用的です。しかし、うまくいきませんでした。
チームは Git データ向けの分散ファイルシステムについて、多くのアプローチを試しました。最も分かりやすい方法である、NFS を使ってすべてのリポジトリを中央サーバーに保存する方式は、すぐに却下されました。Git のデフォルト実装は、遅い開発者用ノート PC のローカルファイルシステムでも十分なパフォーマンスを確保できるよう、ファイルシステムのセマンティクス (ロック、破損、読み取り、同期など) について多くの前提を置いていますが、それらがネットワークファイルシステム上でどのように振る舞うかはまったく考慮していません。低速で、バグもありました。
その後、率直に言えば振り返ってみると恐ろしい、ブロックレベルでファイルシステムを複製する技術も試されました。GFS による短期間のデプロイ。DRBD をベースにした、より長期間のデプロイ。いずれも行き詰まりました。日常的な運用は ひどい もので、その分を補えるほどパフォーマンスが良いわけでもありませんでした。すべては、ディスク上の packfile のデザインに起因します。
Git のグラフ状データ構造によって、ラウンドトリップが法外に高コストになることはすでに見てきました。残念ながら、非常によく似た原理がディスク上の基盤データにも当てはまります。DAG 内のオブジェクトの配置と、それらが packfile 内に置かれる方法には相関関係がありません。packfile の生成時に使われる主なヒューリスティックは、サイズを最小化することです。オブジェクトはパック全体にランダムに配置され、圧縮され、さらに重要なことに、完全な形で保存されることはほとんどありません。ほとんどのオブジェクトは、同じ packfile 内の別のオブジェクトを基にした差分として保存されます。個々のオブジェクトを読み取るには、グラフデータ構造内の数多くの論理的なホップをたどった後、ディスク上の形式における物理的なホップもたどる必要があります。
リポジトリで実行されるすべての Git 操作で発生する、ギガバイト単位のデータをまたぐこの種のランダムウォークは、ネットワークファイルシステム (ファイルレベルかブロックレベルかを問わず) とは相性がよくありません。極端な低速化を避けて動作させるには、ファイル全体をローカルにキャッシュできる必要があります。しかし、同じファイルシステムに数十万ものリポジトリがあるため、キャッシュは選択肢になりません。
最終的に、GitHub のシステムエンジニアは意を決し、ファイルシステムの分散を断念しました。リポジトリを専用ファイルサーバーに置けるようにする RPC システムの開発を始め、Rails アプリを更新して、すべての操作をリモートで実行するようにしました。これによりかなりの水平スケーラビリティが得られましたが、可用性も、最も利用頻度の高いリポジトリのパフォーマンスも改善されませんでした。結局のところ、各リポジトリは依然として 1 台のマシンにしか保存されていなかったのです。
Spokesと一貫性
Spokesは2013年ごろにGitHubで開発され、その後業界標準となりました。現在では、ほとんどのGitホスティングサービスが、Spokesのアプローチ (Gitリポジトリに対するアプリケーションレベルのレプリケーション) の派生形をアーキテクチャに採用しています。Spokesが長年にわたってうまく機能してきた主な理由は、時を経て最適であることが実証された、次の3つの基本的な選択をしたことにあります。
- Git自体を分散するのではなく、packfileレベルで処理する。
- すべてのデータを、ローカルNVMeディスク上の実際のGitリポジトリとして保存する。
- Gitデータをレプリケートしつつ、すべての複製を一貫して同期状態に保つ。
先ほど説明した_packfile_内でのランダムな読み取りパターンを踏まえると、基本的なGit操作をすべて高速に保つには、素のGitリポジトリをNVMeドライブに保存することが実質的に必須です。また、データをGitクライアントが期待する形式に変換する必要がないため、cloneも効率的に行えます。さらに、特殊なリポジトリで動作するGitのフォークを自ら保守するのではなく、Gitの上にプロダクトを構築することに集中できます。
データのすべての複製を一貫して同期状態に保つことも、極めて重要です。これは痛い目を見て学ぶことですが、Gitクライアントは_結果整合性_と本当に相性がよくありません。ローカルのGitクライアントがcommitをpushした後、fetchの直後にそのcommitを読み取れなければ、大きな問題です。Gitはこうした状況を非常に混乱します。100台のrunnerでCIパイプラインを実行し、そのうち3台がリポジトリをcloneした後にテスト対象のcommitを見つけられなければ、それも大きな問題です。ユーザー体験も非常に悪くなります。
クライアント側でもバックエンド側でも、Gitリポジトリの結果整合性のあるビューを扱うには、多くの落とし穴があります。そのためSpokesは、システムが常に完全な一貫性を保てるよう、非常に高い複雑度を受け入れています。これが具体的に何を意味するのか見ていきましょう。
Spokesは_コンセンサスベースの_分散システムです。Gitリポジトリの複数の複製を異なるサーバーに保存することで動作します。新しいデータをpushするたびに、オーケストレーターがpushをファンアウトし、リポジトリのすべてのインスタンスが複製を受け取るようにします。この「fan-out」は、3PC (三相コミット) と呼ばれる古典的なコンセンサスアルゴリズムによって同期されるため、過半数のノードが承認した場合にのみpushが受け入れられます。
Spokesが3PCをどのように使うかをさらに説明する前に、Git pushの仕組みを理解する必要があります。Git pushには、_packfile_と_reference transaction_という2つのコンポーネントがあります。すでに説明したpackfileには、リポジトリにpushするオブジェクト (変更を含むblob、tree、commit) が含まれます。transactionは、1つ以上のreference (たとえば作業中のbranch) を、先ほどpushしたcommitを指すよう更新することで、実際に変更をリポジトリへ公開します。
この分離はここで非常に便利です。なぜなら、pushされたcommitは、それを指すreferenceが更新されるまで可視にならない (Git用語では「reachable」にならない) からです。つまり、packfileをすべてのhostへ同時に分配し (ここでは同期する必要はありません) 、その後に、packfileよりはるかに小さく同期も高速なreference transactionに対して三相コミットを実行することで、pushに対するコンセンサスを実装できます。Git自体もreference transactionの準備をサポートしています。referenceのlockを取得し、既存の値が想定どおりであることを検証してから、transactionに対するcommitまたはabortコマンドを受け取るまでlockを保持できます。
この設計により、すべてのpushが全replica間で完全に同期されます。その後、すべてのreplicaは常に最新の状態であるため、読み取り (fetch、clone) は_any single replica_に安全にルーティングできます。
これがSpokesの基本的な仕組みであり、過去13年間にわたって非常にうまく機能してきました。もちろん、Spokesは完璧ではありません。完璧なシステムは存在しません。2026年には、人々のGitリポジトリの使い方は大きく変化しており、その過程で分散システムの構築について多くの重要な教訓を学びました。時と経験を通じて、Spokesの選択のどれが最適だったのか、そしてどれがそうではなかったのかが明らかになりました。
重大な欠点として明らかになったのが、3PCの水平スケーラビリティが制限されることです。Spokesが最初にリリースされた当時、リポジトリあたり3つのレプリカが最適な構成でした。平均的なリポジトリであれば、3つのコピーで余裕をもって処理でき、1台のマシンが停止してもpushを受け付け続けられるだけの冗長性がありました。
2026年の状況は大きく異なります。現在、企業の平均的なリポジトリは巨大なモノレポです。このようなリポジトリのトラフィック、特にCIのトラフィックを処理するには、3つのレプリカでは不十分です。もちろん、厄介な大規模環境でのテールレイテンシを除けば、Spokesを3つを超えるレプリカで実行できない理由はありません。三相コミットはGitのトランザクションモデルに非常に自然に対応しますが、コンセンサスアルゴリズムとしては根本的な制約があります。各ステップのレイテンシは、クラスター内で最も遅いサーバーに左右されます。クラスターにレプリカを追加するほど、pushスループットは低下します。
このスケーラビリティの制約は、逆の方向にも当てはまります。エージェントが大規模にGitリポジトリを扱う場合、モノレポではなく、膨大な数の小規模なリポジトリを作成して利用することも珍しくありません。その多くは使い捨てで、ほとんど使われません。Spokesは、こうしたリポジトリにもそれぞれ3つのレプリカを必要とするため、このケースでは苦戦します。大半がアイドル状態の3つのレプリカであっても、減らすとシステムの完全な一貫性が失われ、データ損失の可能性が生じるため、削減できません。三相コミットでは、下限は常に高すぎ、上限は低すぎます。
もう1つの欠点は、事前には見えず、実際に苦しんだ後になって痛感するものです。Spokesは大規模環境での運用が_厄介_になることがあります。ディスク上のリポジトリが常にコンセンサスにおける信頼できる情報源であるため、すべてのリポジトリのすべてのコピーが_非常に重要_です。リポジトリは_家畜ではなくペット_として扱う必要があります。
つまり、まずすべてのリポジトリがどこにあるかを正確に把握する必要があります。そのため、各リポジトリと、それがレプリケートされているすべてのマシンを対応付ける非常に大規模なルーティングテーブルを維持する外部データベースへの依存関係 (および潜在的な可用性の問題) が生じます。また、すべてのリポジトリのチェックサムを計算し、リポジトリがディスク上で有効な状態を保っていることを確認するため、そのチェックサムをテーブル内で継続的に更新する必要があります。リポジトリに問題が発生したら (信じてください、悪いことは常に起こります。Gitは実運用では非常に扱いが難しいことがあります) 、それを検出し、正常な状態に戻す修復ジョブをスケジュールする必要があります。そして、これは非常に迅速に行わなければなりません。繰り返しますが、ディスク上のリポジトリが信頼できる情報源だからです。破損したコピーは、コピーが存在しないのと同じくらい深刻です。3つのコピーのうち2つが破損すると、クォーラムを満たせないため、システムはpushを受け付けられなくなります。
Continuity
Continuity (略して Cnt) は、Cursor が開発した Git ストレージシステムです。アプローチは非常に明確です。Spokes の優れた点をすべて取り入れ、長年を経て問題だと分かった点を修正します。
Cnt はシンプルなシステムです (システムはシンプルでなければ運用も容易にはなりません) 。中核となるプリミティブは先行書き込みログで、S3 互換のオブジェクトストレージに保存します。本番環境では S3 上で直接稼働させていますが、任意のクラウドにデプロイできるよう設計されています。
リポジトリが push を受け取ると、その push を S3 に WAL エントリとして保存します。**完全に永続化されるまで、push を受理することは決してありません。**各 push は個別のオブジェクトとして保存されます。push された packfile をディスクに書き込み、同時に S3 へアップロードします。ただし、WAL エントリをアップロードしただけでは公開されません。ローカルのリポジトリコピーで reference transaction を正常に準備し、ストア内の別オブジェクトである WAL インデックスファイルに WAL エントリへのポインタを記録して初めて、push が可視になります。これにより、すべての push は線形化可能になります。
push ごとに S3 への書き込みを 1 回だけ行うことは避けています。アクセスの多いリポジトリでは、S3 PUT 操作のレイテンシが push スループットの厳しい上限となるためです。慎重に調整したバッチ処理の実装により、またレプリカのクォーラムではなく単一のローカルリポジトリと reference transaction を同期するだけでよいため、ディスクが許す限り高速に push を取り込めるシステムを実現しています。
もちろん、リポジトリのローカルコピーは、非常に高速な NVMe ドライブに保存された通常の Git リポジトリです。Spokes と同じ方法を採用しています。Spokes はこの点をまさに正しく実現していたと考えているからです。これにより、アップストリームの Git クライアントや数多くのパフォーマンス最適化を含む、Git コミュニティによる優れた OSS の成果をすべて再利用できます。Git に奇妙な手を加えるのではなく、新機能のリリースに集中できます。
コンセンサス
Spokes クラスターの運用が難しい理由の一つは、各サーバー上のすべてのリポジトリの配置を正確に把握しておく必要があることです。Cnt では、これをまったく異なる方法で扱います。各リポジトリはどこにあるのでしょうか?答えは「どこでも」です。問題ありません。リポジトリはディスク上のウォームキャッシュとして扱いますが、信頼できる唯一の情報源は常に S3 上の先行書き込みログです。システムはステートレスで、ルーティングテーブルもありません (運用が必要なリレーショナルデータベースもありません — ありがたいことです) 。ホスト上でリポジトリにアクセスした際、ローカルディスクに存在しなければ、WAL からマテリアライズします。これは非常に効率的に行えますが、無駄なので、当然ながら 常に 行いたいわけではありません。本番環境では、ランデブーハッシュ を使用して、リポジトリ ID をそのリポジトリが存在すると想定されるノードのリストにマッピングしています。リポジトリのルーティングに必要な状態は、リポジトリ ID とクラスター内で現在正常なノードのセットだけです。ただし、この状態の同期がずれた場合 (たとえばノードが異常になった場合) でも、まったく問題ありません。次に選ばれたノードでリポジトリをマテリアライズするだけです。
コンセンサスはどうでしょうか?選出は?特定のリポジトリの primary はどのサーバーでしょうか?これも問題になりません。ここには状態もコンセンサスもありません。どのサーバーも primary になれます。先行書き込みログへのすべての更新は、S3 上のアトミックな compare-and-swap (CAS) 操作で同期されるため、リポジトリのどのインスタンスが push を受け取っても常に安全です。繰り返しになりますが、ルーティングと同様、任意のサーバーを primary として動作させるのは最も効率的な方法ではありません (CAS の再試行が発生し、push が遅延する可能性があります) 。そのため実際には、ランデブーハッシュで順位付けされたリストの先頭にあるサーバーを、常に primary として選択します。ただし、デプロイ、フェイルオーバー、ネットワークの一時的な障害といった例外的な状況では、どのサーバーが primary なのかを厳密に気にする必要はありません。このシステムは、性能が低下した状態でも常に正しく動作し、healthy な状態では常に高速に動作するよう設計されています。
レプリケーション
先行書き込みログを S3 に置くことで、スケールに関する可能性が大きく広がります。S3 は比類ないスケーラビリティを備え、すべてのレプリカがそこから直接追いつけるため、文字どおり いくつでもレプリカを持てます。クラスタ内で gossip UDP パケットをやり取りすることで、楽観的レプリケーションを行います。パケットには、各レプリカが push のたびに S3 から直接追いつくために必要なメタデータがすべて含まれています。「それはすごいな」と、時空を越え、画面の向こうであなたがつぶやくのが聞こえてきます。「UDP は信頼できるトランスポートではない。」もちろん、そうです。分散システムでは、何も信頼できません!ネットワークも、ルーティングも、トポロジーも信頼できません。しかし問題ありません。重要ではないからです。各レプリカは、追いついている WAL インデックスの最新バージョンの ETag を把握しています。レプリカに対して読み取り操作を行うと、想定する ETag を使って S3 に条件付き GET を実行します。ボディのない 304 応答は、最新の状態であることを示し、すぐに fetch または clone を提供できます (メタデータのみを対象とする S3 操作のため、平均 10ms 未満とほぼ瞬時です) 。200 応答には WAL インデックスの最新バージョンが含まれており、読み取りを提供する前にそれを使って追いつきます。
レプリケーション用 UDP パケットが失われても、トポロジーの変化によって誤ったサーバーに届いても問題ありません。すべてのレプリカでのすべての読み取りは、信頼できる情報源である S3 に対して検証されるため、完全に一貫しています。システムは、性能が低下した状態でも常に正しく動作し、正常な状態では常に高速に動作するよう設計されています。
これには 2 つの意味があります。まず、システムは常に一貫しているため、その上にインフラストラクチャを構築するのは容易です。私たち (エージェント、Web インターフェース、クライアント) は常に、リポジトリ全体で一貫したビューを確認できます。また、システムは_両方向に_スケールするため、各リポジトリには最適な数のレプリカが割り当てられます。大規模な モノレポ は、CI ジョブからのすべての負荷に対応するため、数百のレプリカにデプロイできます。エージェントが作成する数百万の小規模リポジトリには、それぞれ 1 つのレプリカで対応できます。S3 が信頼できる情報源であるため、可用性を確保するのに複数のレプリカは必要ありません。実際、アイドル状態のリポジトリには 1 つすら必要ありません。レプリカがしばらくトラフィックを受信していない場合は、ノードのディスクからガベージコレクションし、次に fetch が届いたときに WAL から再度実体化するだけです。
コンパクション
先行書き込みログには定期的なコンパクションが必要です。ログを無制限に増やし続けることはできません。完全復元ではすべてのエントリを再生するため、エントリが増えるほどコストも高くなります。
通常の Git リポジトリにも、偶然にも定期的なコンパクションが必要です。Git は WAL をベースとしていないにもかかわらずです。Git リポジトリにおけるストレージの基本単位は packfile です。リポジトリのリモートコピーに push するたび、またはローカルコピーに fetch するたびに、新しい packfile が作成されます。これは無限にスケールするものではありません。各 packfile にはそれぞれインデックスが付属しており、Git はそこに含まれるオブジェクトを効率的に検索できます。しかし、この検索が効率的なのは packfile 単位に限られます。特定のオブジェクトを探す際、リポジトリに 100 個の packfile があれば、いずれかの packfile で見つかるまで、それぞれのインデックスを開いてオブジェクトを検索する必要があります。数百回、数千回と実行しなければならない操作は、効率的とは言えません。
最新の Git はこの問題への対処に非常に長けており、マルチパックインデックスや増分の幾何学的コンパクションをサポートしています。しかし最終的には、ディスク上の Git リポジトリを再パックする必要があります。歴史的に、これは Spokes のようなシステムにおける恒常的な可用性の課題でした。再パックは増分で行った場合でも CPU 負荷が非常に高く、システム内のすべてのレプリカで実行する必要があるためです。同じリポジトリに対して 2 つ以上の Spokes ノードで誤って保守操作をトリガーすると、リポジトリは簡単にフェイルオーバーしてしまいます。
ここでは、コンパクションのコストを償却します。コンパクションを実行するのは primary のみで、その結果はディスク上のリポジトリと WAL の両方に適用されます。すべてのレプリカは WAL に追従するため、コンパクションイベントにも追従します。レプリカは再パックを行わず、すでにコンパクション済みの pack を S3 からダウンロードするだけです。CPU 使用量を抑える代わりに、帯域幅を使用します。
スケール
レプリケーションとコンパクションは、Git ストレージシステムの負荷時の挙動を左右する2つの重要な要素です。先ほど見たように、両者は本質的に結び付いています。リポジトリが1秒あたりに取り込む push が増えるほど、読み取りパフォーマンスは低下します。Git 操作の効率を維持するには、すべての push の packfile をコンパクションする必要があるためです。これらの push をレプリケーションする場合、コンパクション結果もレプリケーションするか、各レプリカで個別にコンパクションを実行する必要があります。
Continuity の WAL-first 設計は、完全な一貫性を備えた水平スケーラビリティを実現します。任意の数のレプリカをデプロイでき、読み取り専用の Git 操作のスループットはレプリカ数に比例して増加します。クラスター内のすべてのレプリカが完全に一貫しているため、Git プロトコル (clone、fetch) と、Origin がリポジトリ上で実行するすべての RPC 操作 (Web UI でのやり取り、REST API、すべてのエージェント型インターフェースなど) をスケールできます。
最大100個のレプリカで合成ストレステストを実行し、push スループットを低下させることなく、読み取りが一貫して線形にスケールすることを確認しました。
クラスターの push スループットは、S3 上の WAL を更新できるレイテンシに依存します。S3 Standard では、コンパクションを行い、コンパクション済みデータを他のすべてのノードにレプリケーションしながら、最大120 push/s を維持できます。また、PUT 操作のレイテンシが大幅に低い S3 Express One Zone 上にも高パフォーマンスクラスターをデプロイしています。この環境では300 push/s 以上を取り込むことができ、実質的には Git がディスク上のデータをコンパクションできる速度がボトルネックになります。コンパクションの影響を軽減するため、このデータをディスク上に配置する革新的な方法に取り組んでいます。耐久性と一貫性に関する厳格な保証を緩めることなく、Git リポジトリがコードを取り込める速度を継続的に最適化することが目標です。
- S3 Standard
- S3 Express One Zone
Cursor の モノレポ である everysphere の Push/Clone スループット。
すべての push は線形化可能で、確認応答前に外部ストレージへ永続化されます。
すべての clone は完全に一貫しています。
真実の源泉としての WAL
S3 は優れた技術です。S3 API によって先駆的に確立された blob storage という概念は、大規模データストレージシステムの非常に強力な構成要素であることが実証されており、Git リポジトリのホストにも間違いなく当てはまります。ここで紹介する設計には多くの新規性がありますが、packfile を blob として保存する最初のものではありません。Azure DevOps (Microsoft 自身の GitHub に対する Microsoft 自身の競合製品) には、packfile を blob storage に、参照をリレーショナルデータベース (MS SQL Server) に保存する、非常に成功している Git ストレージシステムがあります。このようなシステムには多くのトレードオフがあります。リレーショナルデータベースは大規模な reference transaction に適しています。しかし、そのためにはリレーショナルデータベースを運用する必要があります。私たちは、Git データの一貫性が他のどの考慮事項よりも重要だと強く信じています。これが、外部データベースに依存しない WAL ベースのシステムを設計する決め手となりました。
本番環境の Git リポジトリでは、多くの問題が発生する可能性があります。保存中のデータ破損、再パック中のバグ、push 中の競合。まさにエッジケースの集合です。その大半は Git 本体で解消されています。しかし、すべてではありません。OSS で広くデプロイされているシステムであっても、バグがないわけではありません。当社の一貫性モデルにより、リポジトリで発生するすべての基本操作を追跡できます。push が WAL に完全に永続化されるまで、決して受け付けません。すべての push を直列化します。アクセスするすべてのリポジトリのすべての表示は、常に完全に一貫しています。 すべての push は WAL に記録されるため、リポジトリがこれまでにどのような状態にあったかをすべて確認できます。すべての push とすべての再パックについて、完全な来歴データを保持しています。すべての replica を巻き戻したり、早送りしたりできます。参照のみを保存するデータベースであれ、すべての object データを保存するデータベースであれ、外部データベースと状態を同期する必要はありません。Git でバグに遭遇した場合 (遭遇するかどうかではなく、いつかの問題です) 、何が起きたかを正確に特定し、元に戻せます。また、Git にすでに存在するバグ以外には、新たなバグをほとんど導入しません。なぜなら、このすべてを通じて、すべての Git 操作は市販のツールを使い、disk 上の通常の Git リポジトリで実行されるからです。
Origin
私たちは、誰かのソースコードをホストすることがどれほど重要かを痛感しています。このブログ記事を読んで内容を理解している皆さんも、その重要性を十分に認識しているはずです。開発者が Git リポジトリにプッシュやプルをできなくなれば、企業の業務は停止しかねません。CI システムが 5 分間停止した場合の生産性損失を金額で算出するのは難しいものの、どのような基準で見ても莫大です。
エージェントはソフトウェアとの関わり方を根本から変え、多くの面でこの状況をさらに悪化させました。コードは増え、PR は増え、CI の実行回数も増えます。バージョン管理はそのすべての中核にあり、一夜にして変更するのが最も難しいものかもしれません。
Cursor でも、こうした課題に数か月にわたって社内で直面してきました。そこで私たちは、自分たちの課題を解決し、できればお客様の課題も解決できるプラットフォームの構築に、多くの時間と思慮を注いできました。現在は、より高い信頼性、優れたパフォーマンス、より大きなスケールへとできる限りスムーズに移行できるようにし、その移行の負担を可能な限り軽減することに注力しています。
Origin は実験ではありません。こうしたシステムを何十年にもわたって構築してきた、課題の大きさを深く理解する人々の経験の結晶です。私たちには、実証済みのエンジニアリングと運用の哲学があり、バージョン管理を取り巻く環境の進化に合わせて、それを継続的に発展させていく強い意志があります。
私たちと私たちのプラットフォームを信頼していただけることを願っています。