本番環境でデータベースマイグレーションが失敗するとき:夜も眠れなくなる3つのシナリオ

本番環境でマイグレーションを実行した。成功した。エラーもタイムアウトもロックもなかった。安心して次のタスクに移る。

2時間後、電話が鳴る。レポートが壊れた。データがおかしい。忘れていたサービスが重要なテーブルにNULLを書き込んでいる。マイグレーションは成功したが、本番システムは崩壊しつつある。

これがデータベースマイグレーションの悪夢だ。アプリケーションデプロイメントでは障害は即座に明らかになることが多いが、マイグレーションの障害は数時間から数日間隠れることがある。気づいたときには、被害はすでに広がっている。

ここでは、SQLが失敗したからではなく、副作用が全員を不意打ちにした3つの実際のシナリオを紹介する。

シナリオ1:すべてを壊した新しいカラム

チームはusersテーブルにphone_numberカラムを追加する必要がある。ステージングではマイグレーションは正常に動作する。すべてのテストに合格する。自信を持って本番環境にプッシュする。

カラムは作成される。エラーはない。しかし数秒後、アプリケーションの動作がおかしくなる。

何が起こったのか:本番アプリケーションはまだ更新されていない。古いコードがまだ実行されており、SELECT * FROM usersのようなクエリを送信する。これは問題ない。新しいカラムは単に無視される。本当の問題は別の場所にある。別のコードがphone_numberにデータを挿入し始めるが、新しいアプリケーションが期待する形式とは異なる形式を使用している。電話番号は、国コードがあるもの、ないもの、ダッシュがあるもの、ないものと、混在した形式で入ってくる。

このシナリオを引き起こしたマイグレーションを考えてみよう:

ALTER TABLE users ADD COLUMN phone_number VARCHAR(20);

これは無害に見える。しかしNOT NULL制約やデフォルト値がないため、カラムは任意の形式を受け入れる。さらに悪いことに、テーブルが大きい場合、このALTER TABLEは操作中ずっとテーブルへの書き込みをロックする。本番環境では、そのロックによって数秒で何百ものリクエストがキューイングされる可能性がある。本当の危険はSQL自体ではなく、すべてのアプリケーションインスタンスが新しいスキーマを処理できるようになる前にスキーマが変更されたことだ。

これで、複数のシステムが依存するカラムに不整合なデータができてしまう。チームは厄介な選択を迫られる:既存のデータをクリーンアップするか、準備が整う前に新しいアプリケーションバージョンを本番環境に急いで投入するか。

ここでの核心的な問題はタイミングだ。スキーマが、それを理解するアプリケーションコードが完全にデプロイされる前に変更された。分散システムでは、すべてのインスタンスが同時に更新されるわけではない。短い期間、時にはそれ以上、古いコードが新しいスキーマと相互作用する。

シナリオ2:夜間レポートを壊した型変更

これはもっと微妙だ。チームはpriceカラムをintegerからdecimalに変更することにした。良いアイデアだ。価格には精度が必要だ。マイグレーションは完璧に実行される。即座のエラーはない。アプリケーションは正常に見える。

しかし6ヶ月前、誰かがpriceを整数として扱うレポートクエリを書いた。そのクエリはメインページでは使われていない。財務レポートのために夜間に1回実行される。午前2時、レポートは完全に失敗する。価格を整数値と比較するすべてのクエリが、型不一致エラーをスローするようになる。

これはエンジニアが「ブロッキング変更」と呼ぶものだ。スキーマ変更は日中は何も壊さなかったが、夜間に実行される重要なバッチプロセスを静かに壊した。朝までに、財務チームは昨日の数字が合わない理由を尋ねてくる。

危険な部分は?この障害を発見するまでに何時間もかかる可能性があることだ。そして修正は簡単ではない。型変更を元に戻すには別のマイグレーションが必要であり、それ自体にリスクが伴う。

シナリオ3:3つのテーブルを汚染した削除されたカラム

これが最も危険なシナリオだ。チームはold_statusカラムがもはや使われていないと確信している。数ヶ月前に非推奨になった。メインアプリケーションでは誰も参照していない。それを削除するマイグレーションを書く。

マイグレーションはスムーズに実行される。カラムは消える。どこにもエラーはない。

