データベースのケーススタディ名前空間ストレージ統計

導入

グループのストレージと制限の管理について、グループによって消費されるストレージの量を簡単に表示する方法を容易にし、簡単に管理できるようにしたいと考えています。

提案

  1. ネームスペースの統計を集約形式で保持する新しいActiveRecordモデルを作成します(ルート・ネームスペースのみ)。
  2. このネームスペースに属するプロジェクトが変更されるたびに、このモデルの統計情報を更新します。

問題

GitLabでは、プロジェクトが保存されるたびにコールバックを通してプロジェクトのストレージ統計情報を更新しています。

そして、名前空間ごとの統計情報の要約をNamespaces#with_statistics scope で取得します。このクエリを分析すると、次のことに気づきました:

  • 15k プロジェクトを超えるネームスペースでは、1.2 秒かかります。
  • タイムアウトするため、ChatOpsでは分析できません。

さらに、現在プロジェクト統計を更新するために使用されているパターン(コールバック)は十分にスケールしません。現在、本番環境で最も大きなデータベース・クエリ・トランザクションの1つで、全体として最も時間がかかっています。トランザクションが長くなるため、クエリを1つ追加することはできません。

以上のことから、namespaces テーブルは GitLab.com で最も大きなテーブルの一つであるため、同じパターンを名前空間の統計情報の保存と更新に適用することはできません。そのため、パフォーマンスと代替方法を見つける必要がありました。

試み

試み A:PostgreSQL マテリアライズド・ビュー

プロジェクトルートSQLとマテリアライズドビューに基づいたリフレッシュ戦略によって、モデルを更新することができます:

SELECT split_part("rs".path, '/', 1) as root_path,
        COALESCE(SUM(ps.storage_size), 0) AS storage_size,
        COALESCE(SUM(ps.repository_size), 0) AS repository_size,
        COALESCE(SUM(ps.wiki_size), 0) AS wiki_size,
        COALESCE(SUM(ps.lfs_objects_size), 0) AS lfs_objects_size,
        COALESCE(SUM(ps.build_artifacts_size), 0) AS build_artifacts_size,
        COALESCE(SUM(ps.pipeline_artifacts_size), 0) AS pipeline_artifacts_size,
        COALESCE(SUM(ps.packages_size), 0) AS packages_size,
        COALESCE(SUM(ps.snippets_size), 0) AS snippets_size,
        COALESCE(SUM(ps.uploads_size), 0) AS uploads_size
FROM "projects"
    INNER JOIN routes rs ON rs.source_id = projects.id AND rs.source_type = 'Project'
    INNER JOIN project_statistics ps ON ps.project_id  = projects.id
GROUP BY root_path

でクエリを実行できます:

REFRESH MATERIALIZED VIEW root_namespace_storage_statistics;

これは単一のクエリ更新を意味しますが(そしておそらく高速です)、いくつかの欠点があります:

  • マテリアライズド・ビューの構文は PostgreSQL と MySQL で異なります。この機能に取り組んでいる間、MySQLはまだGitLabによってサポートされていました。
  • Railsはマテリアライズドビューをネイティブサポートしていません。データベースのビューを管理するには専用のgemを使う必要があります。

試みB:CTEによる更新

試行 A と同様:共通テーブル式を使用したリフレッシュ戦略によるモデル更新

WITH refresh AS (
  SELECT split_part("rs".path, '/', 1) as root_path,
        COALESCE(SUM(ps.storage_size), 0) AS storage_size,
        COALESCE(SUM(ps.repository_size), 0) AS repository_size,
        COALESCE(SUM(ps.wiki_size), 0) AS wiki_size,
        COALESCE(SUM(ps.lfs_objects_size), 0) AS lfs_objects_size,
        COALESCE(SUM(ps.build_artifacts_size), 0) AS build_artifacts_size,
        COALESCE(SUM(ps.pipeline_artifacts_size), 0) AS pipeline_artifacts_size,
        COALESCE(SUM(ps.packages_size), 0) AS packages_size,
        COALESCE(SUM(ps.snippets_size), 0) AS snippets_size,
        COALESCE(SUM(ps.uploads_size), 0) AS uploads_size
  FROM "projects"
        INNER JOIN routes rs ON rs.source_id = projects.id AND rs.source_type = 'Project'
        INNER JOIN project_statistics ps ON ps.project_id  = projects.id
  GROUP BY root_path)
UPDATE namespace_storage_statistics
SET storage_size = refresh.storage_size,
    repository_size = refresh.repository_size,
    wiki_size = refresh.wiki_size,
    lfs_objects_size = refresh.lfs_objects_size,
    build_artifacts_size = refresh.build_artifacts_size,
    pipeline_artifacts_size = refresh.pipeline_artifacts_size,
    packages_size  = refresh.packages_size,
    snippets_size  = refresh.snippets_size,
    uploads_size  = refresh.uploads_size
