コンテンツへスキップ
DedicatedPHP 接触

PHPでダウンタイムなしのデータベース移行

一時的な互換性、再開可能な移行、データ検証、運用上のロールバックにより、PHPスキーマを進化させる方法を学びます。

PHPアプリケーションのデータベーススキーマを変更するための、拡張・移行・撤去フェーズを示す編集用ダイアグラム

スキーマ移行は、コード変更がテストを通過していても失敗する可能性があります。本番環境では、アプリケーションが一度に切り替わることは通常ありません。異なるバージョンを実行するWebプロセス、キューワーカー、スケジュールタスク、レプリカが共存する場合があります。新バージョンが古いワーカーがまだ読み取るカラムを削除したり、すべての書き込み側が値を設定する前にカラムを必須にしたりすると、デプロイは互換性を失います。

PHPでダウンタイムなしのデータベース移行では、スキーマとデータを運用契約の構成要素として扱います。目的は正しいDDL文を実行することだけではなく、旧バージョンと新バージョンが共存する間も読み取りと書き込みを利用可能に保ち、現実的な復旧経路を維持することです。

スキーマがテスト済みコードを壊す理由

スキーマがテスト済みコードを壊す理由 — guía visual de DedicatedPHP

ローカルテストは通常、ゼロから作成されたデータベース、または即時に更新されたデータベースを前提とします。このシナリオでは、移行過程、すなわち不完全な履歴データ、数百万行、ロック、永続接続、非同期コンシューマーが考慮されません。一見小さな変更でも、エラーや性能劣化を引き起こす可能性があります。

  • カラム名の変更または削除は、以前の名前をまだ使用しているクエリ、ORMマッパー、レポート、プロセスを壊します。
  • NOT NULL制約の追加は、値のない古い行が存在する場合、または書き込み側がまだ新フィールドを認識していない場合に失敗します。
  • 型の変更は、値の切り捨て、比較の変更、インデックスの無効化、または高コストな変換を引き起こす可能性があります。
  • インデックスの作成や大きなテーブルの再書き込みは、ロックを保持し、通常の操作のレイテンシを増加させる可能性があります。
  • 単一トランザクションでの一括更新は、トランザクションログを枯渇させ、リソース競合を起こし、レプリケーションを困難にする可能性があります。

重要な問いは、デプロイウィンドウ全体にわたり、各データ表現をどのコードバージョンが読み書きできるか、です。答えには、HTTPリクエストだけでなく、自動的には再起動されない実行ファイルも含めなければなりません。

コード、データ、プロセス間の一時的な互換性

段階的デプロイの間には、少なくとも互換性を保つべき3つの状態があります。旧コード、新コード、そして旧形式・新形式・部分的に変換済みの形式のデータです。互換性とは、すべてのコンシューマーが永遠にすべての形式を理解することでは必ずしもありません。予測可能な組み合わせが機能する、限定されたウィンドウを定義することです。

たとえば、full_namefirst_namelast_nameに置き換える場合、最初に元のフィールドを削除するべきではありません。新バージョンは両方の形式を書き込み、新フィールドが完全に揃っている場合はそれらを優先して読み取り、旧値への明示的なフォールバックを持てます。旧バージョンはfull_nameで引き続き動作します。履歴データが変換され、古いコンシューマーが撤去された後に、読み取りを新しい構造だけに依存させられます。

一時的な互換性をコントローラーに分散させないでください。読み取り、書き込み、正規化はドメインサービスまたはリポジトリに集約します。これにより、どの形式バージョンが生成されるか、どの値が優先されるか、一時的なロジックをいつ撤去するかを監査できます。移行テンプレートはこの互換性モデルの代わりにはなりません。テンプレートは変更を実行しますが、モデルは移行中にアプリケーションがどのように振る舞うかを定義します。

拡張、移行、撤去のパターン

1. 現在のコンシューマーを無効化せずに拡張する

最初のフェーズでは、既存の機能を削除せずに能力を追加します。nullableなカラム、新しいテーブル、追加インデックス、または並行構造です。破壊的な変更を避け、データベースエンジンが必要とする場合は、ロックを抑えるための作成方法を計画しなければなりません。カラムの追加は、直ちにデフォルト値を強制したり、すべての行を再計算したり、必須と宣言したりしても安全であることを意味しません。

操作を実行する前に、テーブルサイズ、最も頻繁なクエリ、外部キー、利用可能な容量、レプリケーション負荷、データベースエンジン固有の挙動を確認してください。代表的なコピー、または同等のデータ量と同時実行性を持つ環境で試行します。また、観測可能な制限、すなわち所要時間、許容レイテンシ、エラー率、中止条件も定義してください。

2. 互換性のある書き込み側と読み取り側を公開する

次に、両方の表現を理解するコードをデプロイします。コストと整合性が許容する場合、新しい書き込み側は二重書き込みを行えます。読み取り側は明確な優先順位を定めなければなりません。新しい値が検証済みならそれを読み取り、そうでなければ古い値を使用します。例外をフォールバック機構として使用しないでください。データ欠陥を隠し、クリティカルパスに不要な処理を追加するためです。

二重書き込みには明示的な判断が必要です。更新が両方の構造に影響する場合、同一トランザクションで実行すべきかを決定してください。不可能な場合は、冪等な照合と、不一致を検出するメトリクスを設計します。イベント、キャッシュ、API、エクスポートもコンシューマーです。PHPリポジトリだけを変更しても、エンドツーエンドの互換性は保証されません。

3. 履歴データを再開可能な形で移行する

