PostgreSQLにおけるGit
Gitをデータベースとして使うのではなく、データベースをGitのように使うという発想を提案する記事です。
キーポイント
GitのデータモデルをPostgreSQLに実装する「gitgres」プロジェクトの紹介
Gitリポジトリをデータベース化することで、バージョン管理データとアプリケーションデータの統合クエリが可能に
Forgejo/GiteaなどのGitホスティングプラットフォームのアーキテクチャ改善の可能性を示唆
パッケージマネージャーがGitをデータベースとして使用する際のスケーラビリティ問題への解決策
影響分析・編集コメントを表示
影響分析
この記事は、GitのストレージバックエンドをPostgreSQLに置き換えることで、バージョン管理データとアプリケーションデータの統合的な分析・クエリを可能にする新たなアプローチを提案しています。特にGitホスティングプラットフォーム(Forgejo/Gitea)のアーキテクチャ改善や、パッケージマネージャーのスケーラビリティ問題解決に貢献する可能性があります。
編集コメント
Gitのインフラストラクチャを根本から再考する野心的な試みで、開発ツールのアーキテクチャに新たな可能性を開く内容です。
昨年12月、私はパッケージマネージャーがGitをデータベースとして使用することについて書き、Cargoのインデックス、Homebrewのタップ、Goのモジュールプロキシ、CocoaPodsのSpecsリポジトリがすべて、アクセスパターンがGitリポジトリの設計対象を超えた時点で同じ壁にぶつかることを説明しました。
homebrew-coreはパッケージフォーミュラごとに1つのRubyファイルを持ち、すべてのbrew updateは
Gitリポジトリは、コンテンツアドレッサブルなオブジェクトストアであり、オブジェクトはそのコンテンツのSHA1によってインデックス付けされて格納され、特定のオブジェクトをハッシュで指し示す名前付き参照のセットが加わります。ディスク上のフォーマット(個別ファイルとしてのルーズオブジェクト、デルタ圧縮アーカイブと別個のインデックスを持つパックファイル、ファイルのディレクトリとNFSで破綻するロックプロトコルを持つpacked-refsフラットファイルに分割された参照ストア)は実装の詳細です。リポジトリ間でオブジェクトと参照を同期するプロトコルが実際に重要であり、gitプログラムはその単なる一実装に過ぎないため、クライアントに気づかれることなくストレージバックエンドを交換できます。
データモデル全体は2つのテーブルに収まります:
CREATE TABLE objects ( repo_id integer NOT NULL, oid bytea NOT NULL, type smallint NOT NULL, size integer NOT NULL, content bytea NOT NULL, PRIMARY KEY (repo_id, oid) ); CREATE TABLE refs ( repo_id integer NOT NULL, name text NOT NULL, oid bytea, symbolic text, PRIMARY KEY (repo_id, name) );
オブジェクトのOIDは、Gitが行うのと同じ方法、SHA1("<type> <size>\0<content>") で計算されます。
SELECT FOR UPDATE
これをテストするために、私はgitgresを構築しました。これは約2,000行のCコードで、libgit2のgit_odb_backendとgit_refdb_backendを実装し、git-remote-gitgresというリモートヘルパーを提供します。
objectsテーブルには、Gitがディスクに保存するのと同じバイト列が含まれており、一連のSQL関数がそれらをツリーエントリ、コミットメタデータ、親リンクに解析し、他のテーブルと同様に結合できます。
SELECT r.name AS repo, c.author_name, c.authored_at, i.title AS issue FROM commits c JOIN repositories r ON r.id = c.repo_id JOIN issues i ON i.repo_id = c.repo_id AND c.message ILIKE '%#' || i.index || '%' WHERE c.authored_at > now() - interval '30 days';
このクエリは、GitコミットデータをForgejoの課題トラッカーと結合します。これは現在、git logを通じてコミットを取得し、出力を解析する必要があるものです。
セルフホスト型のForgejoまたはGiteaインスタンスは、実際には2つのシステムを組み合わせたものです:PostgresをバックエンドとするWebアプリケーションと、ファイルシステム上のベアGitリポジトリのコレクションです。Web UIでGitデータを表示する必要があるものはすべて、バイナリをシェルアウトしてテキストを解析する必要があり、それがブラームビューといった単純なものでさえ、クエリを実行するのではなくサブプロセスを生成する必要がある理由です。もしGitデータが他のすべてと同じPostgresインスタンス内に存在すれば、その境界は消えます。
Forgejoは、課題、プルリクエスト、ユーザー、権限、ウェブフック、ブランチ保護ルール、CIステータスをすでにPostgresに保存しており、Gitリポジトリだけがファイルシステム上に残っています。これにより、すべてのデプロイメントで両者のバックアップを調整することを強制され、2つのシステムは異なる方法でスケールし、障害が発生します。コードベースはすでにその負担を示しています:ForgejoはブランチメタデータをGitから独自のデータベーステーブル(models/git/branch.go)にミラーリングし、すべてのGit操作はmodules/gitを通じて行われます。
SELECT content FROM objects WHERE oid = $1
デプロイメントは単一のPostgresインスタンスに集約され、pg_dumpが完全なバックアップを提供します。
Postgresは、フォージが現在カスタムインフラを構築しているようなものに対して、独自のプリミティブを持っています。refsテーブルのトリガーがNOTIFYを発行し、LISTENしているクライアントがウェブフックを送信します。差分、マージ、ブラームは、libgit2がすでにそのサポートを持ち、cgoバインディングを通じてPostgresバックエンドに対して動作するため、SQLで再実装するのではなく、libgit2内に留まります。Forgejoフォークは「modules/gitを置き換える」だけです。
Gitパックファイルはデルタ圧縮を使用し、10MBのファイルが1行変更された場合に差分のみを保存しますが、objectsテーブルは各バージョンを完全に保存します。100回変更されたファイルは、パックファイルでは約50MBであるのに対し、Postgresでは約1GBを占めます。PostgresはTOASTを行い、大きな値を圧縮しますが、それは個々のオブジェクトを単独で圧縮するものであり、パックファイルが行うようなバージョン間でのデルタ圧縮ではないため、ストレージオーバーヘッドは現実のものです。Postgres内で定期的にオブジェクトを再パックするデルタ圧縮層、またはLFSが行うように大きなブロブをS3にオフロードすることは、自然な次のステップです。ほとんどのリポジトリでは、中央値のリポジトリは小さく、ディスクは安価であるため、依然として問題にはならず、GitHubのSpokesシステムは何年も前に同様のトレードオフを行い、冗長性と運用の単純さがストレージ効率を上回るため、すべてのリポジトリの完全な非圧縮コピーを3つデータセンター全体に保存していました。
gitgresは現在、巧妙なハックですが、オープンソースホスティングがForgeFed、Forgejoのフェデレーション作業、そしてより多くの人々がコミュニティのために小さなインスタンスを実行するようになることで、フェデレーションと分散化に向かって進み続けるならば、単一Postgresデプロイメントの運用上の単純さは、生のストレージ効率よりも重要です。少数の大規模なフォージから多数の小規模なフォージへ移行することは、おそらくdocker compose upで立ち上げられるフォージにかかっています。
原文を表示
In December I wrote about package managers using git as a database, and how Cargo’s index, Homebrew’s taps, Go’s module proxy, and CocoaPods’ Specs repo all hit the same wall once their access patterns outgrew what a git repo is designed for.
homebrew-core has one Ruby file per package formula, and every brew update
A git repository is a content-addressable object store where objects go in indexed by the SHA1 of their content, plus a set of named references pointing at specific objects by hash. The on-disk format (loose objects as individual files, packfiles as delta-compressed archives with a separate index, a ref store split between a directory of files and a packed-refs flat file with a locking protocol that breaks on NFS) is an implementation detail. The protocol for synchronising objects and refs between repositories is what actually matters, and since git-the-program is just one implementation of it, you can swap the storage backend without clients noticing.
The whole data model fits in two tables:
CREATE TABLE objects ( repo_id integer NOT NULL, oid bytea NOT NULL, type smallint NOT NULL, size integer NOT NULL, content bytea NOT NULL, PRIMARY KEY (repo_id, oid) ); CREATE TABLE refs ( repo_id integer NOT NULL, name text NOT NULL, oid bytea, symbolic text, PRIMARY KEY (repo_id, name) );
An object’s OID is computed the same way git does it, SHA1("<type> <size>\0<content>")
SELECT FOR UPDATE
To test this I built gitgres, about 2,000 lines of C implementing the libgit2 git_odb_backend
git_refdb_backend
git-remote-gitgres
The objects table contains the same bytes git would store on disk, and a set of SQL functions parse them into tree entries, commit metadata, and parent links that you can join against like any other table.
SELECT r.name AS repo, c.author_name, c.authored_at, i.title AS issue FROM commits c JOIN repositories r ON r.id = c.repo_id JOIN issues i ON i.repo_id = c.repo_id AND c.message ILIKE '%#' || i.index || '%' WHERE c.authored_at > now() - interval '30 days';
That query joins git commit data against Forgejo’s issue tracker, something that currently requires fetching commits through git log
A self-hosted Forgejo or Gitea instance is really two systems bolted together: a web application backed by Postgres, and a collection of bare git repositories on the filesystem. Anything that needs to show git data in the web UI has to shell out to the binary and parse text, which is why something as straightforward as a blame view requires spawning a subprocess rather than running a query. If the git data lived in the same Postgres instance as everything else, that boundary disappears.
Forgejo stores issues, pull requests, users, permissions, webhooks, branch protection rules, and CI status in Postgres already, and git repositories are the one thing left on the filesystem, forcing every deployment to coordinate backups between them, and the two systems scale and fail in different ways. The codebase already shows the strain: Forgejo mirrors branch metadata from git into its own database tables (models/git/branch.go
All git interaction goes through modules/git
SELECT content FROM objects WHERE oid = $1
The deployment collapses to a single Postgres instance where pg_dump
Postgres has its own primitives for things that forges currently build custom infrastructure around. A trigger on the refs table firing NOTIFY
Diff, merge, blame
Content-level diffs, three-way merge, and blame stay in libgit2 rather than being reimplemented in SQL, since libgit2 already has that support and works against the Postgres backends through cgo bindings. The Forgejo fork would be “replace modules/git
Git packfiles use delta compression, storing only the diff when a 10MB file changes by one line, while the objects table stores each version in full. A file modified 100 times takes about 1GB in Postgres versus maybe 50MB in a packfile. Postgres does TOAST and compress large values, but that’s compressing individual objects in isolation, not delta-compressing across versions the way packfiles do, so the storage overhead is real. A delta-compression layer that periodically repacks objects within Postgres, or offloads large blobs to S3 the way LFS does, is a natural next step. For most repositories it still won’t matter since the median repo is small and disk is cheap, and GitHub’s Spokes system made a similar trade-off years ago, storing three full uncompressed copies of every repository across data centres because redundancy and operational simplicity beat storage efficiency even at hundreds of exabytes.
gitgres is a neat hack right now, but if open source hosting keeps moving toward federation and decentralization, with ForgeFed, Forgejo’s federation work, and more people running small instances for their communities, the operational simplicity of a single-Postgres deployment matters more than raw storage efficiency. Getting from a handful of large forges to a lot of small ones probably depends on a forge you can stand up with docker compose up
関連記事
今日のまとめ
AI日報で今日の重要ニュースをまとめ読み