FROM refresh
    INNER JOIN routes rs ON rs.path = refresh.root_path AND rs.source_type = 'Namespace'
WHERE namespace_storage_statistics.namespace_id = rs.source_id

試行Aと同じ利点と欠点。

試行C: モデルを取り除き、Redisに統計情報を保存します。

統計情報を集約して保存するモデルを取り除いて、代わりに Redis Set を使うこともできます。GitLabはすでにアーキテクチャの一部としてRedisを含んでいるので、これは退屈な解決策であり、実装するのが一番早いでしょう。

この方法の欠点は、RedisがPostgreSQLのような永続性/一貫性の保証を提供していないことです。

試み D:ルート名前空間とその子名前空間にタグを付けます。

ルート・ネームスペースとその子ネームスペースを直接関連付けます。したがって、親を持たないネームスペースが作成されるたびに、このネームスペースにルート・ネームスペース ID がタグ付けされます:

IDルートID親ID
11NULL
211
312

名前空間内の統計情報を集約するには、次のように実行します:

SELECT COUNT(...)
FROM projects
WHERE namespace_id IN (
  SELECT id
  FROM namespaces
  WHERE root_id = X
)

この方法は集計をはるかに簡単にしますが、いくつかの大きな欠点があります:

  • 新しい列を追加して埋めることで、すべてのネームスペースをマイグレーションしなければなりません。テーブルのサイズが大きいため、時間/コストへの対処は重要です。バックグラウンドマイグレーションには約153hhttps://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/29772を参照してください。
  • バックグラウンドマイグレーションはリリースの1つ前に出荷する必要があり、マイルストーン分だけ機能が遅れます。

試みE(最終):ネームスペース・ストレージ統計を非同期に更新します。

このアプローチでは、すでにある増分統計更新を引き続き使用しますが、Sidekiqジョブを通じて別のトランザクションで更新します:

  1. idnamespace_idの2つのカラムを持つ2つ目のテーブル(namespace_aggregation_schedules)を作成します。
  2. プロジェクトの統計が変更されるたびに、行をnamespace_aggregation_schedules
    • ルート・ネームスペースに関連する行が既に存在する場合は、新しい行を挿入しません。
    • project_statistics(https://gitlab.com/gitlab-org/gitlab/-/issues/29070) を更新するトランザクションの長さを考慮して、挿入は別のトランザクションでSidekiqジョブを通して行う必要があります。
  3. 行を挿入した後、別のワーカーをスケジュールし、2 つのタイミングで非同期に実行させます:
    • 一つは即座に実行されるように、もう一つは1.5h 時間後に実行されるようにスケジュールします。
    • ジョブをスケジュールするのは、ルート名前空間IDに基づくキーでRedisの1.5h リースを取得できる場合のみです。
    • リースを取得できない場合は、すでに別のアグリゲーションが進行中であるか、1.5h 以下でスケジュールされていることを示します。
  4. このワーカーは
    • サービスを通じてすべてのネームスペースにクエリを発行し、ルートネームスペースストレージの統計情報を更新します。
    • 更新後、関連するnamespace_aggregation_schedules を削除します。
  5. もう1つのSidekiqジョブも含まれており、namespace_aggregation_schedules テーブルの残りの行をトラバースし、保留中の行ごとにジョブをスケジュールします。
    • このジョブは、毎晩実行されるようにcronでスケジュールされます(UTC)。

この実装には次のような利点があります:

  • 更新はすべて非同期で行われるため、project_statistics のトランザクションの長さが長くなることはありません。
  • 更新は1つのSQLクエリで行います。
  • PostgreSQLとMySQLに対応しています。
  • バックグラウンドでのマイグレーションは不要です。

この方法の唯一の欠点は、ネームスペースの統計が変更後1.5 時間まで更新されるため、統計が不正確になる時間帯があることです。これは、統計が不正確になる時間帯があることを意味します。ストレージの制限をまだ実施していないため、これは大きな問題ではありません。

結論

ストレージ統計の非同期更新は、ルート・ネームスペースを集約する方法としては問題が少なく、パフォーマンスも高いものでした。

このユースケースに関するすべての詳細は、以下を参照してください:

名前空間ストレージの統計のパフォーマンスがステージングとプロダクション(GitLab.com)で測定されました。すべての結果はhttps://gitlab.com/gitlab-org/gitlab-foss/-/issues/64092に投稿されました:今のところ問題はレポーターされていません。