Time-Decay データ
Time-Decay データ
このドキュメントでは、データベーススケーラビリティ ワーキンググループで導入された time-decay パターンについて説明します。 Time-Decay データの特性を議論し、このコンテキストで GitLab 開発者が考慮すべきベストプラクティスを提案します。
一部のデータセットは強い時間減衰効果の影響を受け、最近のデータが古いデータよりもはるかに頻繁にアクセスされます。 Time-Decay のもう一つの側面は、時間の経過とともに一部のデータが重要性を失うことです。つまり、古いデータを少し耐久性や可用性の低いストレージに移動したり、極端な場合には削除したりできます。
これらの効果は通常、プロダクトおよびアプリケーションのセマンティクスに結びついており、古いデータへのアクセス頻度や古いデータがユーザーやアプリケーションにとってどれだけ有用または必要かという程度によって異なります。
まず、データに固有の時間的バイアスを持たないエンティティを考えてみましょう。
ユーザーやプロジェクトのレコードは、いつ作成されたかに関係なく、同様に重要で頻繁にアクセスされる可能性があります。ユーザーの id や created_at を使って、関連するレコードがどれだけ頻繁にアクセスまたは更新されるかを予測することはできません。
一方、強い時間減衰効果を持つデータセットの良い例はログや時系列データです。例えば、ユーザーアクションを記録するイベントのようなものです。 多くの場合、そのような種類のデータは数日または数週間後にはビジネス上の用途がなくなり、データ分析の観点からも急速に重要性が低下します。 これらはアプリケーションの現在の状態にとって急速に関連性の低いスナップショットを表し、ある時点では実質的な価値がなくなります。
2 つの極端な例の間には、保持したい有用な情報を持つデータセットがありますが、作成後の最初の(短い)期間の後は古いレコードへのアクセスがほとんどありません。
Time-Decay データの特性
私たちは以下の特性を示すデータセットに関心があります:
- データセットのサイズ: かなり大きい。
- アクセス方法: 時間に関連するディメンションまたは時間減衰効果を持つカテゴリ的なディメンションによって、データセットにアクセスするクエリの大部分をフィルタリングできる。
- 不変性: 時間減衰のステータスは変わらない。
- 保持: 古いデータを保持するかどうか、またはアプリケーションを通じてユーザーが古いデータにアクセスできるかどうか。
データセットのサイズ
強い時間減衰効果を示すデータセットにはさまざまなサイズがありますが、このブループリントのコンテキストではかなり大きなデータセットを持つエンティティに焦点を当てます。
小さなデータセットはデータベース関連のリソース使用に大きく貢献せず、クエリに対して大きなパフォーマンスペナルティを与えません。
対照的に、~5,000 万件以上のレコードおよび/または 100GB 以上のサイズの大きなデータセットは、実際にはデータの非常に小さなサブセットに常にアクセスすることで、大きなオーバーヘッドを追加します。そのような場合、時間減衰効果を優位に利用して、アクティブにアクセスされるデータセットを削減したいと思います。
アクセス方法
Time-Decay データの 2 番目で最も重要な特性は、ほとんどの場合、日付フィルターを使用して暗黙的または明示的にデータにアクセスできることです。時間に関連するディメンションに基づいて結果を制限します。
このようなディメンションは多くありますが、作成日に焦点を当てます。作成日は最も一般的に使用され、最適化のためにコントロールできるものだからです。 レコードが作成されたときに設定され、不変であり、レコードを移動させることなく物理的にクラスタリングに結びつけることができます。
時間減衰データがデフォルトではアプリケーションによってその方法でアクセスされなくても、クエリの大多数がそのような方法で明示的にデータをフィルタリングする方法があることを付け加えることが重要です。 そのような時間減衰に関連するアクセス方法のない時間減衰データは、スケーリングパターンを設定および追跡する方法がないため、最適化の観点からは役に立ちません。
定義を、時間減衰に関連するアクセス方法を常に使用してアクセスされるデータに限定しているわけではありません。いくつかの外れ値の操作が必要な場合があり、それほどスケールしなくても受け入れられる可能性があります(残りのアクセス方法がスケールできる限り)。 例えば、管理者が特定のタイプのすべての過去のイベントにアクセスする場合、他のすべての操作は最大 1 ヶ月のイベントにアクセスし、過去 6 ヶ月に制限されるようなケースです。
不変性
Time-Decay データの 3 番目の特性は、時間減衰のステータスが変わらないことです。一度「古い」と見なされると、「新しい」や関連するものに戻ることはできません。
この定義は自明に聞こえるかもしれませんが、「古い」データに対する操作をより高コストにする(例: アーカイブしたり、コストの安いストレージに移動したりすることで)、関連するものに戻って重要なアプリケーション操作のパフォーマンスが低下することを心配せずに行えなければなりません。
Time-Decay データアクセスパターンへの逆の例として、更新された日時でイシューを表示するアプリケーションビューを考えてみてください。「更新」の観点からも最新のデータに興味がありますが、その定義は不安定であり、実行可能ではありません。
保持
最後に、Time-Decay データをサブカテゴリにさらに区別する特性は、古いデータを保持するかどうか(例: 保持ポリシー)および/またはアプリケーションを通じてユーザーが古いデータにアクセスできるかどうかです。
(オプション)Time-Decay データの拡張定義
補足として、前述の定義をクラスタリング属性に基づくデータの明確に定義されたサブセットへのアクセスを制限するアクセスパターンに拡張すると、他の多くのタイプのデータに time-decay スケーリングパターンを使用できます。
例えば、未完了としてマークされていない ToDo、マージされていない MR のパイプライン(または同様の時間ベースでない制約)など、アクティブとラベル付けされている間だけアクセスされるデータを考えてみてください。 この場合、減衰を定義するために時間ディメンションを使用する代わりに、カテゴリ的なディメンション(つまり有限の値セットを使用するもの)を使用して関心のあるサブセットを定義します。 そのサブセットがデータセット全体のサイズと比べて小さい限り、同じアプローチを使用できます。
同様に、例えば 6 ヶ月以上前に失敗した CI パイプラインのように、時間ディメンションと追加のステータス属性の両方に基づいてデータを古いと定義することもできます。
Time-Decay データ戦略
パーティショニング
これは、純粋なデータベースの観点から time-decay データに対処するための許容されるベストプラクティスです。 PostgreSQL のテーブルパーティショニングについての詳細はテーブルパーティショニングのドキュメントページで確認できます。
日付間隔(例: 月、年)によるパーティショニングにより、各日付間隔に対してはるかに小さなテーブル(パーティション)を作成し、アプリケーション関連の操作には最新のパーティションのみにアクセスできます。
パーティションキーは、2 つの要因によって異なる可能性のある対象の日付間隔に基づいて設定する必要があります:
データにどれだけ過去まで遡ってアクセスする必要があるか?
1 年前のデータに常にアクセスする場合、週単位のパーティショニングは役に立ちません。毎回 52 個の異なるパーティション(テーブル)にわたってクエリを実行しなければならないためです。その例として、GitLab ユーザーのプロフィールのアクティビティフィードを考えてみてください。
対照的に、作成されたレコードの過去 7 日間だけにアクセスしたい場合、年単位のパーティショニングでは各パーティションに不必要なレコードが多すぎます。
web_hook_logsの場合がこれにあたります。作成されるパーティションはどれくらいの大きさか?
パーティショニングの主な目的は、できる限り小さなテーブルにアクセスすることです。それ自体が大きくなりすぎると、クエリのパフォーマンスが低下し始め、さらに小さなパーティションに再パーティション(分割)しなければならない場合があります。
完璧なパーティショニングスキームは、データセットに対するすべてのクエリをほぼ常に単一のパーティションに対して保つものです。2 つのパーティションにわたる場合や、まれに複数のパーティションにわたる場合は許容できるバランスです。また、パーティションをできるだけ小さく保つことを目標とし、最大で 500 万〜1,000 万レコードおよび/または 10GB 以下にします。
パーティショニングは、古いパーティションを削除(プルーン)したり、データベース内の安価なストレージに移動したり、データベースの外に移動(アーカイブまたは他のタイプのストレージエンジンの使用)するための他の戦略と組み合わせることができます。
古いレコードを保持したくなく、パーティショニングが使用される限り、古いデータのプルーニングは大きなテーブルからデータを削除するのと比較して、実質的にゼロのコスト(あらゆる意味で一定)です。 保持ポリシーの期間外になったとき、そのパーティション内のすべてのデータをドロップするバックグラウンドワーカーが必要なだけです。
例えば、6 ヶ月以内のレコードのみを保持したく、月単位でパーティショニングする場合、常に最新の 7 つのパーティション(現在の月と過去 6 ヶ月)を安全に保持できます。 つまり、各月の始まりに 8 番目に古いパーティションをドロップするワーカーを持つことができます。
PostgreSQL ではテーブルスペースを使用することで、同じデータベース内の安価なストレージへのパーティション移動が比較的簡単です。 各パーティションに個別にテーブルスペースとストレージパラメータを指定できるため、この場合のアプローチは次のとおりです:
- 安価な低速ディスクに新しいテーブルスペースを作成する。
- PostgreSQL オプティマイザーがディスクが遅いことを知るように、その新しいテーブルスペースに高いストレージパラメータを設定する。
- バックグラウンドワーカーを使用して古いパーティションを自動的に低速テーブルスペースに移動する。
最後に、データベースの外へのパーティション移動は、データベースアーカイブまたは手動でパーティションを別のストレージエンジンにエクスポートすることで実現できます(専用サブセクションに詳細があります)。
古いデータのプルーニング
どんな形でも古いデータを保持したくない場合、プルーニング戦略を実装してデータを削除できます。
実装が簡単な戦略で、過去のデータを削除するプルーニングワーカーを使用します。以下でさらに分析する例として、90 日より古い web_hook_logs をプルーニングしています。
大きな非パーティションテーブルに対するこのソリューションの欠点は、関連性がなくなったと見なされるすべてのレコードに手動でアクセスして削除しなければならないことです。 それは Postgres のマルチバージョン同時実行制御により非常に高コストな操作です。 また、新しいレコードの作成レートが閾値を超えるとプルーニングワーカーが追いつけなくなります。これは本ドキュメント作成時点での web_hook_logs の場合に当てはまります。
前述の理由から、強い理由がない限り、データ保持戦略の実装はパーティショニングに基づくべきであるという提案をします。
古いデータをデータベースの外に移動
多くの場合、古いデータは価値があると考えるため、プルーニングは望みません。同時に、データベース関連の操作(例: 直接アクセスやジョイン、その他のクエリの種類)に必要でない場合は、データベースの外に移動できます。
これはアプリケーションを通じてユーザーが直接アクセスできないことを意味しません。データをデータベースの外に移動して、古いデータのケースに限ってメタデータのオフロードと同様に、他のストレージエンジンやアクセスタイプを使用することもできます。
最もシンプルなユースケースでは、最近のデータへの高速かつ直接的なアクセスを提供しながら、古いデータを含むアーカイブをユーザーがダウンロードできるようにします。
これは audit_events のユースケースで評価されているオプションです。国や業界によっては、監査イベントに非常に長い保持期間がある場合があり、GitLab のインターフェースを通じてアクティブにアクセスされるのは過去 1 ヶ月分のデータのみです。
追加のユースケースには、そのタイプのデータを処理するためにより適しているため、データをデータウェアハウスや他のタイプのデータストアにエクスポートすることが含まれます。例えば、テーブルに保存することがある JSON ログ: そのようなデータを BigQuery や Redshift のような列指向ストアにロードすることは、データの分析/クエリに有利な場合があります。
データをデータベースの外に移動するための戦略を検討することがあります:
このタイプのデータをログにストリーミングし、セカンダリストレージオプションに移動するか、他のタイプのデータストアに直接(CSV/JSON データとして)ロードする。
データを CSV にエクスポートし、オブジェクトストレージにアップロードし、このデータをデータベースから削除し、CSV を別のデータストアにロードする ETL プロセスを作成する。
データストアが提供する API を使用してバックグラウンドでデータをロードする。
これは大きなデータセットには実行不可能な解決策かもしれません。ファイルを使用した一括アップロードがオプションである限り、API 呼び出しよりも優れたパフォーマンスを発揮するはずです。
ユースケース
Web Hook ログ
関連エピック: Partitioning: web_hook_logs table
web_hook_logs の重要な特性は以下のとおりです:
データセットのサイズ: 非常に大きなテーブルです。パーティショニングを決定した時点(
2021-03-01)で、~5 億 2,700 万件のレコードと合計サイズ ~1TB がありました。テーブル 行数 合計サイズ テーブルサイズ インデックスサイズ TOAST サイズ web_hook_logs~5 億 2,700 万件 1.02 TiB (10.46%) 713.02 GiB (13.37%) 42.26 GiB (1.10%) 279.01 GiB (38.56%) アクセス方法: 最大で過去 7 日間のログのみをリクエストします。
不変性: 変更されない属性
created_atでパーティショニングできます。保持: 90 日の保持ポリシーが設定されています。
さらに、当時はバックグラウンドワーカー(PruneWebHookLogsWorker)を使用してデータをプルーニングしようとしていましたが、挿入レートに追いつけない状況でした。
その結果、2021 年 3 月の時点では 2020 年 7 月以来まだ削除されていないレコードがあり、テーブルは安定したサイズを維持する代わりに 1 日あたり 200 万件以上のレコードが増加し続けていました。
最後に、2021 年 3 月までに挿入レートは月あたり 170GB 以上のデータに成長し、引き続き増加しているため、古いデータをプルーニングする唯一の実行可能な解決策はパーティショニングでした。
私たちのアプローチは、90 日の保持ポリシーに合わせて月単位でテーブルをパーティショニングすることでした。
必要なプロセスは以下のとおりです:
パーティショニングキーを決定する
この場合、
created_at列を使用することは簡単です: 保持ポリシーが存在する場合の自然なパーティショニングキーであり、競合するアクセスパターンがありませんでした。パーティショニングキーを決定したら、パーティションの作成とバックフィル(既存テーブルからデータをコピー)を開始できます。
既存のテーブルをパーティショニングすることはできないため、新しいパーティションテーブルを作成する必要があります。
そのため、パーティションテーブルとすべての関連パーティションを作成し、すべてをコピーし始め、新しいデータや既存データへの更新/削除が新しいパーティションテーブルにミラーリングされるように同期トリガーを追加する必要があります。
テーブルのパーティショニングを開始する方法の必要な詳細を含む MR
そのプロセスの完了には 15 日 7 時間 6 分かかりました。
次のステップは、バックフィルに使用されたバックグラウンドマイグレーション後の一マイルストーンで、残りのジョブの実行を完了し、失敗したジョブを再試行するなどです。
次に、パーティションテーブルに残りの外部キーとセカンダリインデックスを追加できます。
この操作の目的は、次のマイルストーンで入れ替える前に、元の非パーティションテーブルと同等のスキーマにすることです。
最初に追加しないのは、各挿入にオーバーヘッドが追加され、テーブルの初期バックフィルが遅くなるためです(この場合、5 億件を超えるレコードに対して大幅に遅くなる可能性があります)。そのため、テーブルの軽量なバニラバージョンを作成し、すべてのデータをコピーしてから残りのインデックスと外部キーを追加します。
必要な詳細を含む MR: インデックスを追加する MR(MR-1、MR-2)、外部キーを追加する MR
ベーステーブルをパーティションコピーと入れ替える
これはパーティションテーブルがアプリケーションで積極的に使用され始める時点です。
元のテーブルの削除は破壊的な操作であり、プロセス中に問題がなかったことを確認したいため、古い非パーティションテーブルを保持します。また、パーティションテーブルで発生している操作で非パーティションテーブルも最新の状態に保つよう、逆方向に同期トリガーを切り替えます。これにより、必要な場合にテーブルを元に戻せます。
最後のステップ、入れ替えから 1 マイルストーン後 - 非パーティションテーブルの削除
非パーティションテーブルが削除された後、過去のパーティションをドロップしてプルーニング戦略を実装するワーカーを追加できます。
この場合、ワーカーは常に 4 つのパーティションのみがアクティブであることを確認し(保持ポリシーが 90 日のため)、4 ヶ月より古いパーティションをドロップします。現在の月がまだアクティブな間は 4 ヶ月分のパーティションを保持する必要があります。90 日前に遡ると 4 番目に古いパーティションになるためです。
監査イベント
関連エピック: Partitioning: Design and implement partitioning strategy for Audit Events
audit_events テーブルは前のサブセクションで説明した web_hook_logs テーブルと多くの特性を共有しているため、異なる点に焦点を当てます。
解決策の必要性(1、2)と、パーティショニングがほとんどのパフォーマンス問題を解決できるという合意(1、2)がありました。
他のほとんどの大きなテーブルとは対照的に、主要な競合するアクセスパターンはありません: 月単位のパーティショニングに合わせてアクセスパターンを切り替えることができました。 これは例えば他のテーブルの場合(ネームスペースなどによるパーティショニングアプローチを正当化できる可能性があっても、多くの競合するアクセスパターンがある)とは異なります。
さらに、audit_events は読み取り(クエリ)が非常に少ない書き込みの多いテーブルで、非常にシンプルなスキーマを持ち、データベースの残りの部分とは接続されていません(受信または送信の FK 制約がない)、そして 2 つのインデックスのみが定義されています。
後者は当時、外部キー制約がないことは PostgreSQL 11 を使用している間でもパーティショニングできることを意味していたため重要でした。これは web_hook_logs のユースケースで見られるように、必要なデフォルトとして PostgreSQL 12 に移行した現在はもはや懸念事項ではありません。
audit_events のパーティショニングに必要なマイグレーションとステップは、前のサブセクションの web_hook_logs で説明されたものと同様です。現時点では audit_events に定義された保持戦略はないため、プルーニング戦略は実装されていませんが、将来的にアーカイブソリューションを実装する可能性があります。
audit_events のケースで興味深いのは、パーティション化された最適なクエリを促進するために必要な UI/UX の変更について行った議論です。これは、特定の time-decay に関連するアクセス方法にすべてのアクセスパターンを整合させるためにアプリケーションレベルで必要な変更の出発点として使用できます。
CI テーブル
WIP: CI テーブルのユースケースの要件と分析 - まだ作業中なので、分析が進んだ後に詳細を追加する必要があります。
