データベースパーティショニング

GitLab のデータベースへの PostgreSQL テーブルパーティショニングの導入

これは GitLab にテーブルパーティショニングをどのように導入するかを議論するための作業ドキュメントです。

動機

2020年初頭の時点で、GitLab.com で稼働している PostgreSQL データベースは合計サイズが 5 TB を超えています。しかし、データベース全体のサイズはパーティショニングを導入する動機ではなく、個々のテーブルのサイズこそが問題です。

gitlab-com-table-sizes

100 GB を超える個々のテーブルがあり、テラバイト範囲に達するものさえあることがわかります。

この議論で無視できる注目すべきテーブルが1つあります。merge_request_diff_files は最大のテーブルで、現在データベース全体サイズの 30% 以上を占めています。これはこのデータを外部化してクラウドストレージに移動することで対処されています。

ここに示されている他のテーブルは、GitLab で最も使用されている機能のデータです。

  • CI: ビルドジョブ、トレースセクション、アーティファクト、パイプライン
  • マージリクエスト: 説明、コミット、ノート
  • Issue: 説明とノート
  • Web フック
  • イベント、監査イベント、通知

テーブルサイズの増大

テーブルサイズはクエリパフォーマンスと実行時間に直接影響します。これまで、適切なインデックスを追加することだけでクエリ時間を改善してきました。これは特にセレクティビティが低い(非常に絞り込まれた)クエリの改善に効果的です。

テーブルサイズ増大の影響は次のとおりです。

  1. インデックスのメンテナンスオーバーヘッドが増加する
  2. インデックスルックアップのパフォーマンスが悪化する
  3. データが大きなテーブルに物理的に分散し、不必要に高い I/O を引き起こす
  4. テーブルの肥大化の管理がより困難になる
  5. 関連するワーキングセットがメモリに収まらなくなり、I/O が増加する

例: Issue のグループ検索

gitlab-org グループには GitLab.com 全体の 2300 万件の Issue のうち約 13.5 万件の Issue があります。

グループの Issue に対してテキスト検索を実行し、他のフィルターとともにタイトルや説明に “foobar” を含むすべての Issue を検索できます: このグループでのテキスト検索だけで約 8 秒かかります

内部的には、テキスト検索にトライグラムインデックスを使用しています。簡略化すると、これには2つの方法があります。

  1. テキスト検索を先に行う: グローバルトライグラムインデックスを使用して “foobar” に一致するすべての Issue を検索し、他のフィルターを適用する
  2. 他のフィルターを先に適用する: グループフィルターと権限チェックを先に適用してグループ内の Issue を見つけ、テキストデータに対してオンラインテキスト検索を実行する

どちらのアプローチがより効率的かはデータ分布によって異なり、データが増えるほどどちらのアプローチも遅くなります。

問題 1: 大きなトライグラムインデックス

アプローチ 1 では、最初にグローバルトライグラムインデックスを使用します。クエリのセレクティビティが非常に低い(2,000 件未満のレコードを返す)にもかかわらず、また トライグラムインデックスに適した条件であるにもかかわらず、今日の時点で “foobar” に関連するレコードを見つけるだけで既に 2.8 秒かかります。

トライグラムインデックスはグローバルです: カラムごとに1つの GIN インデックスのみ利用可能です。このインデックス構造は今日の時点で既に 19 GB のサイズです(issues.description の場合)。

問題 2: グループあたりの Issue 数が多い

アプローチ 2 では、まずアクセス可能なグループ内のすべての Issue を見つけます。これには通常通り btree インデックスを使用しますが、全体として 13.5 万件の Issue が得られます。この時点ではトライグラムインデックスを使用できなくなり、代わりにすべてのテキストデータをメモリに読み込んでシーケンシャルなオンライン検索を実行する必要があります。グループに Issue が多ければ多いほど(そしてテキストサイズが大きければ大きいほど)、このアプローチにかかる時間が長くなります。

テーブルパーティショニングによる解決

