Content last updated 2026-09-11

Git のスケーリング:クラスタリングとルーティング

Gitaly におけるクラスタリングとルーティングの設計案

このドキュメントでは、Gitaly におけるクラスタリングとルーティングの MVP 案、および管理者が新しいクラスターをどのように設定することが想定されるかについて説明します。

スコープ

MVP

クラスタリングおよびルーティングレイヤーの初期版では、機能全体の一部を実装します。これは、水平スケーラビリティへの全体的なアプローチを検証し、お客様にすぐに価値を提供することを目的としています。

目標は次のとおりです。

  1. 固定サイズの Gitaly ノードクラスターを、単一のストレージとして Rails に公開できるようにします。
  2. 固定の $k$ 値を設定できるようにします。$k$ の詳細については、ランデブーハッシュを参照してください。
  3. 等しい重みを持つ重み付きランデブーハッシュ方式を使用して、ルーティングレイヤーを通じてキャッシュのスティッキネスを維持します。
  4. プロキシではなくリダイレクトを通じてルーティングします。

今後の作業

MVP をお客様へ展開し、クラスターが実際にどのように動作するかについて理解を深めるにつれて、次の機能強化を検討し始めます。

  1. クラスターの自動スケーリング。これは Kubernetes デプロイに限定される可能性が高いです。
  2. リポジトリごとの $k$ 値の設定。これには、すべてのノードがクエリできるグローバルに一貫したリポジトリメタデータの情報源が必要です。または、gRPC メタデータやヘッダーを通じてクライアントから提供することもできます。
  3. ヘルスまたはリソース使用量のメトリクスを使用して、実行時にノードの重みを調整し、ルーティングに影響を与えます。
  4. キャッシュの事前ウォームアップ。調整可能な $k$ 値では、ノードがリポジトリを初めて提供する可能性が大幅に高まります。事前ウォームアップが不可欠になります。
  5. この Kubernetes ネイティブのルーティング提案で説明されているように、サービスメッシュなどの Kubernetes ネイティブな構成要素を使用してルーティングとクラスタリングを実行します。
  6. クラスターの自動スケーリングと負荷分散に関する、より高度なポリシー。カスタムの HELP プロトコルは、実現可能性を示すよい例です。
  7. ポリシー駆動のルーティング判断。これにより、Gitaly クライアント(Rails など)はビジネス要件を指定してルーティング動作に影響を与えられます。たとえば、ヨーロッパ内の Gitaly ノードが特定のリクエストを処理することや、特定のリポジトリへのリクエストを 3 つのノードに分散することを要件とする場合があります。

クラスタリング

私たちは、容易に弾力的なスケーリングができる、ゴシップネットワークを基盤とした結果整合性のあるクラスターを実装します。これは、既存の Praefect ノードの設定を更新する必要があるためスケールアップが難しい Praefect とは対照的です。また、ゴシップネットワークは Raft のようなコンセンサスネットワークと比べて実装が簡単であり、実行時のクラスター状態を永続ストレージに保存する必要もありません。

memberlist パッケージは、私たちが使用を目指しているゴシップネットワークの実装です。SWIM プロトコルを基盤とし、フラッピングを減らすためのいくつかの改善を加えています。フラッピングとは、ネットワーク内のノードが正常に機能しているにもかかわらず、繰り返し停止と判定されることです。これにより、クラスター構成に多くの不要な変動が生じます。memberlist パッケージでは、ネットワークに参加するノードが最大 512 バイトの任意のメタデータを送信することもできます。メタデータの更新はいつでも送信できます。このサイズ制限があるのは、SWIM が UDP ヘルスプローブにこれらのペイロードを相乗りさせるためです。Memberlist は TCP を通じてノードメタデータの調整も定期的に試みます。

ゴシップネットワークの欠点は次のとおりです。

  1. 大量の状態を伝播できないこと。
  2. 結果整合性モデルであること。
  3. ネットワークパーティションやその他の障害によりスプリットブレインが発生するリスク。

このモデルでは(追加コンポーネントで補強しない限り)保証できなくなるものとして、次が挙げられます。

  1. 特定のリポジトリに対してライターを厳密に 1 つだけに制限すること。
  2. 特定のリポジトリに対する同時操作の書き込み直列化。

つまり、特定のシナリオでは、複数の Gitaly ノードが特定のリポジトリに同時書き込みを行う可能性があります。次の世代オブジェクトの書き込み時に競合が発生する可能性があり、これは別途対処します。

