データベースデプロイが特別な理由:見えない依存関係の網

何年も稼働している本番データベースがあるとしよう。ある日、ordersテーブルにカラムを追加する必要が生じた。単純な作業に思える。メインアプリケーションは既知のカラムしか読み取らないので、新しいカラムを追加しても問題ないはずだ。

ところが、夜間バッチジョブが失敗する。週次レポートが異常な値を出力し始める。他チームが管理するサービスが、見たことのないエラーを突然吐き出す。

何が起きたのか? たった1つのテーブルを変更しただけで、すべてが壊れたのだ。

データベースの利用者は想像以上に多い

多くの組織では、単一のデータベースが1つのアプリケーションだけに使われることは稀だ。時間とともに利用者が増えていく。把握しているものもあれば、忘れ去られたものもある。何年も前に退職したメンバーが構築したものもある。

典型的な本番データベースが支えているものは次のようなものだ:

以下の図は、単一の本番データベースが、それぞれ異なるアクセスパターンを持つ多数のコンシューマにどのように接続されているかを示している:

flowchart TD DB[Production Database] DB -->|reads/writes| Web[Main Web Application] DB -->|reads/writes| API[Internal API Services] DB -->|writes| Batch[Nightly Batch Jobs] DB -->|reads| Report[Weekly Reporting Scripts] DB -->|reads| AdHoc[Ad-hoc Queries - Data Team] DB -->|reads/writes| Legacy[Legacy Services]
  • 顧客が利用するメインのWebアプリケーション
  • 他チームが管理する内部APIサービス
  • 数千行を処理する夜間バッチジョブ
  • ダッシュボードにデータを供給する週次レポートスクリプト
  • データチームやビジネスアナリストが実行するアドホッククエリ
  • 誰も触りたくないが、まだ動いているレガシーサービス

これらのコンシューマはそれぞれ異なる方法でデータベースにアクセスする。Webアプリケーションはstatusカラムを読んでページを表示するかもしれない。バッチジョブはINSERT ... SELECT *で数千行を書き込むかもしれない。レポートスクリプトは複数のテーブルを結合する特定のビューに依存しているかもしれない。アナリストはカラムの順序に依存した保存クエリを持っているかもしれない。

スキーマを変更するとき、あなたは自分のアプリケーションのためだけに変更しているわけではない。すべてのコンシューマのために変更しているのだ。

本当の問題:このデータベースに誰が依存しているか分からない

データベーススキーマ変更の最も難しい部分は、技術的な作業ではない。発見の問題だ。

ドキュメントは不完全か古くなっていることが多い。もはやアクティブではないアプリケーションのコードが、まだコンシューマとしてリストされているかもしれない。年に1回しか実行されないバッチジョブは見落としやすい。他チームが管理するサービスは、変更を行うチームとコミュニケーションを取っていないかもしれない。

完全に理解していないものを安全に変更することはできない。

後方互換性が重要な理由

スキーマ変更における最も安全なアプローチは、後方互換性を確保することだ。つまり、まだ更新されていない既存のコードを壊さない変更でなければならない。

リスクを減らす実践的なパターンを紹介する:

新しいカラムはNULL許容にするか、デフォルト値を持つべきだ。 デフォルト値なしの必須カラムは、それを含まないINSERT文をすべて壊す。必須カラムを追加する必要がある場合は、まずNULL許容で追加し、データをバックフィルしてから、別の変更で必須に変更する。

カラムの型を直接変更するのは避ける。 代わりに、新しい型のカラムを追加し、データを段階的に移行し、すべてのコンシューマを新しいカラムに更新してから、古いカラムを削除する。このプロセスには数週間から数ヶ月かかるかもしれないが、ダウンタイムを防ぐことができる。

SELECT *には注意する。 すべてのカラムを選択し、結果を固定データ構造にマッピングするクエリは、新しいカラムが現れると壊れる。コンシューマコードを制御できるなら、必要なカラムだけを選択するように変更する。コードを制御できないなら、所有チームと調整する必要がある。

確認なしにカラムやテーブルを削除してはいけない。 使われていないように見えるカラムやテーブルが、四半期ごとに実行されるスクリプトから参照されているかもしれない。クエリログやアプリケーションコードを確認し、削除する前に他のチームと話し合う。

忘れられたコンシューマ:人間

アプリケーションやサービスだけがデータベースのコンシューマではない。人間も手動でクエリを実行する。

データチームは毎月の役員報告書を生成するスクリプトを持っているかもしれない。ビジネスアナリストは毎週月曜の朝に顧客データを取得するデータベースツールの保存クエリを持っているかもしれない。運用チームはインシデント対応中にアドホッククエリを実行するかもしれない。

カラム名を変更したりテーブルを削除したりすると、これらの手動クエリは静かに壊れる。レポートが間違って出力されるか、障害時に誰かが必要なデータを見つけられなくなるまで誰も気づかない。

これらの人間のコンシューマは、コードリポジトリに現れないことが多いため、追跡が難しい。最善の防御策はコミュニケーションだ。スキーマ変更を行う前に、影響を受ける可能性のあるチームに通知を送る。懸念を提起する期限を設定する。変更内容を誰でも見つけられる場所に文書化する。

データベースのロールバックはアプリケーションのロールバックとは異なる

アプリケーションのデプロイに失敗した場合、以前のバージョンにロールバックできる。古いコードが新しいコードを置き換え、システムは回復する。

データベースの変更はこのようには機能しない。

カラムを追加してロールバックする必要が生じても、カラムは自動的には消えない。そのカラムに書き込まれたデータは残る。カラムの型を変更してロールバックすると、既存のデータが古い型に合わなくなる可能性がある。カラムを削除すると、バックアップがない限りデータは失われる。

さらに悪いことに、問題のあるデータベース変更はすべてのコンシューマに同時に影響する。アプリケーションのロールバックはそのアプリケーションのユーザーだけに影響するが、データベースのロールバックはデータベースに触れるすべてのサービス、スクリプト、人に波及する。

これが、データベースデプロイにアプリケーションデプロイよりも多くの計画、テスト、調整が必要な理由だ。爆発半径は大きく、復旧オプションは限られている。

次のスキーマ変更前の実践的チェックリスト

本番でALTER TABLEを実行する前に、このリストを確認しよう:

  • このテーブルにアクセスするすべてのアプリケーション、サービス、スクリプトをリストアップする
  • 壊れる可能性のあるSELECT *クエリをチェックする
  • 新しいカラムにデフォルト値があるか、NULL許容であることを確認する
  • 削除予定のカラムやテーブルがまだ使用されていないことを確認する
  • このテーブルに対して手動クエリを実行する可能性のあるチームに通知する
  • 変更中に書き込まれたデータを考慮したロールバック計画を用意する
  • 本番コンシューマを模したステージング環境で変更をテストする

まとめ

データベーススキーマ変更は、単なるデータベースの問題ではない。そのデータベースに接続されたすべてのコンシューマに影響する調整の問題だ。コンシューマが多ければ多いほど、より注意深くなる必要がある。後方互換性、徹底的な発見、明確なコミュニケーションはオプションではない。それらは、スムーズなデプロイと、複数のシステムを同時にダウンさせる本番インシデントの違いを生む。