複数の起動経路、コンテナ、またはスケジューラーが同じアップグレード手順を自分の担当だと認識すると、データベースのマイグレーションが2回実行されることがあります。
セルフホスト型アプリケーションでは、entrypoint、Webプロセス、ワーカー、サイドカー、systemdユニット、デプロイフックなどからマイグレーションを開始することがよくあります。再デプロイ後に古いコンテナと新しいコンテナが重複して動作したり、再起動ポリシーによって失敗したマイグレーターが再実行されたり、いずれか一方が完了を記録する前に2つのレプリカがデータベースへ到達したりする場合があります。診断では、実行を開始し得るすべてのプロセスを特定し、データ修復を始める前に、マイグレーションフレームワークが永続的なロックまたはスキーマ履歴レコードを使用していることを確認する必要があります。
マイグレーションを開始できるすべてのプロセスを特定する
イメージのentrypoint、Composeコマンド、ワーカーコマンド、systemdユニット、cronジョブ、デプロイスクリプト、アプリケーションの起動ログから、マイグレーションコマンドを検索します。2回の実行時点におけるプロセスIDとコンテナ名を記録します。
Docker Composeは、宣言された依存関係に従ってサービスを起動できますが、起動順序だけでは、アプリケーションレベルのマイグレーション所有者を1つに限定できません。公式の起動順序に関するガイダンスが示すように、データベースが利用可能であることと、マイグレーションの実行担当が一意に定まっていることは別の条件です。
同じコマンドがWebのentrypointと専用のマイグレーションサービスの両方に存在する場合は、どちらか一方を担当から外します。コマンドが1つしか存在しない場合は、レプリカ、再起動ループ、マイグレーション状態の記録を確認します。
古いコンテナと新しいコンテナが重複していないか確認する
デプロイ中に、実行中、再起動中、終了済み、孤立状態のコンテナを一覧表示します。プロジェクト名、サービス名、コンテナID、作成日時を比較します。
systemdのドキュメントでは、サービスの再起動ポリシーにより、ユニット設定に従って失敗したコマンドを再実行できることが説明されています。そのサービス再起動モデルにより、コンテナ内での試行がゼロ以外の終了コードで終了した後、ホスト側のランチャーがマイグレーションを再度実行する理由を説明できます。
孤立した実行プロセスであることを確認した場合でも、ログを保存してから削除します。最初の実行直後に2回目のマイグレーション日時が記録されている場合、多くは別のスケジュールジョブではなく、再試行を示しています。
スキーマ変更を適用する前にデータベースロックを使用する
アプリケーションがマイグレーション状態を読み取り、変更を適用する前に、データベースレベルのロックを取得しているか確認します。使い捨て環境で、同時に2つの起動試行を実行してテストします。
PostgreSQLにはアプリケーション定義の調整に使えるアドバイザリーロックがあり、ほぼ同時に起動した場合でも、一方のマイグレーション実行プロセスがもう一方を排除できます。
ロックは、判断から実行までの全期間をカバーする必要があります。ロックを取得する前に現在のスキーマバージョンを確認するだけでは、2つの実行プロセスが同じ保留中のマイグレーションを選択することを防げません。
MySQLまたはMariaDBでロックの動作を確認する
MySQL互換アプリケーションでは、マイグレーションツールが名前付きロック、トランザクション、ロックテーブルのいずれを使用しているか、またマイグレーション全体を通じて接続が維持されているかを確認します。
MySQLには、接続単位の名前付きロックがあり、所有セッションが終了すると解放されます。そのため、クラッシュや再起動後に安全に再取得できる仕組みが必要です。
接続に失敗すると、マイグレーションフレームワークが完了を記録する前にロックが解放されることがあります。この流れを特定するには、データベースログとコンテナの再起動日時を比較します。
マイグレーションフレームワークの履歴テーブルを確認する
マイグレーションID、実行順序、成功フラグ、チェックサム、タイムスタンプを一覧表示します。ログに記録された2回の実行と、実際にデータベースへコミットされたレコードを比較します。
Flywayは、適用済みマイグレーションとその状態を追跡するスキーマ履歴テーブルを使用します。
最初の実行でスキーマが変更されたものの、成功を記録する前に失敗した場合、2回目の実行で、べき等性を持たないマイグレーションが再試行されることがあります。履歴を修復するのは、実際のスキーマとマイグレーションが想定する結果を比較した後にしてください。
変更履歴の識別情報とチェックサムの変更を確認する
イメージの更新前後で、マイグレーションのファイル名、ID、作成者、パス、チェックサムを比較します。イメージに重複した変更履歴エントリや、名前を変更された変更履歴エントリが含まれていないか確認します。
Liquibaseは、実行済みの変更をDATABASECHANGELOGテーブルに記録します。変更の識別情報は、ID、作成者、ファイルパスによって決まります。
変更履歴ファイルを移動したり、識別子を再生成したりすると、SQLが似ていても過去の処理が新しいものとして扱われることがあります。広範囲の履歴を手動で削除するのではなく、安定したマイグレーション識別情報を復元します。
マイグレーションの担当を1つにして再デプロイし、べき等性を確認する
マイグレーションの担当を1つに決め、永続的なロックを追加し、Webサービスとワーカーサービスが正常な完了を待機するようにして、データベースのテストコピーへ再デプロイします。
ZimaSpaceのコンテナのスケジューリング境界に関する記事では、関連するルールとして、1つのメンテナンスタスクには、実証済みの実行担当を1つだけ割り当てるべきだと説明しています。
同時または繰り返しの起動によって、適用済みマイグレーションが1つ、永続的な履歴レコードが1つだけ生成され、再起動後に2回目のスキーマ変更が発生しなくなれば、問題は解決しています。
よくある質問
マイグレーションを2回実行すると、必ずデータベースが壊れますか?
いいえ。べき等性のあるマイグレーションであれば、既存のオブジェクトを安全に検出できる場合があります。ただし、べき等性のないデータ変換、インデックス作成、カラム変更では、エラーやデータの重複が発生することがあります。
すべてのWebレプリカでマイグレーションを実行できるようにすべきですか?
フレームワークが信頼できるデータベースレベルの調整機能を提供している場合に限ります。ホームサーバー環境では、専用のワンショット型マイグレーション担当を用意した方が監査しやすくなります。
マイグレーションを手動で完了済みにしてもよいですか?
稼働中のスキーマとデータが、マイグレーションの想定結果と一致していることを確認した後に限ります。先に履歴を編集すると、部分的に適用された変更が隠れてしまう可能性があります。
サポートとヒント
もっと読む

Dockerボリュームの復元でファイルの内容は再現されるのに、拡張属性が失われるのはなぜですか?
xattr のインベントリ、tar と Rsync のオプション、名前空間、保存先のサポート状況、権限、ラベル、アプリメタデータ、テストを網羅したボリューム復元の診断。

Composeファイルを変更した後も、実行中のコンテナが古いメモリ制限を維持するのはなぜですか?
ライブcgroup、再起動と再作成の違い、Composeのフィールド、ハードリミットとソフトリミット、親スコープ、スワップ、ランタイムヒープを網羅したメモリ制限の診断。

リバースプロキシを再起動すると、1つのセルフホストアプリですべてのセッションが無効になるのはなぜですか?
再起動の範囲、Cookieの所有権、シークレットのローテーション、キャッシュを利用したセッション、スティッキールーティング、認証ゲートウェイ、復旧を網羅したセッション喪失の診断。

