インフラ・DevOps
安全だと思われたMySQLアップグレードがそうではなかった話
A safe MySQL upgrade that wasn't so safe (blog.elis.cc)
要約
MySQLのデータベースバージョンがEOL(サポート終了)を迎えたため、AWSの延長サポート料金を避けるためにアップグレードを実施しました。レプリカを先にアップグレードし、正常を確認してから本番に切り替えましたが、1時間後に奇妙なバグが報告されました。特定のテーブルのIDがソースとレプリカで異なる順序で割り当てられており、関連テーブルの参照がおかしくなっていました。これは、AUTO_INCREMENT列の追加とMySQLのレプリケーションフォーマット(MIXED)の組み合わせが原因でした。
全文翻訳
安全だと思われたMySQLアップグレードがそうではなかった話
公開日 2026年8月28日
データベースのバージョンがEOL(サポート終了)に達し、アップグレードが必要であるという通知を受け取りました。AWSの延長サポート料金は、できるだけ早くアップグレードするための良い動機となります。グリーンなレプリカ(正常に稼働しているレプリカ)を準備し、それをアップグレードして、すべてが正しく機能することを確認してから切り替えました。簡単で迅速、ですよね?私もそう思いました。しかし、すべてが正しかったわけではありません。
1時間後、奇妙なバグの報告を受け取り、データベースを調査したところ、特定のテーブル(テーブルXと呼びましょう)のIDが異なる順序で割り当てられていることが判明しました。以前のデータベースでID 1だった行が、新しいデータベースではID 26になっていました。このテーブルは他の6つのテーブルからも参照されていますが、そのうち5つは新しいIDを正しく参照していましたが、1つのテーブルは以前のデータベースのIDを誤って使用しており、それは完全に異なる行を参照していました。これは驚くべきことです。どうしてこんなにひどいことになったのでしょうか?
移行
アップグレードの少し前に、テーブルXに新しい自動インクリメント(AUTO_INCREMENT)の主キーを追加する移行が実行されました。
ALTER TABLE X ADD COLUMN id INT NOT NULL AUTO_INCREMENT PRIMARY KEY;
この移行では、6つの関連テーブルも更新され、古いIDではなく新しいIDを参照するようにしました。各テーブルについて、更新は概ね以下のようでした。
UPDATE some_table JOIN x ON x.old_id = some_table.x_old_id SET some_table.x_id = x.id;
AUTO_INCREMENT列
レプリケートされたテーブルにAUTO_INCREMENT列を追加すると、ソースとレプリカで異なるIDが割り当てられる可能性があることが判明しました。MySQLのレプリケーションとAUTO_INCREMENTに関するドキュメントによると、ALTER TABLEでAUTO_INCREMENT列を追加すると、ソースとレプリカで同じ行の順序が生成されない可能性があります。IDが割り当てられる順序は、ストレージエンジンと行が処理される順序に依存します。なるほど、それは知りませんでした。しかし、なぜこの新しいフィールドが6つのうち5つのテーブルでは正しく参照され、1つのテーブルでは完全に混乱していたのでしょうか?
バイナリログフォーマット
ここからさらに混乱します。MySQLのレプリケーションは、ソースで行われた変更を記録するために「バイナリログ」を使用します。これらの変更はレプリカに送信され、レプリカはそれらを使用して同じトランザクションを再現します。何が記録され、どのようにレプリカで適用されるかは、バイナリログの「フォーマット」に依存します。MySQLは3つの「フォーマット」をサポートしています。
STATEMENT:SQLステートメント自体がバイナリログに書き込まれ、レプリカはそのステートメントを実行します。
ROW:個々の行に加えられた変更がバイナリログに書き込まれ、それらの行の変更が直接レプリカに適用されます。
MIXED:MIXEDロギングでは、デフォルトでステートメントベースのロギングが使用されますが、特定のケースではロギングモードが自動的に行ベースに切り替わります。
私のソースデータベースでは、binlog_formatがMIXEDに設定されていました。そのため、5つのテーブルが正しい新しいIDを参照していた理由は、それらの更新ステートメントがSTATEMENTモードを使用してレプリケートされたためです。実行された正確なUPDATEステートメントがレプリカでも再度実行されました。レプリカのx.idのバージョンが検索され、各関連テーブルに正しいローカルIDが書き込まれました。しかし、残りの1つのテーブルについては、MySQLは代わりにROWモードを使用することを決定しました…そのモードでは、レプリカは元のUPDATEを実行しません。ソースからの行の変更結果を受け取り、それを直接適用します。そのため、ソースで生成されたx_idの値がレプリカにコピーされました。しかし、テーブルXはレプリカで異なるIDを持っていたため、それらの値は完全に異なる行を参照するようになりました。
なぜMySQLが1つのテーブルのためにだけROWモードを使用することにしたのか、疑問に思うかもしれません。このテーブルと他の5つのテーブルとの間に見つけられた唯一の目に見える違いは、このテーブルにAUTO_INCREMENT列があったことでした。MySQLには、ステートメントベースのレプリケーションにとって安全でないと見なされるAUTO_INCREMENTに関連する特定のケースがあり、そのためMIXEDモードではROWを使用してログに記録します。皮肉ですよね?結局すべてはAUTO_INCREMENT機能に行き着くようです。
結論
MySQLレプリカには注意してください。この種の問題の恐ろしい点は、予期せぬものであり、見逃しやすいですが、本番環境ではすぐに大惨事につながる可能性があり、どうしてこうなったのかと首をかしげることになります。