この例では、テーブルパーティショニングにより issues テーブルを管理可能なチャンク(パーティション)に分解します: トップレベルのネームスペースに対するハッシュパーティショニングを使用してテーブルを 128 個のパーティションに分解しましょう。各パーティションには全 Issue のおよそ 1/128 の Issue レコードが含まれます。この例では gitlab-org というトップレベルのネームスペースは、1つのパーティションにのみ Issue データを持ちます。

これはどのように状況を変えるでしょうか?

  1. 各パーティションには独自のトライグラムインデックスがあり、それらははるかに小さくなります(レコードの約 1/128 のみを含む)。
  2. 検索例の範囲はグループごとです: クエリプランナーはこの情報を使用して、トップレベルのネームスペースに関連する1つのパーティションに直接ナビゲートし、それによって他のすべてをスキャンから除外します。

はるかに小さなトライグラムインデックスにより、テキスト検索をずっと短い時間で実行できます。この特定の問題に対する追加の改善は、複合 GIN インデックスを使用することです。

グループベースの Issue 検索に対するパーティショニングの影響を分析しました(サマリーIssue)。

GitLab データベーススキーマ: パーティショニングキー

これは GitLab のデータベーススキーマの一部の簡略化されたビューです: GitLab.com 上の最大テーブルとその参照の一部。色はテーブルのおおよそのサイズを示しています: 深紅(> 200 GB)、オレンジ(> 100 GB)、黄色(> 60 GB)、青(その他)。詳細なサイズについては、ドキュメントの冒頭の画像を参照してください。

gitlab-model-simplified

(免責事項: 高度に簡略化されており、WIP であり、詳細が不十分な可能性があります)

予想通り、ほとんどのテーブルはプロジェクトに関連しています: ユーザー固有のデータを除いて、プロジェクトはここに示されている大きなテーブルのほとんどによって(直接または間接的に)参照されています。

さらに、プロジェクトは常にネームスペース階層の一部であり、そのためプロジェクトには常に1つのトップレベルのネームスペースが存在します。

そのため、プロジェクトまたはネームスペースをパーティショニングキーとして考えることができます。GitLab のほとんどの機能はプロジェクト内で行われますが、ネームスペースをスコープとした検索の種類があります。上記の例では、トップレベルのネームスペースが検索スコープです。したがって、トップレベルのネームスペースは一般的に良いパーティショニングキーと言えるでしょう。

トップレベルのネームスペースによるパーティショニング

トップレベルのネームスペースでテーブルをパーティション化するには、テーブルにトップレベルのネームスペースへの参照(top_level_namespace_id カラム)が含まれている必要があります。既存のテーブルにはこれがありません。スキーマが正規化されており、トップレベルのネームスペースは namespaces の階層を通じて特定できるためです。

たとえば issues テーブルなどへのトップレベルのネームスペース参照の追加は、非正規化の一形態です。トップレベルのネームスペースでテーブルをパーティション化するためには必要です。

非正規化の欠点は、マスアップデートが必要な状況が生じる可能性があることです。この場合、プロジェクトを別のトップレベルのネームスペースに移動するときに発生します。この場合、プロジェクトのトップレベルのネームスペースへの参照を持つすべてのレコードを更新する必要があります。非正規化なしでは、単一の参照を更新するだけで済みます。

ハッシュパーティショニング戦略

ハッシュベースのパーティショニング戦略(トップレベルのネームスペース別)を使用して、Issue がパーティション間でどのように分布するかを分析しました。これは128個のパーティションのセット全体で Issue の数がどのように分布するかを示しています(64256512 パーティションのグラフも参照)。赤い線は希望する最適な分布(パーティションあたり 1/128)を示しています。

issues_with_128_partitions

実装ロードマップ

このセクションでは、途中で対処しなければならないと特定したいくつかのトピックについて説明します。

高度な PostgreSQL 機能のサポート

パーティショニングを実装するには、データベーススキーマの追跡方法が高度な PostgreSQL 機能をサポートする必要があります。現在使用されている schema.rb は、パーティション、トリガー、および必要となる他の高度な機能の構文をサポートしていません。

つまり、schema.rbstructure.sql に置き換え、Rails の抽象化ではなく SQL でデータベーススキーマを管理するよう移行します。

Issue: schema.rb の代わりに structure.sql を使用する

