本番データベースマイグレーションを安全に実行する方法

デプロイパイプラインはグリーン。コードレビューも承認済み。ステージング環境も問題なし。そして、すべてのエンジニアが恐れる瞬間が訪れます。本番データベースに対してマイグレーションを実行する時です。

ここに本当のリスクが潜んでいます。ラップトップ上で完璧に動作したスキーマ変更が、本番テーブルを数分間ロックするかもしれません。1000行で問題なかったデータマイグレーションが、100万行相手では数時間かかるかもしれません。そして、何かがうまくいかなければ、アプリケーションは実際のユーザーにエラーを返し始めます。

問題は技術面だけではありません。タイミング、調整、そしていつ止めるべきかを知ることにも関わります。

マイグレーションを実行するタイミング

アプリケーションのデプロイとは異なり、データベースマイグレーションはクエリのパフォーマンスとデータ可用性に直接影響を与えます。最も安全な実行時間帯はトラフィックが少ない時です。多くのチームはこれをダウンタイムウィンドウと呼びますが、この名前は誤解を招きます。ダウンタイムウィンドウはアプリケーションをオフラインにする必要があるという意味ではありません。混乱を引き起こす可能性のある変更を行い、その可能性に備えることに合意した期間を意味します。

本当の問いはこうです。アプリケーションがトラフィックを処理している間にマイグレーションを実行できますか? マイグレーションを慎重に設計すれば、答えは多くの場合イエスです。ただし、それは特定のコマンドを実行したときにデータベースが何をするかを理解していることが前提です。

ロック問題

本番マイグレーションにおけるトラブルの最も一般的な原因はロックです。ALTER TABLE のようなコマンドを実行すると、ほとんどのデータベースは操作の進行中に他の変更を防ぐためにテーブルをロックします。そのコマンドに時間がかかると、そのテーブルにアクセスするアプリケーションからのすべてのクエリが待機するか失敗します。ユーザーはページの表示が遅くなったり、エラーメッセージが表示されたりします。

一部のデータベースはロックを減らす方法を提供しています。PostgreSQL は CREATE INDEX CONCURRENTLY をサポートしており、書き込みをブロックせずにインデックスを構築します。MySQL も特定の操作に対して同様のオプションを持っています。しかし、すべての変更をロックなしで実行できるわけではありません。デフォルト値を持つカラムの追加、カラム型の変更、カラムの削除などは、多くの場合排他ロックが必要です。

重要なのは、マイグレーションを書く前にデータベースが何をサポートしているかを知ることです。使用しているデータベースバージョンのドキュメントを確認してください。小さなサンプルだけでなく、本番データのコピーでマイグレーションをテストしてください。現実的な条件下でロックがどのくらい続くかを測定してください。

例えば、PostgreSQL でデフォルト値を持つカラムを追加する安全なパターンは以下の通りです。長い排他ロックを回避します。

-- ステップ1: カラムをNULL許容で追加(高速、デフォルト値の再書き込みなし)
ALTER TABLE users ADD COLUMN display_name text;

-- ステップ2: カラムを小さなバッチでバックフィル
UPDATE users SET display_name = username WHERE display_name IS NULL LIMIT 1000;
-- 行がなくなるまで繰り返し

-- ステップ3: NOT NULLを設定(高速、データの再書き込みなし)
ALTER TABLE users ALTER COLUMN display_name SET NOT NULL;

大きなマイグレーションを小さなステップに分割する

よくある間違いは、1つのマイグレーションでやりすぎようとすることです。例えば、新しいカラムを追加し、別のカラムからデータをバックフィルし、古いカラムを削除する、すべてを1つのステップで行うことです。これにより、長時間実行される操作が発生し、その間ずっとロックが保持されます。

より安全なアプローチは、作業を複数の小さなマイグレーションに分割することです。

  1. デフォルト値なしで新しいカラムを追加する。
  2. バックグラウンドジョブを実行して、新しいカラムにデータをバッチで投入する。
  3. データが完全で正しいことを確認する。
  4. 別のマイグレーションで古いカラムを削除する。

