- カラムの削除
- カラム名の変更
- 列制約の変更
- カラムタイプの変更
- 列のデフォルトの変更
- ラージテーブルのスキーマ変更
- インデックスの追加
- インデックスの削除
- テーブルの追加
- テーブルの削除
- テーブル名の変更
- 外部キーの追加
- 外部キーの削除
-
integer
プライマリキーのマイグレーションbigint
- データのマイグレーション
マイグレーションにおけるダウンタイムの回避
データベースを扱うとき、あるオペレーションではダウンタイムが必要になることがあります。マイグレーションではダウンタイムを発生させることができないため、ダウンタイムなしで同じ最終結果を得るための一連の手順を使用する必要があります。このガイドでは、ダウンタイムが必要と思われるさまざまなオペレーション、その影響、ダウンタイムを発生させずに実行する方法について説明します。
カラムの削除
カラムを削除するのはやっかいです。GitLabのプロセスがまだカラムを使っている可能性があるからです。これを安全に回避するには、3つのリリースで3つのステップが必要です:
3つのリリースにまたがる理由は、カラムの削除は簡単にロールバックできない破壊的オペレーションだからです。
この手順に従うことで、GitLab.comへのデプロイやセルフマネージドインストールのアップグレードプロセスで、これらの手順をひとまとめにしてしまうようなことがないようにすることができます。
列を無視する (リリース M)
最初のステップは、アプリケーションコード内でカラムを無視し、モデルのバリデーションを含むそのカラムへのすべてのコード参照を削除することです。Railsはカラムをキャッシュし、さまざまな場所でこのキャッシュを再利用するため、このステップが必要です。これは無視するカラムを定義することで実行できます。たとえば、リリース12.5
、Userモデルのupdated_at
を無視するには次のようにします:
class User < ApplicationRecord
include IgnorableColumns
ignore_column :updated_at, remove_with: '12.7', remove_after: '2019-12-22'
end
複数のカラムを無視することもできます:
ignore_columns %i[updated_at created_at], remove_with: '12.7', remove_after: '2019-12-22'
モデルがCEとEEに存在する場合、CEモデルでは列は無視されなければなりません。モデルがCEとEEに存在する場合、列はCEモデルでは無視されなければなりません。
列の無視ルールをいつ削除しても安全であるかを示す必要があります:
-
remove_with
カラム無視を追加した後、GitLabのリリースに通常2リリース(M+2)(12.7
)を設定します。 -
remove_after
カラムignoreを削除しても安全だと考えられる日付、通常はM+1リリース日の後、M+2開発サイクルの間に設定します。たとえば、12.7
の開発サイクルは、2019-12-18
と2020-01-17
の間にあり、列を12.6
削除するリリース12.6
であるため、2019-12-22
として、12.6
日付をリリース日に設定するのが安全12.6
です。
この情報によって、カラムの無視をより適切に推論できるようになり、通常のリリースやGitLab.comへのデプロイの両方でカラムの無視を早く削除しすぎないようにすることができます。たとえば、カラムを無視する変更を含む大量の変更をデプロイして、その後にカラムの無視を削除するような事態を避けることができます (これはダウンタイムにつながります)。
この例では、カラムを無視する変更がリリース12.5
に入っています。
列の削除 (リリースM+1)
この例を続けると、カラムの削除はリリース12.6
の_デプロイ後の_マイグレーションに入ります:
デプロイ後のマイグレーションを作成することから始めましょう:
bundle exec rails g post_deployment_migration remove_users_updated_at_column
カラムを削除するマイグレーションを作成する際には、これらのシナリオを考慮する必要があります:
削除されたカラムには、そのカラムに属するインデックスまたは制約がありません。
この場合、トランザクションマイグレーションを使用することができます:
class RemoveUsersUpdatedAtColumn < Gitlab::Database::Migration[2.1]
def up
remove_column :users, :updated_at
end
def down
add_column :users, :updated_at, :datetime
end
end
大きなテーブルでマイグレーションを実行する場合、ロックの再試行を有効にすることを検討できます。
削除されたカラムには、そのカラムに属するインデックスや制約があります。
down
、削除されたインデックスや制約を元に戻す必要がある場合、トランザクションマイグレーションではこれを行うことはできません。マイグレーションはこのようになります:
class RemoveUsersUpdatedAtColumn < Gitlab::Database::Migration[2.1]
disable_ddl_transaction!
def up
remove_column :users, :updated_at
end
def down
unless column_exists?(:users, :updated_at)
add_column :users, :updated_at, :datetime
end
# Make sure to add back any indexes or constraints,
# that were dropped in the `up` method. For example:
add_concurrent_index(:users, :updated_at)
end
end
down
メソッドでは、列を再度追加する前に、その列が既に存在するかどうかを確認します。これは、マイグレーションが非トランザクションであり、実行中に失敗した可能性があるためです。
disable_ddl_transaction!
はマイグレーション全体をラップするトランザクションを無効にするために使われます。
データベースマイグレーションの詳細については、マイグレーションスタイルガイドのページを参照してください。
無視ルールの削除(リリース M+2)
次のリリース、この例では12.7
で、無視ルールを削除するために別のマージリクエストを設定します。これにより、ignore_column
の行が削除され、不要になった場合はIgnoreableColumns
の内部も削除されます。
これは、remove_with
で示されたリリースで、remove_after
の日付が過ぎたらマージされるべきです。
カラム名の変更
標準的な方法でカラムの名前を変更すると、データベースのマイグレーション中やマイグレーション後にアプリケーションが古いカラム名を使い続ける可能性があるため、ダウンタイムが発生します。ダウンタイムを発生させずにカラムの名前を変更するには、2つのマイグレーションが必要です。これらのマイグレーションは同じリリースで実行できます。手順は以下の通りです:
- 通常のマイグレーションを追加します (リリース M)
- 列を無視(リリース M)
- デプロイ後のマイグレーションを追加(リリース M)
- 無視ルールを削除(リリースM+1)
通常のマイグレーションを追加しました (リリース M)。
まず、通常のマイグレーションを作成する必要があります。このマイグレーションでは、Gitlab::Database::MigrationHelpers#rename_column_concurrently
を使って名前の変更を行う必要があります。例えば
# A regular migration in db/migrate
class RenameUsersUpdatedAtToUpdatedAtTimestamp < Gitlab::Database::Migration[2.1]
disable_ddl_transaction!
def up
rename_column_concurrently :users, :updated_at, :updated_at_timestamp
end
def down
undo_rename_column_concurrently :users, :updated_at, :updated_at_timestamp
end
end
これにより、カラム名の変更、データの同期、インデックスと外部キーのコピーが行われます。
カラムに、元のカラムの名前を含まないインデックスが1つ以上含まれている場合、前述の手順は失敗します。この場合、これらのインデックスの名前を変更する必要があります。
カラムを無視する(リリースM)
次のステップは、アプリケーションコードでカラムを無視し、使用されないようにすることです。Railsはカラムをキャッシュし、さまざまな場所でこのキャッシュを再利用するため、このステップが必要です。このステップはカラムが削除されたときの最初のステップと似ており、同じ要件が適用されます。
class User < ApplicationRecord
include IgnorableColumns
ignore_column :updated_at, remove_with: '12.7', remove_after: '2019-12-22'
end
デプロイ後のマイグレーションを追加する (リリースM)
リネームの手順では、デプロイ後のマイグレーションでいくつかのクリーンアップが必要です。このクリーンアップはGitlab::Database::MigrationHelpers#cleanup_concurrent_column_rename
を使って実行できます:
# A post-deployment migration in db/post_migrate
class CleanupUsersUpdatedAtRename < Gitlab::Database::Migration[2.1]
disable_ddl_transaction!
def up
cleanup_concurrent_column_rename :users, :updated_at, :updated_at_timestamp
end
def down
undo_cleanup_concurrent_column_rename :users, :updated_at, :updated_at_timestamp
end
end
大きなテーブルの名前を変更する場合、最初のマイグレーションが実行され、2回目のクリーンアップマイグレーションがまだ実行されていない状態を注意深く考慮してください。Canaryでは、システムがこの状態でかなりの時間実行される可能性があります。
無視ルールの削除(リリースM+1)
カラムが削除されたときと同じように、リネームが完了したら、その後のリリースで無視ルールを削除する必要があります。
列制約の変更
NOT NULL
節(または他の制約)の追加や削除は、通常ダウンタイムなしで行うことができます。しかし、アプリケーションの変更を_最初に_デプロイする必要があります。したがって、列の制約の変更は、デプロイ後のマイグレーションで行う必要があります。
change_column
。列型全体を再定義するため、非効率的なクエリが生成されるため、使用を避けてください。
具体的な使用例については、以下のガイドを参照してください:
カラムタイプの変更
カラムの型を変更するにはGitlab::Database::MigrationHelpers#change_column_type_concurrently
を使用します。この方法はrename_column_concurrently
と同様に動作します。例えば、users.username
の型をstring
からtext
に変更したいとします:
通常のマイグレーションの作成
通常のマイグレーションは、データの同期を保つためにいくつかのトリガーを設定し、一時的な名前で新しいカラムを作成するために使われます。このようなマイグレーションは次のようになります:
# A regular migration in db/migrate
class ChangeUsersUsernameStringToText < Gitlab::Database::Migration[2.1]
disable_ddl_transaction!
def up
change_column_type_concurrently :users, :username, :text
end
def down
undo_change_column_type_concurrently :users, :username
end
end
デプロイ後のマイグレーションを作成します。
次に、デプロイ後のマイグレーションを使って変更をクリーンアップする必要があります:
# A post-deployment migration in db/post_migrate
class ChangeUsersUsernameStringToTextCleanup < Gitlab::Database::Migration[2.1]
disable_ddl_transaction!
def up
cleanup_concurrent_column_type_change :users, :username
end
def down
undo_cleanup_concurrent_column_type_change :users, :username, :string
end
end
これで完了です!
データを新しい型にキャスト
型の変更によっては、データを新しい型にキャストする必要があります。例えば、text
からjsonb
に変更する場合などです。 この場合、type_cast_function
オプションを使用します。不正なデータがなく、キャストが常に成功することを確認してください。キャスト・エラーを処理するカスタム関数を用意することもできます。
マイグレーション例:
def up
change_column_type_concurrently :users, :settings, :jsonb, type_cast_function: 'jsonb'
end
列のデフォルトの変更
カラムのデフォルトを変更するのは、Railsがデフォルトと等しい値をどのように扱うかによって難しくなります。
実行中のコードがカラムの古いデフォルト値を明示的に書き込むことがある場合は、古いデフォルトを明示的に指定するINSERTクエリでRailsが古いデフォルトを新しいデフォルトに置き換えるのを防ぐために、複数のステップを踏む必要があります。
これを行うには、2つのマイナーリリースで手順を踏む必要があります:
-
モデル に
SafelyChangeColumnDefault
を追加し、マイグレーション後のデフォルトを変更します。 -
次のマイナーリリースでは、
SafelyChangeColumnDefault
の懸念 をクリーンアップします。
セルフマネージドリリースはマイナーリリース全体を1つのゼロダウンタイムのデプロイにバンドルするため、SafelyChangeColumnDefault
をクリーンアップする前にマイナーリリースを待たなければなりません。
SafelyChangeColumnDefault
、マイグレーション後のモデルでデフォルトを変更します。
最初のステップはアプリケーションコードにおいてカラムを変更しても安全であるようにマークすることです。
class Ci::Build < ApplicationRecord
include SafelyChangeColumnDefault
columns_changing_default :partition_id
end
それから、デフォルトを変更するためにデプロイ後のマイグレーションを作成します:
bundle exec rails g post_deployment_migration change_ci_builds_default
class ChangeCiBuildsDefault < Gitlab::Database::Migration[2.1]
def up
change_column_default('ci_builds', 'partition_id', from: 100, to: 101)
end
def down
change_column_default('ci_builds', 'partition_id', from: 101, to: 100)
end
end
大きなテーブルでマイグレーションを実行する場合、ロックの再試行を有効にすることを検討できます。
次のマイナーリリースでSafelyChangeColumnDefault
。
次のマイナーリリースでは、columns_changing_default
の呼び出しを削除する新しいマージリクエストを作成してください。また、SafelyChangeColumnDefault
のインクルードも、別のカラムで不要であれば削除してください。
ラージテーブルのスキーマ変更
change_column_type_concurrently
、rename_column_concurrently
、ダウンタイムなしでテーブルのスキーマを変更することができますが、大きなテーブルではあまりうまくいきません。すべての作業が順番に行われるため、マイグレーションが完了するまでに非常に長い時間がかかり、デプロイを進めることができません。また、多くの行を順番に高速に更新するため、データベースへの負荷が大きくなります。
データベースへの負荷を軽減するために、大きなテーブル(例えば、issues
)の列をマイグレーションする場合は、バックグラウンドマイグレーションを使用する必要があります。バックグラウンドマイグレーションは、デプロイを遅くすることなく、作業や負荷をより長い期間に分散します。
詳細については、バッチバックグランドマイグレーションのクリーンアップに関するドキュメントを参照してください。
インデックスの追加
add_concurrent_index
を使用している場合、インデックスの追加にダウンタイムは必要ありません。
詳細はマイグレーション・スタイル・ガイドも参照してください。
インデックスの削除
インデックスの削除にダウンタイムは必要ありません。
テーブルの追加
このオペレーションは、まだテーブルを使用するコードがないので安全です。
テーブルの削除
テーブルの削除は、デプロイ後のマイグレーションを使用して安全に行うことができますが、アプリケーションがテーブルを使用しなくなった場合に限ります。
データベース辞書で説明したプロセスを使用して、db/docs/deleted_tables
にテーブルを追加します。テーブルが削除されても、データベースマイグレーションでは参照されます。
テーブル名の変更
データベースのマイグレーション中やマイグレーション後も、アプリケーションが古いテーブル名を使用し続ける可能性があるため、テーブル名の変更にはダウンタイムが必要です。
テーブルとActiveRecordモデルがまだ使用されていない場合、古いテーブルを削除して新しいテーブルを作成するのが「テーブル名の変更」の望ましい方法です。
テーブルの名前を変更するには、マルチリリースのrename tableプロセスに従ってください。
外部キーの追加
外部キーの追加は、通常3つのステップで行います:
- トランザクションの開始
-
ALTER TABLE
を実行して制約を追加します。 - すべての既存データのチェック
ALTER TABLE
は通常、トランザクションが終了するまで排他ロックを取得するため、この方法ではダウンタイムが発生します。
GitLabではこれを回避するためにGitlab::Database::MigrationHelpers#add_concurrent_foreign_key
。この方法では、ダウンタイムは必要ありません。
外部キーの削除
このオペレーションにはダウンタイムは必要ありません。
integer
プライマリキーのマイグレーションbigint
integer
プライマリ・キー(PK)を持ついくつかのテーブルのオーバーフロー・リスクを防ぐために、それらの PK をbigint
にマイグレーションする必要があります。 ダウンタイムを発生させることなく、データベースに過度の負荷をかけることなく、これを行うプロセスを以下に示します。
コンバージョンを初期化し、既存データのマイグレーションを開始(リリースN)
処理を開始するには、新しいbigint
列を作成するために通常のマイグレーションを追加します。提供されているinitialize_conversion_of_integer_to_bigint
ヘルパーを使用してください。このヘルパーは、新しいレコードに対して両方のカラムを同期させるデータベーストリガーも作成します(コード):
# frozen_string_literal: true
class InitializeConversionOfMergeRequestMetricsToBigint < Gitlab::Database::Migration[2.1]
disable_ddl_transaction!
TABLE = :merge_request_metrics
COLUMNS = %i[id]
def up
initialize_conversion_of_integer_to_bigint(TABLE, COLUMNS)
end
def down
revert_initialize_conversion_of_integer_to_bigint(TABLE, COLUMNS)
end
end
新しいbigint
カラムは無視します:
# frozen_string_literal: true
class MergeRequest::Metrics < ApplicationRecord
include IgnorableColumns
ignore_column :id_convert_to_bigint, remove_with: '16.0', remove_after: '2023-05-22'
end
既存のデータをマイグレーションするために、バッチ化されたバックグラウンドマイグレーション(コード)をエンキューします:
# frozen_string_literal: true
class BackfillMergeRequestMetricsForBigintConversion < Gitlab::Database::Migration[2.1]
restrict_gitlab_migration gitlab_schema: :gitlab_main
TABLE = :merge_request_metrics
COLUMNS = %i[id]
def up
backfill_conversion_of_integer_to_bigint(TABLE, COLUMNS, sub_batch_size: 200)
end
def down
revert_backfill_conversion_of_integer_to_bigint(TABLE, COLUMNS)
end
end
バックグラウンドマイグレーションを監視
マイグレーションが実行中にどのように実行されているかをチェックします。これには複数の方法があります。
バッチバックグラウンドマイグレーションのハイレベルステータス
バッチバックグラウンドマイグレーションのステータスを確認する方法を参照してください。
データベースへのクエリ
関連するデータベーステーブルを直接クエリできます。読み込み専用のレプリカへのアクセスが必要です。クエリの例:
-- Get details for batched background migration for given table
SELECT * FROM batched_background_migrations WHERE table_name = 'namespaces'\gx
-- Get count of batched background migration jobs by status for given table
SELECT
batched_background_migrations.id, batched_background_migration_jobs.status, COUNT(*)
FROM
batched_background_migrations
JOIN batched_background_migration_jobs ON batched_background_migrations.id = batched_background_migration_jobs.batched_background_migration_id
WHERE
table_name = 'namespaces'
GROUP BY
batched_background_migrations.id, batched_background_migration_jobs.status;
-- Batched background migration progress for given table (based on estimated total number of tuples)
SELECT
m.table_name,
LEAST(100 * sum(j.batch_size) / pg_class.reltuples, 100) AS percentage_complete
FROM
batched_background_migrations m
JOIN batched_background_migration_jobs j ON j.batched_background_migration_id = m.id
JOIN pg_class ON pg_class.relname = m.table_name
WHERE
j.status = 3 AND m.table_name = 'namespaces'
GROUP BY m.id, pg_class.reltuples;
Sidekiqログ
Sidekiqログを使用して、バッチバックグラウンドマイグレーションを実行するワーカーを監視することもできます:
-
@gitlab.com
のメールアドレスでKibana にサインインします。 - インデックスパターンを
pubsub-sidekiq-inf-gprd*
に変更します。 -
json.queue: cronjob:database_batched_background_migration
のフィルタを追加。
PostgreSQL の遅いクエリログ
低速クエリログは、実行に1秒以上かかった低速クエリを記録します。バッチバックグラウンドマイグレーションでこれらを見るには、以下のようにしてください:
-
@gitlab.com
のメールアドレスでKibana にサインインします。 - インデックスパターンを
pubsub-postgres-inf-gprd*
に変更します。 -
json.endpoint_id.keyword: Database::BatchedBackgroundMigrationWorker
のフィルタを追加。 - オプション。更新情報のみを表示するには、
json.command_tag.keyword: UPDATE
のフィルタを追加します。 - オプション。失敗したステートメントのみを表示するには、
json.error_severity.keyword: ERROR
のフィルタを追加します。 - オプション。テーブル名によるフィルタを追加します。
Grafana ダッシュボード
データベースの健全性を監視するには、以下の追加メトリクスを使用します:
-
PostgreSQLのタプル統計: 活発に変換されているテーブルの更新率が高い場合や、このテーブルのデッドタプルの割合が増加している場合は、
autovacuum
。 - PostgreSQLの概要:プライマリデータベースサーバで高いシステム使用率や1秒あたりのトランザクション数((TPS) )が表示された場合、マイグレーションが問題を引き起こしている可能性があります。
Prometheusメトリクス
各バッチバックグラウンドマイグレーションのメトリクス数がPrometheusに公開されます。これらのメトリクスは、Thanosで検索および可視化できます(例を参照)。
列の入れ替え(リリースN + 1)
バックグラウンドが完了し、すべてのレコードに新しいbigint
列が入力されたら、列を入れ替えることができます。スワップはデプロイ後のマイグレーションで行われます。正確なプロセスは変換されるテーブルによって異なりますが、一般的には以下の手順で行われます:
- 提供されている
ensure_batched_background_migration_is_finished
ヘルパーを使用して、バッチマイグレーションが終了したことを確認します(例を参照)。マイグレーションが完了していない場合、後続のステップは失敗します。事前に確認することで、より役立つエラーメッセージを表示することができます。 -
integer
列を使用する既存のインデックスと一致するbigint
列を使用するインデックスを作成します(例を参照)。 -
integer
列を使用して既存の FK と一致するbigint
列を使用して外部キー(FK) を作成します。これは、他のテーブルを参照するFKと、マイグレーション対象のテーブルを参照するFKの両方に対して行います(例を参照)。 - トランザクション内で、列を入れ替えます:
トリガと古いinteger
列を削除します(リリースN + 2)。
デプロイ後のマイグレーションと提供されているcleanup_conversion_of_integer_to_bigint
ヘルパーを使用して、データベーストリガと古いinteger
列を削除します(例を参照してください)。
無視ルールの削除(リリースN + 3)
カラムが削除された後の次のリリースでは、無視ルールはもう必要ないので削除してください(例を参照してください)。
データのマイグレーション
データのマイグレーションは厄介です。通常、データのマイグレーションは3つのステップで行われます:
- 最初のデータバッチのマイグレーション
- アプリケーションコードのデプロイ
- 残りのデータのマイグレーション
通常はこれでうまくいきますが、常にうまくいくわけではありません。例えば、フィールドのフォーマットがJSONから他のものに変更される場合、ちょっとした問題が発生します。アプリケーションコードをデプロイする前に既存のデータを変更すると、ほとんどの場合エラーに遭遇するでしょう。一方、アプリケーションコードをデプロイした後にマイグレーションを行うと、同じ問題に遭遇する可能性があります。
単に無効なデータを修正するだけであれば、デプロイ後のマイグレーションで十分です。データの形式を変更する必要がある場合(たとえば、JSONから他のものへ)、通常は新しいデータ形式のために新しいカラムを追加し、アプリケーションにそれを使用させるのが最善です。このような場合、手順は次のようになります:
- 新しいフォーマットで新しいカラムを追加します。
- 既存のデータをこの新しいカラムにコピーします。
- アプリケーションコードのデプロイ
- デプロイ後のマイグレーションで、残っているデータをコピーします。
一般的に、万能な解決策はありません。したがって、可能な限り最善の方法で実装されるように、マージリクエストでこの種のマイグレーションについて議論するのが最善です。