ローカルの Gitaly ディスクは永続ストレージの信頼できる唯一の情報源ではなくなるため、実装、デバッグ、運用がより簡単なアーキテクチャを実現するためのトレードオフとして、これらは許容できます。

CockroachDBApache Cassandraなどの既存のストレージシステムでは、すでにこの形式の結果整合性クラスタリングが使用されています。元の DynamoDB の論文でも、ゴシップネットワークの使用について説明されています。

クラスターへの参加

クラスターは、単一ノードから任意の数のノードまでスケーリングできる必要があります。

クラスターは、gitaly serve による単一ノードの作成から始まります。後続のノードは、既存ノードのアドレスを含む特別なフラグを指定して起動することでクラスターに参加します。例:

# Start the first node, listens on http://gitaly-1.internal
$ gitaly serve

# Start another node and join it to the first
$ gitaly serve --join-addr http://gitaly-1.internal

新しいノードがクラスターに参加すると、いくつかの処理が行われます。

  1. ゴシッププロトコルが既存メンバー全体にイベントを伝播します。新しい参加ノードにもクラスター構成が通知されます。
  2. 各ノードが、拡張されたクラスターを反映するようにローカルの実行時状態を更新します。
  3. 新しい参加ノードのローカルキャッシュが、そのノードが提供することになっているリポジトリに応じて事前にウォームアップされます。

次の制約があっても、クラスターを正しく確立できることが重要です。

  • ノードが同時に起動される。
  • ノードが任意の順序で起動される。
  • --join-addr で指定したノードがまだ起動中のため到達できない。

クラスターからの離脱

ノードは、クラスターがスケールダウンされるため自発的に離脱するか、異常または停止状態になったため離脱します。これらのシナリオは異なる方法で処理されます。

  1. ノードが自発的に離脱する場合、グレースフルシャットダウンを実行し、離脱することをクラスターの残りのノードに伝える必要があります。これにより、ディスパッチ済みのリクエストをそのノードが引き続き処理し、ピアノードが内部状態を更新できる期間が確保されます。
  2. ノードが異常または停止状態になると、ヘルスプローブに応答しないため、ゴシップネットワークはそのノードを「疑わしい」状態としてマークします。疑いのタイムアウトは設定可能であり、前述のフラッピング問題を防ぐように設計されています。ノードが応答しない状態が続くと、障害としてマークされます。

クラスター内のほかのノードがこの変更を認識すると、クラスターの縮小を反映するようにローカル状態を更新します。

クラスターへの接続

Gitaly クライアントは、サービスディスカバリの手段を使用してクラスターに接続する必要があります。たとえば、クラスター内のノードのアドレスを DNS レコードとして公開できます。

これは Praefect クラスターの現在推奨されるアプローチであるため、クライアントで十分にサポートされているはずです。

クラスターの状態

このクラスタリングアプローチの利点は、どのような確定状態も永続ストレージに保存する必要がないことです。クラスターの構成は実行時にのみ定義および維持され、すべての実行時状態は一時的なものと見なされます。

クラスター全体を破棄して再作成しても、データは失われません。

ルーティング

現在のスタンドアロン Gitaly ノード間のルーティングは Rails を通じて行われます。Rails はリポジトリと、そのリポジトリの提供を担当する Gitaly ストレージとの永続的なマッピングを保持しているためです。同じリポジトリは常に同じ Gitaly ノードが提供します。Praefect では「仮想ストレージ」と呼ばれる間接参照レイヤーが導入され、Gitaly ノードのクラスターを単一のストレージとして公開しました。

新しいクラスターでは、ルーティングを内部で処理します。Praefect のような独立したルーティング/調整レイヤーはなく、すべてのノードがリクエストのルーティングと処理という 2 つの責任を負います。クラスター全体を表す単一のストレージが Rails に公開されます。

将来的には、クライアントがルーティング判断をより細かく制御できる、ポリシーベースのルーティングをサポートする可能性があります。たとえば Rails の設定で、repo-a へのリクエストを x 個のレプリカに分散することや、repo-d へのリクエストをヨーロッパから提供することを指定できます。その場合、Gitaly はベストエフォートでこれに従おうとします。

