Gitプロジェクトは最近、Gitバージョン2.45.0をリリースしました。このリリースのハイライトを見てみましょう。これには、GitLabのGitチームと、より広範なGitコミュニティからのコントリビュートによるものがあります。
Reftables: 新しい参照ストレージバックエンド
すべてのGitリポジトリは、次の2つの基本的なデータ構造を追跡する必要があります:
- ファイルのデータ、ディレクトリ構造、コミットメッセージ、タグを保存するオブジェクトグラフ。
- 特定のオブジェクトをよりアクセスしやすい名前に関連付けるためのオブジェクトグラフへのポインタである参照。たとえば、ブランチは名前が
refs/heads/
プレフィックスで始まる参照です。
参照がリポジトリに保存されるディスク上のフォーマットは、Gitの発足以来ほとんど変わっておらず、「files」フォーマットと呼ばれています。参照を作成するたびに、Gitは、パスが参照名と一致するGitリポジトリ内のプレーンなファイルである、いわゆる「ルース参照」を作成します。たとえば、
$ git init .
Initialized empty Git repository in /tmp/repo/.git/
# Updating a reference will cause Git to create a "loose ref". This loose ref is
# a simple file which contains the object ID of the commit.
$ git commit --allow-empty --message "Initial commit"
[main (root-commit) c70f266] Initial commit
$ cat .git/refs/heads/main
c70f26689975782739ef9666af079535b12b5946
# Creating a second reference will end up with a second loose ref.
$ git branch feature
$ cat .git/refs/heads/feature
c70f26689975782739ef9666af079535b12b5946
$ tree .git/refs
.git/refs/
├── heads
│ ├── feature
│ └── main
└── tags
3 directories, 2 files
時々、Gitはそれらの参照を「パックされた」ファイルフォーマットにパックして、参照をより効率的に検索できるようにします。たとえば、
# Packing references will create "packed" references, which are a sorted list of
# references. The loose reference does not exist anymore.
$ git pack-refs --all
$ cat .git/refs/heads/main
cat: .git/refs/heads/main: No such file or directory
$ cat .git/packed-refs
# pack-refs with: peeled fully-peeled sorted
c70f26689975782739ef9666af079535b12b5946 refs/heads/feature
c70f26689975782739ef9666af079535b12b5946 refs/heads/main
このフォーマットはかなりシンプルですが、次のような制限があります。
- 多くの参照を持つ大規模な単一のリポジトリでは、スケーラビリティの問題が発生し始めました。参照を削除することは、特に非効率的です。なぜなら、削除済みの参照を削除するには、「packed - refs」ファイル全体を書き換える必要があるからです。当社の最大のリポジトリでは、参照を削除するごとに数ギガバイトのデータを書き換える可能性があります。
- すべての参照を把握するには複数のファイルを読み取る必要があるため、同時に書き込みを行うことなく参照をアトミックに読み取ることはできません。
- 複数のファイルを作成または更新する必要があるため、アトミックに書き込みを実行することができず、ワンステップで実行できません。
- 完全な「packed - refs」ファイルを書き換える必要があるため、参照のメンテナンスはスケーリングがうまくいきません。
- ルース参照はファイルシステムパスをその名前として使用するため、ファイルシステム固有の動作の対象となります。例えば、大文字と小文字を区別しないファイルシステムは、単に大文字と小文字が異なるだけの参照を保存することはできません。
これらの問題に対処するために、Git v2.45.0は新しい「reftable」バックエンドを導入しました。これは、新しいバイナリフォーマットを使用して参照を保存します。この新しいバックエンドは非常に長い間開発され続けてきました。最初は2017年7月にShawn Pearceによって提案され、JGitに実装されました。これはGerrit プロジェクトで広く使用されています。2021年、Han-Wen NienhuysがライブラリをGitにアップストリームし、reftableフォーマットの読み取りと書き込みを可能にしました。
Git v2.45.0 でアップストリームした新しい「reftable」バックエンドは、ついにreftableライブラリとGitを統合し、新しいフォーマットをGitリポジトリのストレージバックエンドとして使用できるようになりました。
少なくともGit v2.45.0を使用していれば、--ref-format=reftable
スイッチをgit-init(1)
または git-clone(1)
のいずれかに渡すことで、「reftable」フォーマットを使用して新しいリポジトリを作成できます。たとえば、
$ git init --ref-format=reftable .
Initialized empty Git repository in /tmp/repo/.git/
$ git rev-parse --show-ref-format
reftable
$ find -type f .git/reftable/
.git/reftable/0x000000000001-0x000000000001-01b5e47d.ref
.git/reftable/tables.list
$ git commit --allow-empty --message "Initial commit"
$ find -type f .git/reftable/
.git/reftable/0x000000000001-0x000000000001-01b5e47d.ref
.git/reftable/0x000000000002-0x000000000002-87006b81.ref
.git/reftable/tables.list
ご覧のとおり、参照は .git/refs
ディレクトリではなく .git/reftable
に保存されています。参照と参照ログは、.ref
で終わるファイルである「テーブル」に保存されますが、tables.list
ファイルには現在アクティブなすべてのテーブルのリストが含まれています。この仕組みの技術的な詳細については、別のブログ記事でご説明します。引き続きブログをご覧いただくようお願いいたします。
「reftable」バックエンドは、「files」バックエンドに取って代わるものです。したがって、ユーザーの観点からは、すべてが同じように機能する必要があります。
このプロジェクトはPatrick Steinhardtが主導しました。また、Shawn Pearceがフォーマットのオリジナルを発案、Han - Wen Nienhuysがreftableライブラリを作成しています。
参照用ツールの改善
「reftable」フォーマットは、私たちが抱えている多くの問題を解決してくれる一方で、新しい問題も生み出しています。 最も重要な問題の1つが、格納されているデータへのアクセシビリティです。
「files」バックエンドを使用すると、最悪の場合、通常のUnixツールを使用して参照の状態を調べることになります。「packed」参照と「loose」参照の両方には、人間が簡単に理解できるデータが含まれています。これは、バイナリフォーマットである「reftable」フォーマットとは異なります。したがって、Gitは新しい「reftable」フォーマットからデータを抽出するために必要なすべてのツールを提供する必要があります。
すべての参照の一覧表示
最初の問題は、リポジトリからすべての参照を把握するのは基本的に不可能であるということです。 Gitで参照を作成したり変更できるのに、すべての参照を網羅的にリストできないと聞くと、困惑する方もいるかもしれません。
確かに、「files」バックエンドはできません。refs/
プレフィックスで始まるすべての「通常の」参照を簡単にリストすることはできますが、Gitはいわゆるpseudo refs(疑似参照)も使用します。これらのファイルはGitディレクトリのルートに直接存在し、たとえば .git/MERGE_HEAD
などのようなファイルです。問題は、これらの疑似参照が、Gitが格納する、たとえば .git/config
のような他のファイルの隣に存在することです。
一部の疑似参照はよく知られているため識別しやすく、理論上はGitが書き込むことができる参照に制限はありません。「foobar」という参照を作成するのを妨げるものはありません。
たとえば、
$ git update-ref foobar HEAD
$ cat .git/foobar
f32633d4d7da32ccc3827e90ecdc10570927c77d
「files」バックエンドにある問題は、ディレクトリをスキャンして参照を列挙するしかないということです。したがって .git/foobar
が実際には参照であることを理解するために、Gitはファイルを開き、ファイルが参照のようにフォーマットされているかどうかを確認する必要があります。
一方、「reftable」バックエンドは、それに含まれるすべての参照について簡単に認識します。参照はデータ構造にエンコードされているため、それらの参照をデコードして返すだけです。しかし、「files」バックエンドの制限により、存在するすべての参照について学ぶことができるツールは存在しません。
この問題に対処するために、git-for-each-ref(1)
に --include-root-refs
という新しいフラグを追加しました。これにより、参照名の階層のルートに存在するすべての参照もリストアップされます。
たとえば、
$ git for-each-ref --include-root-refs
f32633d4d7da32ccc3827e90ecdc10570927c77d commit HEAD
f32633d4d7da32ccc3827e90ecdc10570927c77d commit MERGE_HEAD
f32633d4d7da32ccc3827e90ecdc10570927c77d commit refs/heads/main
「files」バックエンドの場合、この新しいフラグはベストエフォートで処理され、既知の疑似参照名に一致するすべての参照が含まれます。「reftable」バックエンドの場合、それに関連するすべての参照をすべてリストアップすることができます。
このプロジェクトは、Karthik Nayakが主導しました。
すべての参照ログの一覧表示
ブランチを更新するたびに、Gitはデフォルトでそれらのブランチの更新をいわゆる参照ログで追跡します。この参照ログを使用すると、意図しない変更を行った場合に、そのブランチへの変更をロールバックできるため、非常に役立つツールとなります。
「files」バックエンドでは、これらのログは .git/logs
ディレクトリに保存されます。
$ find -type f .git/logs/
.git/logs/HEAD
.git/logs/refs/heads/main
実際、このディレクトリ内のファイルを一覧表示することが、そもそもどの参照が実際に参照ログを保持しているかを知る唯一の方法です。これは、参照と一緒にそれらのログを保存する「reftable」バックエンドの問題です。そのため、「reftable」フォーマットを使用すると、リポジトリにどの参照ログが存在するかを知る方法はまったくありません。
これは実際には「reftable」フォーマットの欠陥ではありませんが、Gitが提供するツールにおける不備です。この欠落に対処するために、git-reflog(1)
に新しい list
サブコマンドを導入しました。これにより、すべての既存の参照ログをリストアップできます。
$ git reflog list
HEAD
refs/heads/main
このプロジェクトは、Patrick Steinhardtが主導しました。
参照のより効率的なパッキング
Gitリポジトリを効率的に保つには、定期的なメンテナンスが必要です。 通常、このメンテナンスは git maintenance run --auto
を実行してGitリポジトリにデータを書き込むさまざまなGitコマンドによってトリガーされます。このコマンドは、Gitが計算リソースを無駄にしないように、実際に最適化が必要なデータ構造のみを最適化します。
Gitのメンテナンスによって最適化されるデータ構造の1つは、git pack-refs --all
を実行することによって行われる参照データベースです。「files」バックエンドの場合、これは、すべての参照が「packed - refs」ファイルに再パックされ、ルース参照が削除されることを意味します。一方、「reftable」バックエンドの場合、すべてのテーブルが単一のテーブルにマージされます。
「files」バックエンドについては、合理的に改善することはできません。「packed - refs」ファイル全体を書き直す必要があることを考えると、すべてのルース参照をパックしたいと考えるのは理にかなっています。
しかし、「reftable」バックエンドの場合、「reftable」バックエンドが自己最適化されるため、最適ではありません。Gitは、新しいテーブルを「reftable」バックエンドに追加するたびに、必要に応じて自動コンパクションを実行し、テーブルを一緒にマージします。したがって、参照データベースは常に適切に最適化された状態にある必要があり、そのため、すべてのテーブルを一緒にマージすることは無駄な努力となります。
そこでGit v2.45.0では、新しい git pack-refs --auto
モードを導入し、参照バックエンドに必要に応じて最適化を行うようにしました。 「files」バックエンドは、--auto
フラグが設定されていても同じように動作し続けますが、「reftable」バックエンドは、自動コンパクションにすでに使用されているのと同じ経験則を使用します。 実際には、これはほとんどの場合何も操作を行いません。
さらに、git maintenance run --auto
は、デフォルトでこの新しいモードを使用するために、-tauto
フラグを git-pack-refs(1)
に渡すように調整されています。
このプロジェクトはPatrick Steinhardtが主導しました。
補足情報
このブログ記事では、新しい「reftable」バックエンドに重点を置いています。このバックエンドにより、多くの参照を持つ大規模なリポジトリでの拡張性が向上しました。また、このバックエンドをうまく機能させるために関連したツールも導入しました。このGitリリースではさまざまなパフォーマンスの向上、バグ修正、その他の細かい機能がGitコミュニティによって導入されています。Gitプロジェクトの公式リリース発表をご覧いただくと詳細をご確認いただけます。
監修:小松原 つかさ
(GitLab合同会社 ソリューションアーキテクト本部 シニアパートナーソリューションアーキテクト)