しかし、2年前に退社したチームが書いたバックグラウンドサービス(データ同期ジョブ)が、まだ定期的にold_statusを読み取っている。カラムがなくてもクラッシュしない。ただ他のテーブルにNULL値を書き込み始める。NULLが伝播する。データの整合性が、次の2時間で3つの異なるテーブルにわたって静かに壊れる。

誰かが気づいたときには、被害はすでに発生している。カラムの削除を単に「元に戻す」ことはできない。他のテーブルのデータはすでに破損している。回復には、どの行が影響を受けたかを正確に理解し、バックアップから欠落した値を再構築し、注意深く修復スクリプトを実行する必要がある。

データベースマイグレーションがアプリケーションデプロイメントと異なる理由

これら3つのシナリオには共通のパターンがある:マイグレーションは正常に実行されたが、副作用が後で現れた。これがデータベースマイグレーションをアプリケーションデプロイメントと根本的に異なるものにしている。

上記の3つのシナリオには明確なパターンがある:成功したスキーマ変更が遅延障害を引き起こす。次のフローチャートは、各シナリオを根本原因から結果までマッピングしている。

flowchart TD subgraph Scenario1[シナリオ1: 新しいカラム] A1[phone_numberカラムを追加] --> B1[古いコードが不整合な形式を書き込む] B1 --> C1[新しいカラムのデータ破損] end subgraph Scenario2[シナリオ2: 型変更] A2[priceをintegerからdecimalに変更] --> B2[夜間レポートが整数比較を使用] B2 --> C2[午前2時にレポートが失敗] end subgraph Scenario3[シナリオ3: カラム削除] A3[old_statusカラムを削除] --> B3[バックグラウンドサービスがNULLを書き込む] B3 --> C3[NULLが3つのテーブルに伝播] C3 --> D3[データ整合性が破壊される] end

アプリケーションデプロイメントが失敗した場合、通常はすぐにわかる。ログにエラーが表示される。ユーザーが問題を報告する。監視アラートが発報する。アプリケーションバージョンをロールバックして、サービスを迅速に復旧できる。

データベースマイグレーションはそうはいかない。スキーマ変更は以下の可能性がある:

  • 新しいデータが到着したときにのみ現れる不整合を生み出す
  • 継続的ではなくスケジュールで実行されるクエリを壊す
  • 関連テーブルにゆっくりと広がるデータ破損を引き起こす
  • 忘れていた、または知らなかったサービスに影響を与える

最悪の部分は?被害が発生した後は、コード変更を元に戻すようにスキーマ変更を単に「元に戻す」ことはできない。削除されたカラムは、特に他のテーブルがその欠落に依存している場合、簡単には復元できない。変更されたデータ型は、それ自体にリスクを伴う逆マイグレーションを必要とする。

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

次の本番マイグレーションを実行する前に、以下のチェックを実施すること:

  • すべてのコンシューマを特定する。 影響を受けるテーブルに触れるすべてのサービス、cronジョブ、レポート、データパイプラインをリストアップする。すべてを知っていると思い込まないこと。
  • 遅延実行を確認する。 スケジュールで実行されるクエリ、バッチプロセス、バックグラウンドジョブを見つける。これらは数時間後に静かに失敗するものだ。
  • 後方互換性を検証する。 古いアプリケーションコードが新しいスキーマでも動作するか?少なくとも1つのデプロイメントサイクルの間、スキーマは新旧両方のコードをサポートする必要がある。
  • 復旧計画を準備する。 何か問題が発生した場合にデータを復元する方法を正確に把握する。マイグレーションだけでなく、復旧プロセスもテストすること。
  • トラフィックが少ない時間帯にマイグレーションを実行する。 すべての予防策を講じても、問題がユーザーに影響を与える前に発見するためのバッファを確保する。

具体的な教訓

成功したマイグレーションとは、エラーなく実行されるものではない。成功したマイグレーションとは、今も、1時間後も、午前3時に夜間レポートが実行されるときも、何も壊さないものだ。すべてのスキーマ変更を潜在的な時限爆弾として扱い、明白なシステムだけでなく、すべてのシステムが新しい構造を処理できることを確認してから、マイグレーションを完了したと見なすこと。