GitLab における Git オブジェクトの重複排除の仕組み

GitLabユーザーがプロジェクトをフォークすると、GitLabは新しいプロジェクトを作成し、フォークした時点のオリジナルプロジェクトのコピーであるGitリポジトリを関連付けます。大きなプロジェクトが頻繁にフォークされると、Gitリポジトリのストレージディスク使用量がすぐに増えてしまいます。この問題に対処するために、私たちはGitLabにフォーク用のGitオブジェクト重複排除機能を追加します。この文書では、GitLabがどのようにGitオブジェクト重複排除を実装するかについて説明します。

リポジトリプール

Git の代替を理解しましょう

Git レベルでは、Git alternates を使うことで重複排除を実現します。Git alternates は、リポジトリが同じマシン上の別のリポジトリからオブジェクトを借りられるようにする仕組みです。

リポジトリ A がリポジトリ B からオブジェクトを借りるには、次のようにします:

  1. B.git/objects に解決するパスを記述することで、特殊ファイルA.git/objects/info/alternates に alternates リンクを確立します。
  2. リポジトリ A で、git repack を実行して、リポジトリ B にも存在するリポジトリ A のすべてのオブジェクトを削除します。

リパック後、リポジトリAは自己完結型ではなくなりますが、独自の参照と設定を含んでいます。この設定が機能するためには、リポジトリBからオブジェクトを削除してはいけません

caution
@pools ディレクトリに格納されているオブジェクトプールリポジトリでgit prune またはgit gc を実行しないでください。これは、オブジェクトプールに依存している通常のリポジトリでデータ損失を引き起こす可能性があります。

危険なのはgit prune 、およびgit gc の呼び出しですgit prune。問題は git prune、プールリポジトリで実行されている場合、オブジェクトが不要になったかどうかを確実に判断できないことです。

GitLabにおけるGitの代替: プールリポジトリ

GitLabは、ユーザーからは見えない特別なプールリポジトリを作成することで、このオブジェクトの借用を整理します。そして、Git alternatesを使ってプロジェクトリポジトリの集合を一つのプールリポジトリから借用できるようにします。このようなプロジェクトリポジトリの集まりをプールと呼びます。プールは、単一のプールから借用するリポジトリの星形のネットワークを形成します。これは、ユーザーがプロジェクトをフォークしたときに形成されるフォークネットワークに似ています(同一ではありません)。

Gitレベルでは、プールリポジトリはGitaly RPCコールを使って作成・管理されます。一般的なリポジトリと同じように、どのプールリポジトリが存在し、どのリポジトリがプールリポジトリから借用するかについての作成者の権限は、RailsアプリケーションレベルのSQLにあります。

結論として、GitLabプロジェクトのリポジトリをGitレベルで効果的に重複排除するには、次の3つが必要です:

  1. プールリポジトリが存在すること。
  2. 参加しているプロジェクトリポジトリは、それぞれのobjects/info/alternates ファイルを介してプールリポジトリにリンクされている必要があります。
  3. プールリポジトリには、参加しているプロジェクトリポジトリと共通の Git オブジェクトデータが含まれていなければなりません。

重複排除係数

GitLabにおけるGitオブジェクトの重複排除の有効性は、プールリポジトリとその各参加者間の重複量に依存します。ソースプロジェクトでガベージコレクションが実行されるたびに、ソースプロジェクトの Git オブジェクトがプールリポジトリにマイグレーションされます。ガベージコレクションが実行されるたびに、他のメンバープロジェクトはプールに追加された新しいオブジェクトの恩恵を受けます。

SQL モデル

GitLab 11.8では、GitLabのプロジェクトリポジトリは独自のSQLテーブルを持っていません。それらはprojects テーブルのカラムによって間接的に識別されます。言い換えると、プロジェクトリポジトリを調べる唯一の方法は、まずそのプロジェクトを調べ、それからproject.repository を呼び出すことです。

プールリポジトリで、私たちは新たなスタートを切りました。これらは、独自のpool_repositories SQL テーブルにあります。これら2つのテーブルの関係は以下のとおりです:

  • aProject は、最大1つのPoolRepository (project.pool_repository) に属します。
  • 上記の自動的な帰結として、PoolRepository は多くのProjectを持ちます。
  • PoolRepository は、厳密に1つの「ソースProject” (pool.source_project) 」を持ちます。

TODO GitLab 11.11 より前に作成されたプールの無効な SQL データを修正https://gitlab.com/gitlab-org/gitaly/-/issues/1653.