すべてのノードは、特定のリポジトリに対して同じルーティング判断をするよう努めなければなりません。クライアントが repo-a を要求した場合、クライアントが最初にどのノードへ接続したかにかかわらず、最終的には同じノード(またはノードのサブセット)がリクエストを処理する必要があります。特定のリポジトリを提供するノード数は設定可能でなければなりません。

この主な理由は、キャッシュのスティッキネスを維持するためです。リクエストを処理するには、最初にリポジトリデータを S3 からノードのローカルディスクへダウンロードする必要があるため、同じノードが同じリポジトリを担当する方が有利です。

ランデブーハッシュ

上で説明したゴシップネットワークの制約を考えると、リポジトリの大規模な共有テーブルを参照してルーティングを判断することはできません。代わりに、重み付きランデブーハッシュの仕組みを使用し、存在するすべてのリポジトリを事前に把握していなくても、ノードが独立してルーティングを判断できるようにします。このアルゴリズムは「オブジェクト」を入力として受け取り、$1 \leq k \leq N$ 個の「サイト」にハッシュします。

  1. オブジェクトはリポジトリ識別子です。
  2. $k$ はリポジトリを分散するノードの数です。
  3. $N$ はクラスター内のノードの総数です。
  4. サイトは、リクエストを処理する Gitaly ノードのアドレスです。

ランデブーハッシュは、さまざまな非暗号学的ハッシュ方式を使用して実装できる汎用的な仕組みです。クラスター内のすべてのノードで同じシードを使用するようハッシャーを設定すると、特定の入力に対して同じ出力が保証されます。私たちは複数の選択肢を調査し、小規模および大規模クラスターで最も優れたキー分布を示した xxh3 を選択しました。

実行時に、各ノードは概念上の「ハッシュリング」を維持します。ノードがクラスターに参加、またはクラスターから離脱すると、サイトがリングに追加、またはリングから削除されます。

重み付きランデブーハッシュの重み付けにより、各サイトに対応する重みを設定できます。重みの大きいサイトほどルーティング先に選ばれる可能性が高くなります。これにより、将来的に実行時のルーティングポリシーを動的に調整できます。たとえば、リソース競合が発生しているノードへのトラフィックを減らすために役立つ可能性があります。

リダイレクト

クライアントが接続したノードがリクエストを処理すべきノードでない場合、適切なノードへリクエストをルーティングする仕組みが必要です。これを実現する方法は、大きく分けて 2 つあります。

  1. クライアントリダイレクト。ノードが別の接続先をクライアントに伝えます。
  2. プロキシ。ノードが既存のリクエストを別の接続先へ透過的にプロキシします。

従来、Praefect ノードはリクエストを対象の Gitaly ノードへプロキシしていたため、パフォーマンス上の問題の影響を受けやすくなっていました。具体的には、長時間実行されるリクエストが Praefect ノードの貴重なリソースを占有し、状況によっては Praefect ノードがボトルネックになりました。

リダイレクトは、クライアント側で追加処理が必要になる代わりに、これらの問題を解決します。最大の課題は、HTTP と異なり、gRPC がプロトコルレベルでリダイレクトをサポートしていないことです。リダイレクトはアプリケーションで実装する必要があります。つまり、Gitaly はレスポンスにリダイレクトが必要であることを示す情報を含め、クライアントはそれを理解して適切なノードに接続しなければなりません。

したがって、リダイレクトをサポートするには 2 つの変更が必要です。

  1. Gitaly の gRPC レスポンスメッセージを、リダイレクトを指定できるように更新する必要があります。
  2. Gitaly クライアントを、要求された場合に同じリクエストを別の場所へ送信するよう更新する必要があります。

設定

スタンドアロン Gitaly ノードの運用は難しくありません。設定は主に、さまざまな実行時の調整項目、gRPC サービスと Prometheus メトリクスのアドレスバインディング、およびストレージディスクに限られます。Praefect は独立したルーティング/調整レイヤーを持ち、クラスターのサイズが設定内で静的に定義されるため、さらに複雑になります。

新しいクラスターは運用しやすくなければなりません。導入する設定は最小限にし、クラスター自体の構成をファイルで静的に定義すべきではありません。少なくとも次の新しい設定キーが必要です。

  1. 既存の Gitaly デプロイを引き続きサポートする必要があるため、新しいクラスター機能が有効かどうか。
  2. ゴシップネットワークがリッスンするポート。
  3. $k$。特定のリポジトリを提供するノードの数。