なぜデータベースデプロイをアプリケーションデプロイと同じように扱ってはいけないのか
あなたはECサイトを運営しています。午後の忙しい時間帯、ユーザーは商品を閲覧し、カートに追加し、チェックアウトを進めています。その最中、データベースチームがproductsテーブルにdiscount_priceカラムを追加するマイグレーションを実行しました。突然、サイトの動作が遅くなり、商品検索がタイムアウトし、チェックアウトが失敗し、ユーザーがSNSに苦情を投稿し始めます。
何が起きたのでしょうか?データベースはスキーマ変更中にテーブル構造を保護するためproductsテーブルをロックし、商品データの読み書きを必要とするすべてのクエリが待機状態になりました。アプリケーション自体は正常で、サーバーも健全でした。しかし、データベースはスキーマ変更中のデータ破損を防ぐために忙しかったのです。
このシナリオは、データベースデプロイをアプリケーションデプロイと同じように扱うチームでよく発生します。両者には根本的な違いがあります。アプリケーションは停止し、新しいバージョンに置き換え、数秒で再起動できます。しかしデータベースは変更中もユーザーにサービスを提供し続けなければなりません。
ロックの仕組みとその影響
テーブル構造を変更するコマンドを実行すると、データベースは変更中に他の操作が同じデータを変更しないことを保証する必要があります。これがデータベースの一貫性維持の仕組みです。これを強制するため、データベースはテーブルまたは特定の行にロックを取得します。そのロックが有効な間、同じテーブルに対してデータの読み書きを試みる他のクエリはすべて待機しなければなりません。
スキーマ変更の中には高速に完了するものもあります。例えばPostgreSQLでデフォルト値がNULLのカラムを追加する操作は、ミリ秒単位で完了し、読み取りをブロックしません。しかし、他の操作はそれほど優しくありません。大規模テーブルへのインデックス作成、カラムのデータ型変更、カラム削除などは、テーブルを数分から数時間ロックする可能性があります。
次の2つのSQL文の違いを考えてみてください。
-- 安全: NULL許容カラムを追加、ミリ秒で完了、ロックなし
ALTER TABLE products ADD COLUMN discount_price DECIMAL(10,2);
-- 危険: テーブル全体を再書き込み、大規模テーブルでは数分間ロック
ALTER TABLE products ALTER COLUMN price TYPE DECIMAL(12,2);
最初の文はNULLを許容するカラムを追加するため、データベースはメタデータのみを更新します。2番目の文は既存カラムのデータ型を変更するため、データベースはテーブルのすべての行を再書き込みする必要があります。再書き込み中、テーブルはロックされ、productsに対するすべてのクエリが待機します。
本当の危険は連鎖効果です。ロックを待つクエリは、単一の機能を遅くするだけではありません。同じテーブルに依存する他のクエリもブロックする可能性があります。極端な場合、すべてのデータベーススレッドがロック待ちのクエリで消費され、アプリケーションは完全に応答しなくなります。ユーザーには無限に回るローディングスピナーやタイムアウトエラーが表示されます。ユーザーから見ればアプリケーションはダウンしています。データベースは単に自分自身を保護しているだけなのです。
すべてのスキーマ変更が同じではない
データベースごとにロックの扱いは異なり、すべてのスキーマ操作が同じリスクを持つわけではありません。どの操作が安全で、どの操作が危険かを理解することは、データベースデプロイの計画に不可欠です。
PostgreSQLはNULLデフォルトのカラム追加を読み取りをブロックせずに実行できます。MySQLのONLINE DDL操作は、同時DMLのためにテーブルをロックせずに実行できますが、開始時と終了時に短いメタデータロックが必要です。「オンライン」や「ゼロダウンタイム」と謳う操作でも、本番を模した環境でテストする必要があります。
通常、最も問題を引き起こす操作:
- 大規模テーブルへのインデックス作成
- カラムのデータ型変更
- カラム削除
- 非NULLデフォルト値を持つカラム追加(一部のデータベース)
- カラムやテーブルの名前変更
- テーブル全体を再書き込みする
ALTER TABLE文
一般的に安全な操作:
NULLデフォルトのカラム追加(PostgreSQL)CONCURRENTLYを使用したインデックス追加(PostgreSQL)- 新しいテーブルの作成
ONLINEDDLを使用した新しいカラム追加(MySQL、サポートされる操作)
重要なのは、各操作が使用するデータベースシステムでどのカテゴリに該当するかを把握し、本番実行前にステージング環境で実際の実行時間をテストすることです。
ロールバックが想像以上に難しい理由
アプリケーションのロールバックは簡単です。以前のバージョンのコードをデプロイすれば、アプリケーションは古いロジックでリクエストを処理し始めます。データベースのロールバックはそうはいきません。
カラムを追加した後にロールバックする必要が生じた場合、カラムを「アンデプロイ」することはできません。削除するための別のマイグレーションを実行する必要があります。その削除操作自体がテーブルをロックする可能性があります。マイグレーションがデータ型を変更したりテーブルを再構築したりした場合、ロールバックにはデータを古い形式に戻す変換が必要になり、時間がかかりリスクが高まる可能性があります。
この非対称性は、リスクの考え方を変えます。アプリケーションでは迅速にデプロイし、問題があればロールバックできます。データベースでは、問題が発生するのを未然に防ぐ必要があります。なぜなら、復旧の道のりが困難だからです。
より安全なデータベースデプロイのための実践的戦略
データベースデプロイをうまく処理するチームは運に頼りません。ロック関連のインシデントの可能性を減らし、問題発生時の復旧を管理可能にするプロセスを構築しています。
トラフィックが少ない時間帯にマイグレーションをスケジュールする。 火曜日の午後2時にスキーマ変更を実行するのは問題を招く行為です。日曜日の午前2時など、アプリケーションのトラフィックが少ない時間帯にスケジュールしましょう。アプリケーションがグローバルユーザーにサービスを提供している場合、複数の低トラフィック時間帯に実行できる小さなステップに分割する必要があるかもしれません。
大きな変更は小さなステップに分割する。 3つのカラム追加、2つのインデックス作成、データ型変更を1つのマイグレーションで行うのではなく、個別のマイグレーションに分割しましょう。各マイグレーションは迅速に完了し、連鎖効果なくロールバックできる程度に小さくする必要があります。
ステージングで実行時間を測定する。 本番でマイグレーションを実行する前に、同様のデータ量とトラフィックパターンを持つステージング環境で実行します。ステージングで30秒かかるマイグレーションでも、本番の実データでは30分かかる可能性があります。測定し、それに応じて計画しましょう。
マイグレーション中のロック待ち時間を監視する。 クエリが数秒以上ロックを待ち始めたらアラートが発報するように設定します。ロック待ち時間が上昇しているのを確認したら、完全な停止を引き起こす前にマイグレーションを中断する手順が必要です。
明確な中断手順を用意する。 マイグレーションが長時間かかりすぎたり、ロック競合を引き起こしたりした場合に何をするかを明確に定義します。これには、マイグレーションプロセスの強制終了、以前のスキーマバージョンへのロールバック、またはマイグレーション完了中に読み取りレプリカへの切り替えが含まれる可能性があります。
データベースデプロイのための実践的チェックリスト
本番でスキーマ変更を実行する前に、このチェックリストを確認してください。
- この操作は、使用しているデータベースシステムで同時読み取り/書き込みに対して安全ですか?
- 同様のデータ量のステージング環境でマイグレーションをテストしましたか?
- ステージングテストに基づく推定実行時間は?
- マイグレーションはトラフィックが少ない時間帯にスケジュールされていますか?
- 別のリスクの高いマイグレーションを必要としないロールバック計画はありますか?
- ロック待ち時間の監視アラートは設定されていますか?
- 問題発生時の中断手順をチームは把握していますか?
核心的な違い
アプリケーションデプロイはコードの入れ替えです。データベースデプロイは、ユーザーにサービスを提供しながらライブデータを変換することです。これらは根本的に異なる操作であり、異なる戦略、異なるリスク評価、異なるロールバック計画を必要とします。
データベースデプロイで成功するチームは、この違いを尊重しています。彼らはアプリケーションコードをデプロイする同じパイプラインにマイグレーションステップを単に追加するのではありません。適切なセーフガード、テスト手順、監視を備えた個別のワークフローを設計しています。
次回データベース変更を計画するときは、まず「このマイグレーションが実行されている間、ユーザーはどうなるのか?」と自問してください。その答えが、デプロイの準備ができているかどうかを教えてくれます。