前提条件

  • プール内のすべてのリポジトリはハッシュ化されたストレージを使用しなければなりません。これは、object/info/alternates ファイル内のパスの更新を気にする必要がないようにするためです。
  • プール内のすべてのリポジトリは、同じGitalyストレージシャード上になければなりません。Gitの代替メカニズムは、複数のリポジトリ間での直接ディスクアクセスに依存しており、Gitalyストレージシャード内での直接ディスクアクセスしか想定できません。
  • プールからメンバープロジェクトを削除する方法は、(1)プロジェクトを削除する、(2)プロジェクトを別のGitalyストレージシャードに移動する、の2つだけです。

プールとプールメンバーシップの作成

  • プールを作成するには、ソースプロジェクトが必要です。プールリポジトリの初期コンテンツは、ソースプロジェクトリポジトリの Git クローンです。
  • プールを作成する機会は、既存の適格な(非公開、ハッシュストレージ、フォークされていない)GitLabプロジェクトがフォークされ、このプロジェクトがまだプールリポジトリに属していないときです。フォークされた親プロジェクトが新しいプールのソースプロジェクトになり、フォークされた親プロジェクトとフォークされた子プロジェクトの両方が新しいプールのメンバーになります。
  • いったんプロジェクト A がプールのソースプロジェクトになると、将来的に A の適格なフォークはすべてプールのメンバーになります。
  • フォーク元自体がフォークである場合、結果のリポジトリはリポジトリに参加せず、新しいプールリポジトリのシードにもなりません。

    例えば

    フォークAがプールリポジトリの一部だとすると、フォークAから作成されたフォークはフォークAが属するプールリポジトリの一部ではありません

    BがAのフォークで、Aがオブジェクトプールに属していないとします。Cはプールリポジトリの一部ではありません。

TODO フォークのフォークは重複排除されるべきですか?https://gitlab.com/gitlab-org/gitaly/-/issues/1532

結果

  • プールに参加している典型的なプロジェクトが別のGitalyストレージシャードに移動された場合、その “belongs to PoolRepository “関係は壊れます。シャード間でリポジトリを移動する方法が実装されているため、新しいストレージシャードでプロジェクトのリポジトリの新鮮な自己完結コピーを取得します。
  • プールのソースプロジェクトが別のGitalyストレージシャードに移動したり削除されたりしても、”source project “関係は壊れません。しかし、GitLab 12.0では、ソースが同じGitalyシャード上にない限り、プールはソースからフェッチしません。

SQLプールリレーションとGitalyの整合性

Gitalyに関する限り、SQLプール関係は、Gitalyサーバー上の状態について2つのタイプの主張を行います。

プールの存在

GitLabがプールリポジトリが存在する(つまり、SQLに従って存在する)と考えても、Gitalyサーバー上に存在しない場合、Gitalyによってその場で作成されます。

プール関係の存在

ここでうまくいかないことが3つあります。

1.SQLはリポジトリAはプールPに属していると言っていますが、GitalyはAには代替オブジェクトがないと言っています。

この場合、ディスク容量の節約を逃しますが、A自体のすべてのRPCは問題なく機能します。次にAでガベージコレクションが実行されると、Gitalyでオルタネート接続が確立されます。これはGitLab RailsのProjects::GitDeduplicationService

2.SQLはリポジトリAはプールP1に属していると言っていますが、GitalyはプールP2に代替オブジェクトがあると言っています。

この場合、Projects::GitDeduplicationService は例外をスローします。

3.SQLはリポジトリAはどのプールにも属していないと言いますが、GitalyはAはPに属していると言います。

この場合、Projects::GitDeduplicationService 、DisconnectGitAlternates RPCを使ってリポジトリAを「再複製」しようとします。

Git オブジェクトの重複排除と GitLab Geo

プールリポジトリのレコードがGeoプライマリのSQLで作成されると、最終的にGeoセカンダリのイベントがトリガーされます。そしてGeoセカンダリはGitalyにプールリポジトリを作成します。各プール参加者が同期されるにつれて、Geoは最終的にセカンダリのGitalyでガベージコレクションをトリガーし、そのステージでGitオブジェクトが重複排除されるため、これは “最終的に一貫した “状況をもたらします。

To-Do Geoのセカンダリがプールリポジトリを作成しようとした時点で、ソースプロジェクトが存在していなかったというエッジケースをどう扱うか?https://gitlab.com/gitlab-org/gitaly/-/issues/1533