パーティション化されたテーブルへの外部キーの処理

PostgreSQL 11 はパーティション化されたテーブルを参照する外部キーをサポートしていません。外部キーをテーブルから削除し、外部キーを使用している以下の目的を維持する方法を見つける必要があります。

  1. データベース全体の参照整合性
  2. カスケード削除

カスケード削除をサポートするために、トリガーベースのソリューションが使用可能で同じ機能を提供します。トリガー定義をうまく処理するよう実装する必要があります。

外部キーなしでは、参照整合性の欠陥に対してアプリケーションをより堅牢にする必要があります。さらに、一貫性のないデータを検出し、場合によっては修正する方法も必要です。

Issues:

既存テーブルをパーティション化されたテーブルに移行する戦略

既存の大きなテーブルをパーティション化されたテーブルに移行するには、オンラインマイグレーション戦略が必要です。大量のデータを移行してパーティション化されたテーブル階層にコピーする必要があります。この戦略はアプリケーションに対して透過的でなければなりません。つまり、このためのダウンタイムは許容されません。本番システムへの影響を最小限に抑えながらバックグラウンドで実行される必要があります。

Issue: 既存テーブルのマイグレーション戦略

パーティショニングキーを含むようにスキーマを拡張する

パーティショニングキーの選択によっては、パーティショニングキーを含むよう関連テーブルのスキーマを拡張する必要があります。上記の例では、issues テーブルにトップレベルのネームスペースへの参照を追加します。その結果、プロジェクトが別のトップレベルのネームスペースに転送される際(これはまれなイベントと見なされますが、一度に大量のレコードに影響を与える可能性があります)にこの参照を更新するバックグラウンドジョブを実装する必要があります。

パーティショニングキーの使用を強制する

テーブルパーティショニングの恩恵を受けるには、クエリは常にパーティショニングキーを含む形で作成される必要があります。これはしばしば人工的な構造であり、クエリのセマンティクスを変えません。たとえば、project_id によるフィルターはセマンティクス的には十分かもしれず、パーティショニングキーに対する追加フィルターはそれに対して冗長です。しかし、クエリプランナーが無関係なパーティションを除外できるよう、パーティショニングキーを含めることが重要です。

多くの場合、URL からパーティションキーを既に導出できます: gitlab-org ネームスペース内のリクエストに対して発生する多くのクエリは、おそらくトップレベルのネームスペースをフィルターとして持つべきです。パーティショニングキーを自動的に検出し、関連モデルのクエリに適切なフィルターが含まれるようにするために activerecord-multi-tenant(CitusData 製)の使用を検討するかもしれません。

Issue: クエリにパーティショニングキーを強制するための activerecord-multi-tenant の使用を検討する

結果:

  1. gem を使用した実装例
  2. gem はレコード作成時にパーティショニングキーの設定も処理します
  3. パーティショニングキーの大量更新については、バックグラウンドジョブを実装する必要があるかもしれません(上記参照)
  4. クエリに常にパーティショニングキーを追加していることを確認するためのチェックを追加することを検討するかもしれません。SQL を解析したがあります(ActiveRecord にさらに高いレベルでフックすることも可能かもしれません)。

パーティショニングの種類とパーティションのスキーマ処理

PostgreSQL で利用可能なテーブルパーティショニングにはさまざまな種類があります。

  • RANGE/LIST
  • HASH

RANGE/LIST パーティショニングでは、値の明示的な範囲またはリストに対してパーティションが作成されます。データセットが増大するにつれて、新しいパーティションを自動的に作成する方法を実装する必要があります。

一方、ハッシュベースのパーティショニングでは、完全な空間を含む固定数のパーティションを作成できます。欠点は、より多くの小さなパーティションを作成するためにパーティショニングスキームを洗練することが少し複雑になることと、パーティションを2つの小さな部分に分割することが可能ですが、適切に実装する必要があることです。

ハッシュベースのパーティショニングは、パーティション作成と処理のコンテキストで静的なアプローチです。そのため、すべてのパーティションをあらかじめ作成し、静的なデータベーススキーマの一部として作成できるため、管理が簡単です。


著者: Andreas Brandl