Git の大規模運用の課題と Linus Torvalds の設計思想について
本文の状態
日本語全文を表示中
詳細モードで約11分の本文を読めます。
同じ出来事の情報源
この情報源を基点に整理
Cursor Research
Cursor Research は、Git の分散型設計がスケール時のホスティングを困難にしている現状を指摘し、サーバー側でパックファイルの扱い方を独自に最適化する技術的解決策の可能性について論じている。
AI深層分析を開く2026年8月19日 02:13
AI深層分析
キーポイント
Git の分散型設計と実態の乖離
Linus Torvalds が Linux Kernel のような非中央集権的なプロジェクトのために設計した Git だが、現在の多くの企業やオープンソースプロジェクトは実際には集中型のホスティングに依存しており、分散機能はむしろ障害となっている。
スケール時の技術的課題
Git サーバーの構築が困難な理由は、リポジトリのすべてのインスタンスが同一であるという設計上の特徴と、コードやメタデータを圧縮して保存するパックファイル形式がサーバー規模での管理に不向きである点にある。
サーバー側の独自最適化の可能性
Git クライアントの仕様を変更する必要はなく、ネットワーク上でパックファイルを送受信するという要件さえ満たせば、サーバー内部ではパックファイルの扱い方を自由に設計し直すことでスケーラビリティを向上させる余地がある。
Git の設計上の制約
Git はコンテンツアドレス可能だが、リポジトリは有向非巡回グラフ(DAG)として構成されており、単純なキーバリューストアへの直接マッピングでは機能しない。
分散ストレージの課題
オブジェクトを SHA で参照できるが、リポジトリ操作には DAG を順次たどる必要があるため、ネットワーク経由での個別取得は非効率である。
重要な引用
Hosting Git repositories at scale is a nightmare.
The challenge in hosting Git repositories at scale is inherent in the design of Git itself
Nothing ties you to using packfiles — Linus is not going to come over and check.
Git is a content-addressable data store. All objects in a Git repository (blobs, trees, commits, etc) are keyed by the SHA-1 of their contents.
Source Article
元記事を日本語で読む
本文に関係しない購読案内、埋め込み通知、サイト内プロモーションは除いています。
Git リポジトリを大規模にホストするのは、悪夢のような作業です。リナス・トーバルズが「地獄の情報管理システム」の最初のバージョンを設計した際(これは Git のキャッチフレーズそのものです)、彼には明確な利用ケースがありました。それは彼自身の利用ケースです。彼は Linux カーネルの開発に使われていた分散型バージョン管理システムである BitKeeper を置き換えたかったのです。もちろん、その代替手段も分散型でなければなりませんでした。Linux カーネルは特殊なソフトウェアプロジェクトであり、非常に非中央集権的です。多数のサブシステムごとに異なるメンテナーがおり、分散型バージョン管理システムのワークフローには自然に適合します。
20 年後、Git は業界標準となりましたが、その分散型の性質はむしろ障害となり、利点よりも害の方が大きいのが実情です。平均的なオープンソースプロジェクトや企業では、非中央集権的なワークフローは採用されていません。彼らは分散モデルの多くの利点(オフラインでの作業やプッシュの遅延など)を活用しつつも、実際には集中型のホストに強く依存しています。そして驚くべきことに、Git リポジトリをホストすることは非常に困難な作業であることがわかっています。
Git の何が難しいのか?
Git リポジトリを大規模にホストする際の課題は、Git 自体の設計に内在しています。分散型バージョン管理システムであるため、リポジトリのすべてのインスタンスは同一になります。開発者のラップトップにあるリポジトリと Git サーバー上のリポジトリで、本質的な違いはありません。
一見すると、Git リポジトリのホストは単純な作業のように思えるかもしれません。ディスク上のリポジトリのコピーの前に HTTP デーモンを配置すれば、すぐに Git サーバーが稼働するからです。しかし実際には、スケーラビリティや信頼性に関する多くの困難な課題が存在し、その逆の結果をもたらしています。
通常の Git リポジトリでは、コードとメタデータ(ファイル、コミット、ツリー)は圧縮され、*packfiles* に格納されます。これはローカルマシンで扱うには便利な単純なバイナリシリアライゼーション形式ですが、サーバー上で大規模に管理するには理想的ではありません。packfiles は Git のストレージとネットワークの両方の基本構成要素です。リポジトリからデータをプッシュまたはフェッチする際、データは packfile として転送されます。
これは設計上 Git が動作する仕組みですが、本来これが必要不可欠であるとは限りません。Git クライアントを制御することはできません(ユーザーに不快感を与え、多くの摩擦を生じさせない限り)。しかし、サーバー内部の領域であれば、何でも行うことができます。packfiles に縛られる必要はありません。Linus 氏がやってきてチェックすることはないのです。唯一の制約は、すべての Git オペレーションにおいてネットワーク上で packfile を受け取り送信する必要がある点だけです。
長年にわたり、大規模な Git リポジトリのホスティングを試みた企業は、この「packfile」ベースの設計が可用性と拡張性の両面で大きな制限となっていることに気づいてきました。Packfile は Git がアクセスするためにファイルシステム上に存在しなければならない大規模なバイナリファイルです。ディスク上のリポジトリの前に HTTP サーバーを配置するという単純なアプローチには、非常に低い天井しかありません。
理想的には、リポジトリを多数のディスクや複数のマシンに分散させたいものです(これにより、Git 操作を並列実行でき、サーバーがクラッシュしてもリポジトリの利用継続が可能になります)。しかし、それをどう実現すればよいのでしょうか?
これを実現するには、おおまかに言って三つのアプローチがあります。複雑さの低い順から、ファイルシステムの分散、packfile の分散、そして Git そのものの分散です。
Packfile を使わない Git
Git はコンテンツアドレス型のデータストアです。リポジトリ内のすべてのオブジェクト(ブロッブ、ツリー、コミットなど)は、その内容の SHA-1 によってキー付けされています。これは直感的に分散型キーバリューストアと非常に相性がよく、リポジトリのストレージを拡張するためのクリーンな方法を提供できるはずです。しかし、実際にはこれが機能しません。
問題はここにあります:Git リポジトリの実態は有向非巡回グラフ(DAG)です。SHA を使って任意のオブジェクトを検索することはできますが、リポジトリ内で最も単純な操作を実行する際にも、実際に DAG を一歩ずつたどる必要があります。
Git はあらゆる規模に対応
リポジトリ内の最近の変更をリストアップするなどの操作を行うには、まずコミットを処理する必要があります。コミットを処理すると、そのツリーのルートへのポインタが得られます。このツリーからは、各ファイルやサブツリーへのポインタを取得できます。また、元のコミットからは、その親(履歴上で直前のもの)へのポインタも得られます。
しかし、この探索のあらゆる段階で、次のポインタの値は直前のポインタを取得するまで分かりません。もし各取得操作が分散ストレージへの往復通信を必要とするなら、処理コストはあっという間に膨れ上がってしまいます。
オブジェクト単位で Git を分散させるというアプローチは過去にも何度も試みられてきましたが、スケールする場面ではしばしば失敗してきました。最も有望だったのは、私が以前メンターとしてお世話になった Shawn Pearce 氏が Google のバージョン管理システムチーム在籍中に挑戦したものです。彼の手法は 分散ハッシュテーブルにオブジェクトを格納 するというものでした。
これは、Java で実装されたカスタム Git である JGit があってこそ実現可能なものでした。古き良き Java ライブラリらしく、JGit は通常の Git リポジトリのすべての詳細を抽象化するための十分なインターフェースとファクトリ、そしてインターフェースファクトリを提供します。これにより、サーバー上のデータ保存方法にかかわらず、ネットワーク上で *packfiles* を送信する必要があるという Git プロトコルの制約にもかかわらず、ディスク上の packfiles を DHT に置き換えることが可能になりました。
このシステムは実際に動作し、通常の Git 操作には十分な結果を示しましたが、Git プロトコル自体の限界(データ保存方法に関わらずネットワーク上で *packfiles* の送信が必要である点)により、git clone のパフォーマンスが著しく低下しました。その結果、この設計案は断念されました。
GitHub とファイルシステム
Git が Linux カーネルの閉じた世界から抜け出し始めてから数年後、サンフランシスコで意欲的なスタートアップが誕生しました。2008 年に設立された GitHub は、「Git リポジトリのホスティング:もはや痛くない」という先見性のあるキャッチフレーズを掲げたソーシャルコーディングプラットフォームでした。冗談ではありません。実際に 確認できます。
当時、すでに広く共通認識として持たれていたのは、Git の分散型設計が(あるいはそのおかげで)ユーザーフレンドリーにするためには中央集権的なホスティングが必要であり、それを実現するのが非常に困難だったという点です。GitHub はこの状況を打破しようとしました。
同社のプラットフォームは当初、そして現在も主に Rails モノリリスとして構築されています。最初のバージョンでは、単一の(ただし高性能な)マシン上で Ruby サーバーが動作し、ディスク上にはリポジトリのコピーが隣接して配置されていました。Rails アプリのスケールアウト自体は簡単です。インスタンスを複製して展開すればよいからです。しかし、このケースでは Git が絡むため、ここで解決しようとしている繰り返される課題に直面しました。つまり、「Rails アプリがディスク上の Git リポジトリにアクセスする必要がある場合、リポジトリのコピーをどのように展開するか?」という問いです。
コスト意識が高く、少し変わり者の集まりだった初期のシステムエンジニアたちは、スケーリング問題を解決するために最も単純なアプローチを試みました。彼らの考えは、「Git 自体やパックファイルではなく、ファイルシステムそのものを分散させることに注力すれば、Rails アプリケーションを変更せずに済み、増え続けるユーザー向けに新機能をリリースすることに時間を割ける」というものでした。非常に現実的な発想でした。しかし、この方法は失敗しました。
チームは Git データの分散ファイルシステムを実現するために多くのアプローチを試しましたが、最も obvious な方法である「すべてのリポジトリを中央サーバー上の NFS に格納する」方式はすぐに却下されました。Git のデフォルト実装では、ローカルファイルシステム上で低速な開発者用ラップトップでもそれなりのパフォーマンスを発揮できるよう、ロックや tearing(破損)、読み取り、同期などのファイルシステムセマンティクスについて多くの前提を置いています。しかし、ネットワーク越しのファイルシステムでこれらの挙動がどうなるかについては考慮されていませんでした。結果として、処理は遅く、バグも多発しました。
さらに、ブロックレベルでファイルシステムを複製する技術(振り返れば、まさに恐ろしいもの)を用いた試みも行われました。GFS の短期間のデプロイや、DRBD を基盤としたより長期にわたるデプロイなどです。しかし、いずれも壁にぶち当たりました。日常運用においてこれらは極めて扱いにくく、パフォーマンスの良さでその欠点を補うことはできませんでした。根本的な原因は、ディスク上の *packfiles* の設計にあります。
Git のグラフのようなデータ構造が、往復処理を極めて高価なものにしていることはすでに確認しました。残念ながら、この原則はディスク上の基盤データにも同様に当てはまります。
DAG 内のオブジェクトの配置と、それらが *packfile* に格納される方法との間に相関関係はありません。*packfile* を生成する際の主要なヒューリスティックはサイズ最小化です。オブジェクトはパック内をランダムに配置され、圧縮されますが、最も重要なのは、それらがほとんどそのまま保存されない点です。ほとんどのオブジェクトは、同じ *packfile* 内の別のオブジェクトに対する差分(delta)として格納されています。
グラフデータ構造内で多くの論理的な遷移を追跡した後に個々のオブジェクトを読み出す際にも、ディスク上のフォーマットにおける物理的な遷移を追跡する必要があります。
原文を表示
Hosting Git repositories at scale is a nightmare. When Linus Torvalds designed the first version of *the information manager from hell* (that's actually the tagline for Git, look it up), he had a very specific use case in mind: his own. He wanted to replace BitKeeper, the distributed version control system that was being used to develop the Linux Kernel. Of course, the replacement had to be distributed too. The Kernel is an unusual software project; it is extremely decentralized, with many different maintainers for its many different subsystems. A distributed version control system is a natural fit for this workflow.
Twenty years later, Git has become an industry standard, but the truth is that its distributed nature is more of a hindrance than an advantage. The average open-source software project doesn't operate with a decentralized workflow. The average company definitely doesn't. They use the many advantages of the distributed model (such as being able to work offline, delay pushes, etc) but they very much rely on a centralized host. And hosting a Git repository, it turns out, is an incredibly hard thing to do.
What's hard about Git?
The challenge in hosting Git repositories at scale is inherent in the design of Git itself: a *distributed* version control system means that all instances of a repository are identical. There's nothing special about the repository on a Git server that doesn't apply to a repository on a developer's laptop. Although at first it may appear that this makes hosting Git repositories straightforward (simply put an HTTP daemon in front of an on-disk copy of a repository and you've got a Git server going!), there are many hard scalability and reliability challenges that make this quite the opposite.
In a normal Git repository, your code and metadata (files, commits, trees) are compressed and stored in *packfiles* — a simple binary serialization format which is convenient to deal with on a local machine, but not ideal to manage at scale on a server. Packfiles are the fundamental building block of Git storage *and* Git networking. When you push or fetch data from a repository, it's transferred as a packfile.
This is how Git works by design, but it would be fair to think that it needn't be that way. After all, you do not control the Git client (at least not without annoying your users and adding a lot of friction), but within the walls of your own server, you can do *anything* you want. Nothing ties you to using packfiles — Linus is not going to come over and check. The only restriction is that you do need to receive and send *packfiles* over the network for all Git operations.
Over the years, companies that tried hosting Git repositories at scale noticed that this *packfile*-based design was a major limitation on both availability and scalability. Packfiles are large binary files that must exist on a filesystem for Git to access them. The simple approach of having an HTTP server in front of a repository on disk has a very low ceiling. Ideally you'd want the repository to exist on many disks and many machines (this lets you run many Git operations in parallel, and keeps your repository available when a server crashes). But how do you do that?
There are broadly three possible approaches to accomplish this, in increasing order of complexity: distribute the filesystem, distribute the packfiles, or distribute Git itself.
Git without packfiles
Git is a content-addressable data store. All objects in a Git repository (blobs, trees, commits, etc) are keyed by the SHA-1 of their contents. This is something that intuitively maps very well to a distributed key-value store (the key is the SHA-1; the value is the actual object), and could provide a clean way to scale out the storage of a repository. But this actually doesn't work.
Here's the issue: the actual layout of a Git repository is a directed acyclic graph (DAG for short). You can look up any object via its SHA, but to perform even the most trivial operation in the repo, you must actually walk the DAG step by step.
COMMIT DAGTREE /main → c8f3?commit · c8f3NETWORK↓ OLDER COMMITSobjects 0/54 · round-trips 0key/value storeaa42a112c8f3e816f0214b70e8c487ab19b4d5c277b22dc83f7dc43040c22d6e21aa729a0db50f627cf191fe3e81b19092d080a5f31134e06c811f6e7a196e42e147b9086a70ce192ee45d83f7a9b02ec2a70c49b8e25ac074b1a93d47dd9b519d2a8c14d431dc31ef09e205
If you want to do an operation like listing the recent changes in a repository, you must process its commits. When you process a commit, you get a pointer to the root of its tree. From that tree, you get pointers to each file and each subtree. From the original commit, you get a pointer to its parent (the one that comes before it in the history). Crucially, at every step of this walk, you don't know the value of the next pointer until you fetch the previous one. If every fetch requires a round trip to a distributed store, things become very expensive very fast.
This approach to distributing Git at the object level has been tried before, many times, and it often fails at scale. The most promising implementation was attempted by my former mentor Shawn Pearce when he was working on the version control systems team at Google. His approach was storing the objects in a distributed hash table. This was only possible thanks to JGit, a custom Git implementation in Java. Like any good ol' Java library, JGit provides enough interfaces and factories and interface factories to abstract all the details of a normal Git repository, including replacing its on-disk packfiles with a DHT. Although the system worked and results were good enough for normal Git operations, the limitations of the Git protocol (which again, require *packfiles* to be sent over the network regardless of how you store data on the server) made the git clone performance bad enough to discard the design altogether.
GitHub and filesystems
A couple years after Git started to escape its Linux Kernel bubble, a scrappy startup was born in San Francisco. GitHub was founded in 2008 as a social coding platform with a very prescient tagline, "Git repository hosting: no longer a pain in the ass." I'm not joking here either, look it up. There was, all the way back in 2008, a broad consensus that despite (or perhaps because of) Git's distributed design, you actually needed a centralized way to host Git repositories to make them user-friendly, and doing this was very painful. GitHub was set on changing that.
Its platform started as (and mostly still is) a Rails monolith. The very first versions were running off a single, albeit beefy, machine, with a Ruby server and copies of the repositories on disk next to it. Scaling a Rails app is easy: deploy more instances of it. But in this particular case, since Git is involved, they quickly ran into the recurring question we're trying to solve here: If the Rails app needs to access the Git repositories on disk, how do you deploy more copies of them?
Being a thrifty bunch of misfits, the early systems engineers at GitHub tried the simplest approach that could possibly fix their scaling problems. The thinking was that, if they focused on distributing the *filesystem* (instead of packfiles, or Git itself), they could keep the Rails app unchanged and spend their time shipping more features for the ever-growing user base, instead of doing weird stuff with Git. Very pragmatic. It didn't work.
The team attempted many approaches to a distributed filesystem for Git data: the most obvious one, using NFS to store all repositories on a centralized server, was quickly discarded. The default implementation of Git makes a lot of assumptions about filesystem semantics (locking, tearing, reading, syncing...) that ensure decent performance on the local filesystem of a slow developer laptop, but pay no attention to how they behave over a networked filesystem. It was slow, and it was buggy.
Further attempts were made with (frankly, in retrospect, horrific) technologies that replicated the filesystem at the block level. A short-lived deployment with GFS. A longer-lived deployment based on DRBD. They all hit a wall. They were *terrible* to operate day to day, and they didn't make up for it with good performance. It all boils down to the design of *packfiles* on disk.
We've already seen how Git's graph-like data structures make round-trips prohibitively expensive. Unfortunately, a very similar principle also applies to the underlying data on-disk. There is no correlation between the layout of objects in the DAG and the way they're placed in a *packfile*. The key heuristic used when generating *packfiles* is minimizing their size; objects are placed randomly throughout the pack, they are compressed, and crucially they're rarely stored whole. Most objects are stored as a delta on top of another object in the same packfile. Reading an individual object, after following the many logical hops in the graph data structure, also involves following physical hops in the on-disk format.
COMMIT DAGTREE /HEAD·mergecommit · c8f3root /tree · f021server.tsblob · f7a9pack.tsblob · a112README.mdblob · c430Cargo.tomlblob · ef09main~1commit · 9d2aroot /tree · e8c4server.tsblob · d431pack.tsblob · b8e2README.mdblob · 21aaCargo.tomlblob · 92d0featurecommit · 74b1root /tree · 7a19
今日のまとめ
AIデイリーブリーフで今日の重要ニュースをまとめ読み