8月29日、Elisが「A safe MySQL upgrade that wasn't so safe」と題した記事を公開した。「安全なはず」だったMySQLのレプリカ切り替えアップグレードが、なぜデータ不整合を引き起こしたかを詳細に振り返ったポストモーテムだ。
「確認済みで切り替えた」のに1時間後に壊れた
手順は教科書通りだった。AWSが2024年以降MySQL 5.7のRDSインスタンスに対して延長サポート料金(通常料金に加算)を課し始めたことへの対応として、MySQLのバージョンアップが必要になった。そこでBlue/Greenデプロイの要領でグリーンレプリカ(本番と並行稼働させる新バージョンのインスタンス)を立ち上げ、動作確認後に切り替えた。
しかし切り替えから1時間後、不審なバグ報告が届く。調査すると、とある「テーブルX」のIDが完全に入れ替わっていた。旧データベースでID=1だった行が、新データベースではID=26になっている。さらに、このテーブルXを参照する6テーブルのうち5テーブルは新IDを正しく参照しているのに、1テーブルだけ旧IDのままだった。旧IDは今や別の行を指しており、データが静かに壊れていた。
問題の根っこ:AUTO_INCREMENTとレプリケーションの罠
事の発端は、アップグレード前に実行されたマイグレーションだ。テーブルXに新しいAUTO_INCREMENT主キーを追加し、それに続けて6テーブルの外部参照を新しいidに向け直した。
-- テーブルXに新しいAUTO_INCREMENT主キーを追加
ALTER TABLE X ADD COLUMN id INT NOT NULL AUTO_INCREMENT PRIMARY KEY;
-- 各参照テーブルの外部キーを新idに更新
UPDATE some_table
JOIN x
ON x.old_id = some_table.x_old_id
SET some_table.x_id = x.id;
ここに落とし穴があった。MySQLの公式ドキュメントには、ALTER TABLEでAUTO_INCREMENT列を追加した場合、ソースとレプリカでIDの割り当て順が異なる可能性があると明記されている。順序はストレージエンジンや行の処理順に依存するためで、同じSQLを実行しても両者のIDが一致する保証はない。つまりマイグレーションを実行した時点で、ソース(旧本番)とグリーンレプリカ(新本番)のテーブルXのIDはすでにズレていた可能性がある。
バイナリログの「MIXED」モードが完結させた悪夢
6テーブルのうち5つが正しく、1つだけ壊れた理由は、MySQLのバイナリログフォーマットにある。バイナリログはレプリカへ変更を伝える仕組みで、3つのフォーマットが存在する。
- **
STATEMENT**:SQLステートメントそのものをログに記録し、レプリカで再実行する - **
ROW**:変更された行データをログに記録し、レプリカに直接適用する - **
MIXED**:通常はSTATEMENT、特定条件では自動的にROWに切り替わる
今回の環境ではbinlog_formatがMIXEDに設定されていた。
5テーブルの更新はSTATEMENTモードで複製された。レプリカ上でUPDATE文が再実行されるため、レプリカ側のテーブルXのid(レプリカ独自の値)を正しく参照できた。
しかし残り1テーブルについては、MySQLが自動的にROWモードを選択した。この場合、レプリカはUPDATE文を再実行しない。ソース側で生成済みの行データをそのまま受け取って適用する。つまり、ソース側のIDがレプリカにコピーされた。ところがレプリカのテーブルXのIDはソースと異なるため、まったく別の行を指すことになった。
なぜその1テーブルだけROWモードになったのか。他の5テーブルとの違いを調べると、そのテーブル自体にAUTO_INCREMENT列があったという点だけが異なっていた。MySQLはAUTO_INCREMENTを含む操作の一部を「ステートメントベースレプリケーションでは安全でない」と判断し、MIXEDモード下では自動的にROWへ切り替える仕様になっている。
皮肉なことに、問題の発端もAUTO_INCREMENT、トリガーもAUTO_INCREMENTだった。
教訓:「確認済み」をすり抜けるエッジケース
この障害の怖さは、確認済みのレプリカで切り替えたにもかかわらず発生した点にある。データ不整合はすぐには表面化せず、1時間後にようやくバグ報告として浮かび上がった。事前の動作確認をすり抜けるタイプの不整合は、発見が遅れるほど影響範囲が広がる。
整理すると、チェックすべきポイントは以下の通りだ。
- レプリケーション環境で
ALTER TABLEによるAUTO_INCREMENT列追加を行う際は、ソースとレプリカでIDが一致しているか明示的に検証する binlog_format=MIXEDの環境では、どのステートメントがROWモードにフォールバックするかを事前に把握しておく- 切り替え後のデータ整合性チェックは、外部キー参照先のIDレベルまで確認する
「確認済みだから安全」という前提を崩しうるエッジケースが、MySQLのレプリケーション仕様には潜んでいる。
詳細はA safe MySQL upgrade that wasn't so safeを参照していただきたい。