各ステップは、次のステップに進む前に検証できます。ステップ2で問題が発生しても、何も失うものはありません。データを修正して再試行できます。このアプローチは時間がかかりますが、リスクははるかに低くなります。

パイプラインに安全チェックを組み込む

パイプラインは盲目的にマイグレーションを実行すべきではありません。変更を加える前に、データベースの現在の状態を確認する必要があります。マイグレーションを開始する前に、パイプラインは長時間実行されているクエリがないかチェックできます。数秒以上実行されているクエリがある場合、マイグレーションは待機する必要があります。それらのクエリがマイグレーションをブロックするロックを保持している可能性があるか、マイグレーションがそれらをブロックする可能性があります。

以下のフローチャートは、上記で説明した安全チェックプロセスを示しています。

flowchart TD A[マイグレーション開始] --> B{トラフィック低い?} B -- はい --> C[ロック分析を伴うマイグレーション実行] B -- いいえ --> D[待機して再チェック] D --> B C --> E{ロック解放された?} E -- はい --> F{クエリレイテンシ正常?} E -- いいえ --> G[停止して通知] F -- はい --> H[成功を確認] F -- いいえ --> G G --> I[必要に応じてロールバック]

マイグレーション中、パイプラインは以下を監視する必要があります。

  • 実行時間: 各ステートメントにかかる時間
  • ロック待機時間: 他のクエリがロックを待機しているかどうか
  • エラー率: データベースまたはアプリケーションからのエラー

これらのメトリクスのいずれかがしきい値を超えた場合、パイプラインはマイグレーションを停止し、チームに通知する必要があります。これは慎重になりすぎるという話ではありません。小さな問題が本番インシデントに発展するのを防ぐための明確なメカニズムを持つことです。

マイグレーション完了後に起こること

マイグレーションはエラーなく完了しました。パイプラインはグリーンを示しています。しかし、作業はまだ終わっていません。

アプリケーションにはまだ古いデータベース接続が開いている可能性があります。コネクションプールは接続をキャッシュしており、それらのキャッシュされた接続が古いスキーマへの参照を保持している可能性があります。一部のORMは、新しいスキーマと一致しなくなったクエリプランをキャッシュします。これらの問題はすぐに現れるとは限りません。数分後や数時間後に微妙なエラーを引き起こす可能性があります。

多くのチームはマイグレーション後にアプリケーションを再起動するか、少なくとも古い接続をドレインして新しい接続を作成させます。他のチームは、ログとメトリクスを監視しながら数分間待ってから、マイグレーションが成功したと宣言します。正確なアプローチは、アプリケーションスタックとコネクションプーリングの仕組みによって異なります。

本番マイグレーションの実践的チェックリスト

本番でマイグレーションを実行する前に、このチェックリストを確認してください。

  • 開発データベースだけでなく、本番データのコピーでマイグレーションをテストする
  • 各ステートメントにかかる時間と、取得するロックを測定する
  • データベースバージョンが、目的の操作に対してロックフリーの代替手段をサポートしていることを確認する
  • マイグレーションを開始する前に、長時間実行されているクエリがないか確認する
  • マイグレーション中のロック待機時間とエラー率の監視を設定する
  • 明確な中断条件を定義する: どのメトリクスまたはエラーが停止をトリガーするか
  • ロールバック計画を準備する: 問題が発生した場合にマイグレーションを元に戻す方法
  • マイグレーションの実行前後にチームに通知する
  • マイグレーション後にアプリケーションを再起動するか、接続をリフレッシュする計画を立てる

まとめ

本番環境でデータベースマイグレーションを実行することは、リスクを回避することではありません。リスクを理解し、管理可能な部分に分割し、いつ停止すべきかについて明確なシグナルを持つことです。最高のマイグレーションは、スムーズに実行されたため誰も気づかないものです。次に良いのは、実際の被害を引き起こす前に早期に停止されたものです。最悪なのは、完了まで実行されたものの、検出に何時間もかかる方法でアプリケーションを壊してしまうものです。最初のカテゴリを目指してパイプラインを構築しつつ、常に2番目のカテゴリに備えてください。