マルチデータベースバックグラウンドマイグレーション

複数データベースに対応したバックグラウンドマイグレーションの設計

これは、複数データベースに対するバックグラウンドマイグレーションのサポート設計を仕様化するための作業ドキュメントです。

動機

GitLab では、大量のデータをマイグレーションする際にバックグラウンド処理に大きく依存しています。これはアプリケーションロジックに関連する一般的なデータ修正だけでなく、パーティショニングやスキーマ再設計などの将来的なデータベース目標の基盤としても重要です。

同時に、GitLab をスケールさせるために GitLab アプリケーションデータベースを複数のデータベースに分解しています。この取り組みはアプリケーション全体に影響を与え、望ましい設計を実装するために大規模な変更が必要です。

バックグラウンドマイグレーションは非常に重要なツールであるため、マルチデータベース設計を完全に取り入れるよう適切に再設計する必要があります。

背景

提案された設計について議論する前に、バックグラウンドマイグレーションの仕組みと、複数データベースの使用によって生じる制約について説明します。

バックグラウンドマイグレーションの種類

アプリケーション内には2つのバックグラウンドマイグレーションフレームワークがあります。一般的に、同じマルチデータベース設計原則が両方に適用されるべきです。

  1. Redis ベースのソリューション - GitLab で作業する任意の開発者が使用できる一般的なバックグラウンドマイグレーションです。データベースでのジョブ追跡はオプションですが、主に固定間隔の遅延で処理される Sidekiq ジョブをエンキューすることで機能します。
  2. データベースベースのソリューション - まだ一般的には使用できませんが、Redis ベースのソリューションの代替として設計されています。Sidekiq-cron から実行されますが、すべてのジョブ情報はデータベースに永続化されます。

バックグラウンドマイグレーションのコンポーネント

バックグラウンドマイグレーションは、いくつかの疎結合なコンポーネントとして考えることができます。

  1. エンキューマイグレーションヘルパー - デプロイ後のマイグレーションから呼び出され、後でバックグラウンドジョブを実行するために必要な情報を保存します
  2. ジョブ実行フレームワーク - 以前にエンキューされたデータに基づいて個々のマイグレーションジョブを実行する汎用ワーカープロセス
  3. バックグラウンドマイグレーションジョブ - 特定のデータマイグレーションに対するビジネスロジックを含むジョブ

これらのコンポーネントは論理的に分離されているため、一律の解決策を適用する必要はありません。マイグレーションヘルパーはマイグレーションコンテキスト内で実行されるため、Rails のマイグレーションが動作するのと自然に同じように動作するべきです。バックグラウンドマイグレーションの実行は Sidekiq から行われますが、汎用フレームワークとカスタムクラスにはそれぞれ独自の関心事があります。

複数データベースの懸念事項

マルチデータベース設計では、ビジネスロジックのデータを保存するために使用されるテーブルは単一のデータベースにのみ存在します。同じビジネスデータのセットは常に同じ既知のデータベースから来ます。

バックグラウンドマイグレーションはこのモデルに従いません。バックグラウンドマイグレーションジョブは、場合によっては同時に、複数のデータベースと通信する必要があります。さらに、両方の形式のバックグラウンドマイグレーションにはデータベースレベルのトラッキングテーブルがあるため、このデータをデータベース間でどのように分割するかを検討する必要があります。

ストレージレベルの懸念事項とは別に、実行モデルを検討する必要があります。複数の物理データベースを使用することで、各データベースのデータを独立して処理する機会が得られます。

提案された設計

高レベルでは、マルチデータベースバックグラウンドマイグレーションに対してこの設計を提案します。

  • マイグレーションの意図されたコンテキストに基づいてマイグレーションをエンキューします。特定のコンテキスト(例: “CI”)で実行することを意図したバックグラウンドマイグレーションは、そのコンテキストの Rails マイグレーションを通じてスケジュールされ、トラッキングデータもそのコンテキストに関連するデータベース(例: “CI データベース”)に保存されます。バックグラウンドマイグレーションが両方のデータベースと通信する場合、開発者は最も適切と思われる ci または main のどちらかを選択できます。
  • 各データベースのジョブを処理するために個別のキューを使用します。そのキューのワーカーは、適切なトラッキングデータベースに接続するために自身のコンテキストを知っています。
  • 個々のマイグレーションジョブに最大限の柔軟性を許容します。開発者はデータマイグレーションを実行するためにどのデータベースに接続するかを選択します。