互換コードを有効にした後、既存レコードを小さなバッチで変換します。各バッチは、効果を重複させたりデータを破損させたりせずに再実行できなければなりません。安定したキーまたは永続カーソル、サイズ制限、進捗記録、制御されたリトライを使用してください。変更される集合に対するオフセットページネーションは、行のスキップまたは再処理を招くため避けてください。

$lastId = 0; // 正で増加する主キーの場合。

while (true) {
    $rows = $repository->findPendingAfterId($lastId, 500);

    if ($rows === []) {
        break;
    }

    foreach ($rows as $row) {
        $repository->migrateIfNeeded($row);
        $lastId = $row->id;
    }
}

このパターンでは、findPendingAfterId()がカーソルとして使用するのと同じキーで昇順に並んだ行を返す必要があります。カーソルは最初の有効な識別子より前の値から開始し、各行を処理した後にのみ進みます。終了は、クエリがバッチを返さなくなることに依存します。再開した実行では、確定済みの$lastId値を永続化しなければなりません。migrateIfNeeded()は現在の状態を検証し、再実行しても同じ結果を生成しなければなりません。

未処理行、変換済み行、検証エラー、形式間の差異を計測してください。テーブルを走査しただけでフェーズ完了と宣言してはなりません。参照整合性、一意性、業務上の合計値、重要レコードのサンプルも確認してください。

4. 読み取りを切り替え、観測し、撤去する

履歴データが完了し、古いプロセスが実行されなくなったら、読み取りを新しい構造だけ使用するよう切り替えます。この有効化は制御された設定により段階的に行えますが、デプロイと混同してはなりません。デプロイはコードを利用可能にし、有効化はトラフィックが使用する経路を変更します。

クエリエラー、予期しないnullフィールド、機能上の不一致、応答時間、ワーカーの健全性を観測してください。定義された観測ウィンドウの後にのみ、二重書き込み、一時的な依存関係、そして最終的に古いカラム、インデックス、テーブルを撤去します。廃止構造を無期限に残すと曖昧さとコストが増えます。一方、早すぎる削除は容易な復旧を失わせます。

運用を止めないnull、型、制約、インデックス

新しいカラムは、履歴レコードがまだその値を持たないため、通常はnullableから始めます。アプリケーションは、値の欠如をあり得ないケースではなく、想定された状態として扱わなければなりません。バックフィルを完了して検証した後、すべてのアクティブな書き込み側が有効な値を提供するなら、制約を強制できます。

型変更では、新しいカラムを作成し、値を明示的に変換してください。これにより、変換不能な値を検出し、丸めや正規化のルールを適用し、以前のカラムを置き換える前に両方の結果を比較できます。直接的な型変更は限定的なケースでは適切な場合がありますが、エンジンの挙動、データ量、クエリの互換性に基づいて正当化しなければなりません。

インデックスにも同等の分析が必要です。新しいインデックスは読み取りを改善する可能性がありますが、その構築にはリソースが必要であり、不適切な作成戦略は書き込みをブロックする可能性があります。必要とするクエリの実行計画を検証してください。直感だけでインデックスを追加してはなりません。エンジンが低ロックの作成モードを提供する場合、計画に組み込む前にその要件と制限を理解してください。

ロールバック:コードの復帰が常にデータの復帰を意味するわけではない

運用上のロールバックは、複数の判断に分けなければなりません。古い構造と二重書き込みが存在する間は、通常、以前のコードへ戻すことが可能です。しかし、新しい形式が旧モデルでは表現できない情報を受け入れている場合、スキーマを元に戻してもそのデータを意味的に復元することはできません。

  • 可逆的: 両方の構造を維持したまま、新しい読み取りを無効化してフォールバックに戻すこと。
  • 補償可能: 定義済みのソースから、監査されたプロセスでデータを修正または再構築すること。
  • 不可逆: 構造を削除すること、または元データを保持せずに精度を失う変換を受け入れること。

ポイント・オブ・ノーリターン、その承認責任者、必要なバックアップまたはエクスポート、ワーカーを停止する手順を文書化してください。移行ツールのdown()メソッドは、それ自体でロールバック計画ではありません。DDLを戻せる場合はありますが、移行中に書き込まれたデータの有効性を保証するものではありません。

テスト、証拠、チェックリスト

テスト、証拠、チェックリスト — guía visual de DedicatedPHP

互換性マトリクスをテストしてください。拡張済みスキーマと旧コード、未移行データと新コード、変換済みデータと新コード、バージョンが混在する非同期プロセスです。中断・再開された移行、無効なレコード、書き込みの同時実行性、該当する場合は以前のバージョンの復元も含めます。

  • 影響を受けるテーブル、クエリ、ワーカー、統合、レポートを棚卸しする。
  • null値と優先順位を含む、一時的な読み書き契約を定義する。
  • 拡張、互換デプロイ、バックフィル、有効化、撤去を独立したステップに分離する。
  • 代表的なデータでDDL、インデックス、バッチの影響を見積もる。
  • データプロセスを冪等、再開可能、測定可能にする。
  • 整合性検証と事後観測のしきい値を定める。
  • ロールバック、補償、ポイント・オブ・ノーリターンを文書化する。
  • コンシューマーが残っていないことの証拠がある場合にのみ、古い互換性と構造を撤去する。

規律をもって適用すれば、このパターンは高リスクなデータベース変更を検証可能な一連の手順に変えます。鍵は、共存を移行内に隠れた詳細ではなく、プロダクトと運用の一部として設計することです。

これらのアイデアをあなたのプロジェクトに活用してみませんか?あなたのPHPプラットフォームについて話し合いましょう。
関連サービスを見る