同期停止時の対応:Duplicate Entryエラーのトラブルシューティング
本番環境のアラートほど、静かな午後を台無しにするものはありません。つい先ほどまで正常に動作していたレプリカが、突然停止してしまうことがあります。レプリケーションが切れると、スレーブ(Slave)は即座に遅延し始めます。わずか30分の遅延でもレポート用ダッシュボードが役に立たなくなったり、トラフィックの多い環境では復旧に12時間かかるほどのバックログが発生し、解消が困難な状況に陥ることもあります。
監視スタックでアラートを検知した後、SQLスレッドがクラッシュした原因を調査するため、レプリカデータベースにログインしました。
エラーの特定
何が起きたのかを正確に把握するために、スレーブ(Replica)サーバーで以下のコマンドを実行します。
SHOW SLAVE STATUS\G
特に Slave_SQL_Running と Last_SQL_Error フィールドに注目してください。私の場合、出力結果は以下のようになっていました。
Slave_IO_Running: Yes
Slave_SQL_Running: No
Last_Errno: 1062
Last_Error: Could not execute Write_rows event on table my_database.users; Duplicate entry '123' for key 'PRIMARY', Error_code: 1062; handler error HA_ERR_FOUND_DUPP_KEY; the event's master log mysql-bin.000123, end_log_pos 4567
エラー内容は明白です。Duplicate entry '123' for key 'PRIMARY'。すでに存在するデータを書き込もうとしたため、レプリケーションプロセスが停止しました。
発生原因
このエラーは、マスター(Master)がスレーブにすでに存在するプライマリキー(ID 123)を持つ行を挿入しようとしたときに発生します。通常、以下の3つのいずれかが原因です。
- 帯域外の書き込み: 誰かがマスターではなく、スレーブに対して手動でデータを直接挿入した。
- 部分的な障害: 以前のクラッシュにより、データはディスクに書き込まれたが、レプリケーションログには記録されなかった。
- アプリケーションロジック: 設定ミスにより、アプリケーションが書き込み操作のためにスレーブのIPにアクセスしている。
解決方法
修正方法は、データの整合性と稼働時間のどちらを優先するかによって異なります。
方法1:クイックスキップ(非GTID環境)
スレーブ上のデータがすでに正しいことが確実な場合は、MySQLに失敗したトランザクションを無視するように指示できます。警告:この方法はGTIDベースのレプリケーションを使用していない場合にのみ機能します。
-- スレーブスレッドを停止
STOP SLAVE;
-- ちょうど1つのステートメントをスキップ
SET GLOBAL SQL_SLAVE_SKIP_COUNTER = 1;
-- スレーブスレッドを再開
START SLAVE;
再度 SHOW SLAVE STATUS\G を実行します。Slave_SQL_Running が Yes になっていれば、レプリカはバックログを処理しています。
方法2:データ整合性の修正(削除と再同期)
よりクリーンなアプローチは、スレーブから競合している行を削除することです。これにより、レプリケーションスレッドがマスターからの「公式な」バージョンを再挿入できるようになり、両方のサーバーの状態を完全に一致させることができます。
-- スレーブ側で競合している行を確認
SELECT * FROM my_database.users WHERE id = 123;
-- データが不要な場合は削除
DELETE FROM my_database.users WHERE id = 123;
-- レプリケーションを再開
STOP SLAVE;
START SLAVE;
方法3:GTIDレプリケーションでの対処
MySQLの設定でグローバルトランザクション識別子(GTID)を使用している場合、SQL_SLAVE_SKIP_COUNTER は失敗します。代わりに、空のトランザクションを注入して、失敗したIDの完了を「偽装」する必要があります。まず、SHOW SLAVE STATUS\G の出力から失敗しているGTIDを見つけます(例:5340062a-1c22-11ed-9662-0242ac120002:101)。
STOP SLAVE;
-- ブロックされたIDに対して空のトランザクションをコミット
SET GTID_NEXT = '5340062a-1c22-11ed-9662-0242ac120002:101';
BEGIN; COMMIT;
-- 通常の動作に戻す
SET GTID_NEXT = 'AUTOMATIC';
START SLAVE;
復旧の確認
修正を適用した後、Seconds_Behind_Master の値を監視します。スレーブがキューを処理するため最初は数値が跳ね上がるかもしれませんが、徐々にゼロに向かってカウントダウンが始まるはずです。Last_SQL_Error が空の文字列になっていることを確認してください。
教訓と予防策
エラーの修正は一時的なパッチに過ぎません。再発を防ぐために、環境を制限しましょう。
- スレーブを読み取り専用に設定する:
my.cnfを更新してread_only = 1を含めます。これにより、一般ユーザーによる手動の変更を防止できます。 - Super Read-Onlyを使用する: MySQL 5.7.8以降では、
super_read_only = 1を使用します。これにより、SUPER権限を持つユーザーであっても、誤って同期を壊してしまうのを防ぐことができます。 - アプリの監査: アプリケーションの接続文字列で、Writer(マスター)と Reader(スレーブ)のエンドポイントが明確に区別されていることを確認してください。

