リポジトリが「ひとつの箱」では足りなくなった
GitHub が新しい Git ストレージの話を出したとき、目を引くのは 5,000 体の同時エージェントや 7,200 push/秒という派手な数字だが、問題の芯はもっと地味だ。従来の Git は、ひとつのリポジトリをひとつの整った塊として扱い、更新のたびに整合性を保つための手順を積み重ねてきた。人間が手で触る範囲ならそれで十分だった。
ところが、そこにエージェントが入ると状況が変わる。1 人の開発者がたまに送る push ではなく、同じリポジトリへ複数の書き込みが並走し、しかもその直後に CI、デプロイ、別のエージェントの参照取得が連鎖する。書き込み 1 回が読み取りの波を生むので、単純な「書き込みが増えた」では済まない。全体の通信量が一段ずつ膨らむ。
GitHub が説明しているのは、まさにその膨らみ方への備えだ。新しい設計は、単に台数を増やすだけではない。書き込みをもっと細かい単位に分け、読み取りと保存と計算を同じ箱の中に閉じ込めない。ここで重要なのは、従来の「リポジトリ全体をまとめて守る」考え方が、どこで息切れするかという点にある。更新の大半は、実は全体の同意を必要としない。
Git のオブジェクトは内容で名前が決まり、変更できない。だから同じオブジェクトを別々の書き手が同時に保存しても、ぶつかる必要はない。一方で、参照の更新だけは順序を守らないといけない。この差が、新しい設計の入口になる。GitHub はこの部分を、どこまで細かく切り分けたのか。
1,000マイル先の副作用と、待ち行列の正体
旧来の構成では、同じリポジトリが複数の地域にまたがるだけで、書き込みの上限が見えてしまう。GitHub の説明では、あるレプリカが Azure の対象リージョンにあると、社内データセンターから約 1,000 マイル離れ、1 回の往復におよそ 25 ミリ秒かかる。しかも書き込みには複数回の往復が要る。ここでロックがリポジトリ全体にかかると、遅い書き込みが次の書き込みまで抱え込む。
数で考えると、この厳しさが見えやすい。たとえば 1 回の書き込みに 25 ミリ秒の往復が 1 回入るだけで、ロックを握っている間は次の書き込みが進めないので、理屈の上では 1 秒に 40 回が上限になる。往復が複数回なら、その数分の一まで下がる。もちろん実際にはもっと要素があるが、単純な並列化を許さない構造では、どれだけ機械を増やしても待ち行列が先に伸びる。GitHub が「repository-wide lock」と呼んでいるのは、この詰まり方そのものだ。
そこで変えたのが、quorum の範囲だ。これまではリポジトリ全体をまとめて順序付けていたが、個別の参照ごとに切り分ける。Git の整合性は、基本的に参照単位で守れればよい。あるブランチの更新を受け入れたあとに、その参照だけは古い値へ戻らないこと。別の参照まで一緒に並べて待たせる理由はない。ここが、ロックを「捨てた」と見える核心の手前だ。
この切り分けで何が変わるか。たとえばエージェントがそれぞれ別ブランチへ push するなら、同じリポジトリでも互いに足を引っ張りにくくなる。GitHub は内部テストで、異なる参照への書き込みが 3〜4 倍のスループットを低いレイテンシで示したとしている。だが、この数字がなぜ出るのか、その中身はまだ別の層にある。待ち時間を減らすだけでは足りず、保存と読み取りの構造自体を分ける必要があるからだ。
書き込みと読み取りを同じ席に座らせない発想
GitHub の新設計で面白いのは、保存先をただの巨大なディスクに置き換えたのではないことだ。Git オブジェクトの本体は Azure Blob Storage に置かれ、参照とパックの一覧は Ref-CAS という別のサービスに分ける。つまり、データの「中身」と、どのデータを使うかという「目録」が別々に管理される。従来の一体型の運用では、ここが絡み合っていた。
そのうえで、仮想レプリカという考え方が出てくる。ここでのレプリカは、永続状態を抱え込む主役ではない。ローカルディスクは再構築できるキャッシュであり、必要に応じて読み取りや書き込みをさばく使い捨ての計算資源として扱われる。書き込みはまず仮想レプリカで受け、Blob Storage に流し、最後に Ref-CAS へ 1 回のトランザクションを送って確定させる。
この構造が効くのは、書き込みの確定と、読み取りの加速を別々に考えられるようになるからだ。従来は、読み取りを速くしたいときにメモリを増やすと、別のところで必要な計算資源を削ることになりやすかった。GitHub は、読み取り側はより多くのキャッシュを欲しがるが、同じメモリがコンパクションにも要る、と説明している。ひとつの箱に詰め込むと、どちらかが窮屈になる。
もうひとつ重要なのは、読み取りが書き込みと同じレールに乗らなくなることだ。読み取りレプリカは Blob Storage から必要なものを温め、参照の状態を後ろで照合しながら処理する。これなら、普段は大量に来る読み取りを、書き込みの都合で止めなくて済む。GitHub の言い方を借りれば、読み取りは需要に応じて広げられる。この時点で、まだ答え切っていない疑問が残る。なぜこの設計で、あの 7,200 push/秒に近い数字まで見えるのか。
7,200回の背後にある、まだ語られていない部分
数字だけを見ると、5,000 体のエージェントと 7,200 push/秒は同じ話に見える。だが実際には、これは「大きな負荷を投げてみた」話ではなく、どこを分離すればその負荷に耐えられるかの実験結果だ。GitHub は、まず書き込みの同意を参照単位まで小さくし、そのうえで仮想レプリカと Blob Storage を組み合わせた。つまり、最初のボトルネックを外してから、別のボトルネックをどこに置くかを調整している。
この順番が大事だ。たとえば書き込みを細かく分けても、読み取りが同じ計算資源を奪うなら、結局はどこかで渋滞する。逆に、読み取りだけ強化しても、更新の順序づけが大きいままだと待ち行列は消えない。GitHub は「各コンポーネントを独立にスケールさせる」と言うが、それは抽象論ではない。参照の更新、オブジェクトの永続化、キャッシュの温まり方、それぞれの限界が違うからだ。
日本の現場で想像すると、効いてくるのは大規模モノレポだけではない。CI が密に回り、ボットが小さな修正を次々と入れ、レビュー待ちのブランチが何百本も並ぶチームでは、ひとつの push が他の作業を押し止める設計はかなり痛い。特に夜間にエージェントが走る運用だと、人間が触っていない時間帯に負荷が積み上がり、朝の最初の操作で一気に詰まる。そこで何が先に限界を迎えるのかは、見た目以上に重要になる。
ただし、ここまででは「どうやって内部で順序を壊さずに高スループットを出しているのか」という肝心の部分にまだ触れていない。GitHub の元記事は、この先で仮想レプリカと Ref-CAS の役割分担まで書いている。仮想レプリカは何を持ち、何を持たないのか。参照単位に切っても残る限界はどこなのか。次に知りたいのは、まさにその部分だろう。
👉 続き(核心はここから)は PaPoo で読めます: https://papoo.work/doc/e023b1fa1eb92b24