次の図はこのアプローチの高レベルのアーキテクチャを示しています。

multidb-bg-migrations

以下でこれらの各ポイントについて詳しく説明します。

意図されたコンテキストに基づくマイグレーションのエンキュー

例として、分解の取り組みにより、メインデータベースと CI データベースを持つことを意図しています。開発者がメインデータベースのテーブルに対してバックグラウンドマイグレーションを実行する必要がある場合、メインデータベースの Rails マイグレーションを使用してエンキューします。CI データベースのテーブルに対してマイグレーションを実行する必要がある場合、CI の下の Rails マイグレーションによってエンキューされます。

このアプローチにはいくつかの利点があります。

  • マイグレーションは自然に Rails マイグレーションのモデルに従います。Rails マイグレーションがジョブをエンキューする間、クロスデータベース(Rails がサポートしていない)にアクセスする必要はありません。
  • 実行フレームワークは、汎用マイグレーションジョブのトラッキング情報とデータベースコンテキストの両方にアクセスするために SharedModel を一貫して使用できます。特定のデータベースへのハードコードされた接続は使用しません。
  • トラッキングデータをマイグレーションのビジネスデータが存在するデータベースにローカルに保持します。

この実行モデルをサポートするために、開発者にマイグレーションに必要なコンテキストをタグ付けすることを要求できます。簡略化した例は次のようになります。

class BackgroundMigration < GitlabMigration[1.0]
  restrict_gitlab_migration schema: :ci

  def up
    # schedule jobs
  end
end

これにより、エンキューマイグレーションをスキーマを一貫させるためにすべてのデータベースに適用される DDL 変更とは異なり、対象の論理データベースでのみ実行されるよう制限できます。

個別のキューとワーカー

具体的な例として、メインと CI データベースがある場合、メインと CI の両方のバックグラウンドワーカーも持つことになります。

このアプローチの利点。

  • 推論と保守が容易な明確な関心の分離を提供します。
  • 各データベースを独立して処理することで複数の物理データベースを最大限に活用します。また、それらを独立して管理するための運用の柔軟性を提供します。
  • ワーカーは追加のデータを渡すことなく、実行するコンテキストを自動的に知っています。

開発者がマイグレーションの接続方法を選択できるようにする

これは本当にハード要件である唯一の「決定」です。個々のバックグラウンドマイグレーションジョブは1つまたは複数のデータベースと通信する必要があるかもしれないため、決定は開発者に委ねなければなりません。

シナリオに応じて、実際にはいくつかの方法で機能する可能性があります。

  • ジョブは意図されたデータベースの正しい基底クラスを継承するマイグレーション固有のモデルを使用します。

    class ExplicitMigrationJob
      class SecurityScan < Gitlab::Database[:main]
      end
    
      class Build < Gitlab::Database[:ci]
      end
    
      def perform(start_id, end_id)
        build_ids = SecurityScan.(id: start_id..end_id).pluck(:build_id)
    
        Build.connection.execute(<<~SQL)
          DELETE FROM ci_builds
          WHERE id IN (#{build_ids.join(',')})
            AND (...)
        SQL
      end
    end
    
  • ジョブは汎用として設計され、実行フレームワークによって正しいコンテキスト用に既に設定されている SharedModel(基底クラスとして、または接続を取得するために)を使用できます。

    class GenericMigrationJob
      def perform(source_table, target_table, start_id, end_id)
        SharedModel.connection.execute(<<~SQL)
          INSERT INTO #{target_table}
          SELECT *
          FROM #{source_table}
          WHERE #{source_table}.id BETWEEN #{start_id} AND #{end_id}
        SQL
      end
    end