レプリケーションとは?バックアップとの違いをMySQLの実測で解説

実務レシピ

レプリケーションとは、あるサーバーのデータを別のサーバーへ自動で複製し続ける仕組みです。元になるサーバー(ソース/マスター)で行われた変更が、そのまま複製先のサーバー(レプリカ/スレーブ)へ流れ込み、両者が同じ内容を保ち続けます。障害時の切り替え先を用意する目的と、参照処理を分散させる目的で使われます。

ただ、言葉の説明だけを読むと「要するにバックアップでは?」という疑問が残ります。結論から言うと、レプリケーションはバックアップの代わりになりません。この記事では説明で終わらせず、MySQL 8.0でレプリケーションを実際に組み、誤って実行したDELETEが何ミリ秒でレプリカ側にも伝わるのかを計測しました。結果は8ミリ秒です。

source で DELETE した時刻   : 2026-08-02 15:27:25.471
replica から全行消えた時刻  : 2026-08-02 15:27:25.479
伝播までの時間: 8.0 ms

「気づいてレプリカを切り離す」猶予は事実上ありません。この記事では、この実測を軸にレプリケーションの意味・バックアップとの違い・種類を整理したうえで、Dockerで2台のMySQLを組んで動かし、遅延の実測値、監視値が嘘をつくケース、そして誤操作からデータを救い出す手順までを扱います。検証はすべてUbuntu上のDocker(mysql:8.0、実バージョン8.0.46)で行い、出力はそのまま貼っています。

  1. レプリケーションとは|データを別サーバーへ自動で複製し続ける仕組み
    1. 登場人物は「ソース」と「レプリカ」の2役
    2. データが流れる仕組み
    3. なぜレプリケーションを組むのか
  2. レプリケーションとバックアップの違い|8ミリ秒で両方から消えた
    1. 実測:誤操作の DELETE は8ミリ秒でレプリカにも届く
    2. 違いを整理する
    3. 冗長化・ミラーリングとの違い
  3. レプリケーションの種類
    1. 同期・準同期・非同期
    2. 物理レプリケーションと論理レプリケーション
    3. 構成のパターン
  4. DockerでMySQLレプリケーションを実際に組む
    1. ①2台を起動する
    2. ②レプリケーション専用ユーザーを作る
    3. ③レプリカを接続して開始する
    4. ④動いているか確認する
  5. どれくらい遅れるのか|遅延を実測する
    1. 計測方法の注意点
    2. 結果:遅延は7〜10ミリ秒
  6. 運用でつまずくポイント(実測で確認)
    1. ①rootは --read-only を素通りする
    2. ②差分ができるとエラー1062で止まり、遅延はNULLになる
    3. ③止まっていてもレプリカは古いデータを平然と返す
    4. ④非同期はコミット済みのトランザクションを失う
  7. 誤操作から救う:遅延レプリケーション
    1. SOURCE_DELAY を設定する
    2. 実測:60秒の猶予でデータは生き残る
    3. 救出時に踏むmysqldumpの罠
  8. 結局、何をどう組み合わせればよいか
  9. よくある質問
    1. レプリケーションとバックアップはどちらが必要ですか?
    2. レプリケーションの遅延はどれくらいですか?
    3. Seconds_Behind_Source が NULL になるのはなぜですか?
    4. レプリケーションが止まっていることに気づく方法は?
    5. レプリカに書き込んでしまったらどうなりますか?
    6. レプリケーションがあればデータは絶対に消えませんか?
    7. マスター・スレーブという呼び方は使ってよいですか?
    8. 遅延レプリケーションは何秒に設定すべきですか?
  10. まとめ
  11. 関連記事
  12. 関連記事

レプリケーションとは|データを別サーバーへ自動で複製し続ける仕組み

レプリケーション(replication)は英語で「複製」を意味します。IT分野ではデータベースやサーバーのデータを、別のサーバーへ継続的にコピーし続けることを指します。ポイントは「継続的」という部分です。1回コピーして終わりではなく、元のデータが変わるたびに複製先へ追従します。

登場人物は「ソース」と「レプリカ」の2役

基本の構成は2台です。書き込みを受け付ける側と、それをひたすら真似る側に分かれます。

役割新しい呼び方古い呼び方やること
複製元ソース(source)マスター(master)読み書きを受け付け、変更内容をログに記録する
複製先レプリカ(replica)スレーブ(slave)ソースのログを受け取り、同じ変更を自分にも適用する

MySQLでは8.0.22以降、masterslaveという用語がsourcereplicaに置き換えられました。ネット上の記事や社内手順書は古い用語のままのことも多いので、両方の呼び方を知っておくと混乱しません。なお古いコマンドも当面は動きます。

# 8.0 でも SHOW SLAVE STATUS はまだ動く(項目名も古いまま返る)
$ docker exec repl-replica mysql -uroot -e "SHOW SLAVE STATUS\G"
               Slave_IO_State: Waiting for source to send event
                  Master_Host: source
                  Master_User: repl
                  Master_Port: 3306

データが流れる仕組み

MySQLの場合、複製は「変更履歴のログを転送して、それを再生する」という形で行われます。流れは3段階です。

  1. ソースが、データの変更内容をバイナリログ(binlog)に書き出す
  2. レプリカのI/Oスレッドがそのログを受け取り、自分のディスクにリレーログとして保存する
  3. レプリカのSQLスレッドがリレーログを読んで、同じ変更を自分のデータへ適用する

この「I/Oスレッド」と「SQLスレッド」が別々に動いている点が重要です。後述しますが、片方だけが止まるケースがあり、そのときに監視値が素直な数字を返してくれません。トラブル対応の勘所はここに集中しています。

なぜレプリケーションを組むのか

目的内容効果
可用性の確保ソースが壊れたらレプリカを昇格させて運用を続ける停止時間を分単位まで短縮できる
参照の負荷分散集計や検索といった読み取りをレプリカへ回すソースの負荷を下げ、応答を安定させる
本番に触らない作業場重い集計やバッチ、解析をレプリカで実行する本番の性能に影響を与えずに済む

停止時間をどこまで減らせるかという話は可用性とは?稼働率の計算と高可用性の作り方で、稼働率の計算方法とあわせて整理しています。レプリケーションは「高可用性を実現するための一手段」という位置づけです。

レプリケーションとバックアップの違い|8ミリ秒で両方から消えた

「レプリケーションを組んでいるからバックアップは要らない」は、実務でいちばん危険な誤解です。理由を言葉で説明するより、実際に測ったほうが早いので測りました。

実測:誤操作の DELETE は8ミリ秒でレプリカにも届く

ソースとレプリカが同期している状態(両方11件)で、ソース側にWHEREを書き忘れたDELETEを打ちます。

=== 実験: source で全行 DELETE した瞬間、replica はどうなるか ===
実行前 source : 11
実行前 replica: 11

source で DELETE した時刻   : 2026-08-02 15:27:25.471
replica から全行消えた時刻  : 2026-08-02 15:27:25.479
伝播までの時間: 8.0 ms

実行後 replica: 0

8ミリ秒です。「あっ」と声を出す前に、複製先のデータも消えています。レプリケーションは「正しい操作」も「間違った操作」も区別せずに複製するので、人為ミス・アプリのバグ・ランサムウェアによる暗号化に対しては、複製先も同じ運命をたどります

一方でバックアップは「ある時点で切り取った、それ以降の変更を受け付けない静止画」です。だからこそ、事故が起きた後でも事故前の状態を取り出せます。

違いを整理する

観点レプリケーションバックアップ
データの状態常に最新に追従する取得した時点で固定される
守れる障害サーバー・ディスク・データセンターの障害上記に加えて誤操作・バグ・ウイルス
誤操作への耐性なし(8ミリ秒で複製先にも伝わる)あり(取得時点まで戻せる)
過去の任意の時点に戻すできないできる
復旧までの時間短い(切り替えるだけ)長い(リストア+リカバリが必要)
保管コスト高い(常時稼働のサーバーが必要)低い(安価なストレージに置ける)

つまり両者は代替関係ではなく、守る対象が違う補完関係です。レプリケーションで「サーバーが壊れる事故」に備え、バックアップで「人とソフトウェアがやらかす事故」に備えます。実際のバックアップ設計はクラウド時代のDBバックアップ入門|mysqldump+S3+ライフサイクル定期バックアップ講座|rsync×世代管理×復元テストにまとめています。

冗長化・ミラーリングとの違い

似た用語がいくつかあり、混同されがちです。粒度が違うと考えると整理しやすくなります。

用語指すもの関係
冗長化予備を用意して落ちても動き続けるようにする設計思想の総称いちばん広い概念
レプリケーション別サーバーへデータを複製し続ける手法冗長化の実現手段のひとつ
ミラーリング(RAID 1)同一サーバー内の2本のディスクへ同時に書く手法ディスク故障のみに対応

ミラーリングはディスクが壊れても止まりませんが、サーバー本体や設置場所ごと駄目になると無力です。レプリケーションはネットワーク越しの別筐体・別拠点に置けるので、より広い範囲の障害に対応できます。その代わりサーバーが2台分必要になります。

レプリケーションの種類

同期・準同期・非同期

「ソースはどこまで待ってから、アプリに『コミット成功』と返すか」による分類です。ここが性能とデータ保全のトレードオフになります。

方式ソースが待つ範囲性能障害時のデータ消失
同期レプリカが適用し終えるまで待つ遅いなし
準同期(semi-sync)レプリカがログを受け取るまで待つ中間ほぼなし
非同期待たない速いあり

MySQLの既定は非同期です。何も設定しなければ非同期になっているので、「レプリケーションがあるからデータは絶対に消えない」という理解は誤りです。これも後ほど実測で確認します。

物理レプリケーションと論理レプリケーション

「何を転送するか」による分類です。

方式転送するもの特徴
物理レプリケーションディスクへの書き込みそのもの(ブロック単位)設定が単純で速い。両側が同じ構成・同じバージョンである必要がある
論理レプリケーション「どの行がどう変わったか」という論理的な変更内容テーブル単位で選べる。異なるバージョンや異種DB間でも組める。設定は複雑

MySQLのバイナリログを使った標準のレプリケーションは論理レプリケーションにあたります。この記事で組むのもこの方式です。

構成のパターン

  • シングルソース:ソース1台にレプリカが複数ぶら下がる。最も一般的
  • マルチソース:1台のレプリカが複数のソースから受け取る。分散したDBの集約に使う
  • カスケード:レプリカがさらに別のレプリカへ中継する。ソースの負荷を抑えられる
  • 双方向(マルチマスター):どちらにも書ける。競合の解決が難しく、扱いには注意が要る

DockerでMySQLレプリケーションを実際に組む

ここからは手を動かします。本番サーバーを触らずに済むよう、Dockerで2台のMySQLを立てます。DockerでMySQLを構築する方法の1台構成を、2台に増やす形です。

①2台を起動する

docker-compose.ymlを作ります。レプリケーションにはサーバーごとに異なるserver-idと、ソース側のバイナリログ有効化が必須です。

services:
  source:
    image: mysql:8.0
    container_name: repl-source
    environment:
      MYSQL_ROOT_PASSWORD: rootpass
      MYSQL_DATABASE: shop
    command: >
      --server-id=1
      --log-bin=mysql-bin
      --binlog-format=ROW
      --gtid-mode=ON
      --enforce-gtid-consistency=ON
    ports:
      - "13306:3306"
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-prootpass"]
      interval: 3s
      timeout: 5s
      retries: 30

  replica:
    image: mysql:8.0
    container_name: repl-replica
    environment:
      MYSQL_ROOT_PASSWORD: rootpass
    command: >
      --server-id=2
      --log-bin=mysql-bin
      --binlog-format=ROW
      --gtid-mode=ON
      --enforce-gtid-consistency=ON
      --read-only=ON
    ports:
      - "13307:3306"
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-prootpass"]
      interval: 3s
      timeout: 5s
      retries: 30

設定項目の意味は次のとおりです。

設定意味
--server-idサーバーの識別番号。重複すると組めない
--log-binバイナリログを有効にする。複製の材料になる
--binlog-format=ROW変更後の行そのものを記録する。最も安全な形式
--gtid-mode=ONトランザクションに世界で一意なIDを振る。位置指定が不要になり運用が楽になる
--read-only=ONレプリカへの直接の書き込みを防ぐ(ただし後述の落とし穴あり)

起動して、両方がhealthyになるのを待ちます。

$ docker compose up -d
$ docker inspect -f '{{.State.Health.Status}}' repl-source repl-replica
healthy
healthy

ヘルスチェックで-h 127.0.0.1を付けているのには理由があります。これを省くとソケット経由の接続になり、MySQLが初期化のために内部で起動する一時サーバーにも反応してしまうため、まだ接続を受け付けられない段階でhealthyと判定されます。

②レプリケーション専用ユーザーを作る

ソース側で、レプリカが接続してくるためのユーザーを作ります。必要な権限はREPLICATION SLAVEだけです。動作確認用のテーブルも用意しておきます。

$ docker exec -e MYSQL_PWD=rootpass repl-source mysql -uroot -e "
CREATE USER 'repl'@'%' IDENTIFIED WITH mysql_native_password BY 'replpass';
GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%';
FLUSH PRIVILEGES;
USE shop;
CREATE TABLE orders (
  id INT PRIMARY KEY AUTO_INCREMENT,
  item VARCHAR(50),
  created_at DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3)
);
INSERT INTO orders (item) VALUES ('apple'),('banana'),('cherry');
"

パスワードを-pで直接渡すとUsing a password on the command line interface can be insecure.という警告が出ます。MYSQL_PWD環境変数で渡すと警告なしで実行できるので、スクリプト内ではこちらが扱いやすくなります。

③レプリカを接続して開始する

$ docker exec -e MYSQL_PWD=rootpass repl-replica mysql -uroot -e "
CHANGE REPLICATION SOURCE TO
  SOURCE_HOST='source',
  SOURCE_USER='repl',
  SOURCE_PASSWORD='replpass',
  SOURCE_AUTO_POSITION=1,
  GET_SOURCE_PUBLIC_KEY=1;
START REPLICA;
"

SOURCE_AUTO_POSITION=1がGTIDを使う指定です。これにより「バイナリログのどのファイルの何バイト目から読むか」を人間が調べて指定する必要がなくなります。

GET_SOURCE_PUBLIC_KEY=1は認証方式に関わる指定です。上の例ではユーザーをmysql_native_passwordで作ったため必須ではありませんが、MySQL 8系の既定であるcaching_sha2_passwordでユーザーを作った場合、これがないとTLS未使用の接続で失敗します。実際に既定の方式で試すと次のエラーになりました。

           Replica_IO_Running: Connecting
                Last_IO_Errno: 2061
                Last_IO_Error: Error connecting to source 'repl2@source:3306'.
                    Message: Authentication plugin 'caching_sha2_password' reported error:
                    Authentication requires secure connection.

Replica_IO_RunningYesではなくConnectingのまま止まっているのが特徴です。GET_SOURCE_PUBLIC_KEY=1を付け直すとYesになりました。本番環境では、この指定に頼らずレプリケーション接続をTLS化するほうが適切です。

④動いているか確認する

$ docker exec -e MYSQL_PWD=rootpass repl-replica mysql -uroot -e "SHOW REPLICA STATUS\G"

           Replica_IO_Running: Yes
          Replica_SQL_Running: Yes
        Seconds_Behind_Source: 0
                   Last_Error:
           Retrieved_Gtid_Set: 2b146592-8e86-11f1-aadc-5e5b28bcd287:1-11

確認すべきは3つの値です。Replica_IO_RunningReplica_SQL_Runningが両方Yes、そしてSeconds_Behind_Source0なら正常です。実際にデータが届いているかも見ておきます。

$ docker exec -e MYSQL_PWD=rootpass repl-replica mysql -uroot -e "SELECT * FROM shop.orders;"
id	item	created_at
1	apple	2026-08-02 15:24:14.501
2	banana	2026-08-02 15:24:14.501
3	cherry	2026-08-02 15:24:14.501

レプリケーション開始前に作ったテーブルとデータも入っています。GTIDを使うと、レプリカはソースのバイナリログの最初から未実行のトランザクションを自動で追いかけるためです。

どれくらい遅れるのか|遅延を実測する

非同期レプリケーションでは、ソースへの書き込みがレプリカに反映されるまでに必ず時間差が生まれます。「読み取りをレプリカに逃がす」設計をするなら、この差がどの程度なのかを知っておく必要があります。

計測方法の注意点

素朴に「docker execでINSERT →docker execでSELECTを繰り返す」という測り方をすると、docker execの起動コストが数百ミリ秒あり、測りたい値より桁が大きくなって意味をなしません。そこで、レプリカ側に「行が現れるまで待って、現れた瞬間の時刻を返す」プロシージャを置き、両側ともMySQL内部のNOW(3)で時刻を取って比較しました。コンテナは同じホストの時計を共有しているので、この比較は成立します。

USE shop;
DELIMITER //
CREATE PROCEDURE wait_for(IN t VARCHAR(50))
BEGIN
  DECLARE c INT DEFAULT 0;
  WHILE c = 0 DO
    SELECT COUNT(*) INTO c FROM shop.orders WHERE item = t;
  END WHILE;
  SELECT NOW(3) AS seen_at;
END //
DELIMITER ;

このSQLを流し込むときにも注意点があります。DELIMITERはmysqlクライアント側のコマンドであり、mysql -e "..."では解釈されません。そのまま-eで渡すとプロシージャは作られず、エラーも分かりにくい形で出ます。ファイルに書いて標準入力から流し込んでください。

$ docker exec -i -e MYSQL_PWD=rootpass repl-replica mysql -uroot < proc.sql

結果:遅延は7〜10ミリ秒

lag-1  source=2026-08-02 15:26:11.270  replica=2026-08-02 15:26:11.279  遅延=9.0 ms
lag-2  source=2026-08-02 15:26:12.421  replica=2026-08-02 15:26:12.431  遅延=10.0 ms
lag-3  source=2026-08-02 15:26:13.574  replica=2026-08-02 15:26:13.581  遅延=7.0 ms
lag-4  source=2026-08-02 15:26:14.713  replica=2026-08-02 15:26:14.722  遅延=9.0 ms
lag-5  source=2026-08-02 15:26:15.866  replica=2026-08-02 15:26:15.875  遅延=9.0 ms

同一ホスト上のコンテナ同士で7〜10ミリ秒でした。これは理想的な条件での下限値です。実際の環境では、ネットワークの遅延、大量更新によるバイナリログの肥大、レプリカ側のディスク性能やスレッド数によって、数秒から数分にまで開くことがあります。

ここから導かれる実務上の注意は明確です。「登録した直後にその内容を表示する」処理をレプリカから読むと、まだ存在しないことがあります。登録直後の画面だけはソースから読む、といった作り分けが必要になります。

運用でつまずくポイント(実測で確認)

①rootは --read-only を素通りする

レプリカには--read-only=ONを付けました。一般ユーザーで書き込むとちゃんと拒否されます。

$ docker exec -e MYSQL_PWD=apppass repl-replica mysql -uapp -e "INSERT INTO shop.orders (item) VALUES ('direct-write');"
ERROR 1290 (HY000) at line 1: The MySQL server is running with the --read-only option so it cannot execute this statement

ところが、同じことをrootで実行すると通ってしまいます。

$ docker exec -e MYSQL_PWD=rootpass repl-replica mysql -uroot -e "SELECT @@read_only, @@super_read_only;"
@@read_only	@@super_read_only
1	0

$ docker exec -e MYSQL_PWD=rootpass repl-replica mysql -uroot -e "INSERT INTO shop.orders (id, item) VALUES (999, 'root-wrote-this');"
(エラーなし)

$ docker exec -e MYSQL_PWD=rootpass repl-replica mysql -uroot -e "SELECT * FROM shop.orders WHERE id=999;"
id	item	created_at
999	root-wrote-this	2026-08-02 15:26:33.415

--read-onlySUPER権限を持つユーザーには効きません@@super_read_only0のままだからです。運用者がrootで調査中にうっかり更新すると、レプリカだけにデータがある状態(差分)ができあがります。

対策はsuper_read_onlyを有効にすることです。実際に切り替えて確認しました。

$ docker exec ... repl-replica mysql -uroot -e "SET GLOBAL super_read_only=ON; SELECT @@read_only, @@super_read_only;"
@@read_only	@@super_read_only
1	1

$ docker exec ... repl-replica mysql -uroot -e "INSERT INTO shop.orders (id, item) VALUES (888, 'root-again');"
ERROR 1290 (HY000) at line 1: The MySQL server is running with the --super-read-only option so it cannot execute this statement

rootでも弾かれるようになりました。この状態でもレプリケーションによる適用は止まりません。ソース側に追加した行がレプリカへ反映されることも確認しています。読み取り専用にすると複製まで止まるのでは、と心配になりますが、レプリカのSQLスレッドはこの制限の対象外です。恒久化するには起動オプションに--super-read-only=ONを加えてください。

②差分ができるとエラー1062で止まり、遅延はNULLになる

上でレプリカだけに作ったid=999は、時限爆弾になります。ソース側で同じIDが使われた瞬間に爆発します。

$ docker exec -e MYSQL_PWD=rootpass repl-source mysql -uroot -e "INSERT INTO shop.orders (id, item) VALUES (999, 'source-legit');"

$ docker exec -e MYSQL_PWD=rootpass repl-replica mysql -uroot -e "SHOW REPLICA STATUS\G"
           Replica_IO_Running: Yes
          Replica_SQL_Running: No
        Seconds_Behind_Source: NULL
               Last_SQL_Errno: 1062

詳しい原因はperformance_schemaから取れます。

$ docker exec -e MYSQL_PWD=rootpass repl-replica mysql -uroot -e "
SELECT WORKER_ID, LAST_ERROR_NUMBER, LAST_ERROR_MESSAGE
FROM performance_schema.replication_applier_status_by_worker
WHERE LAST_ERROR_NUMBER<>0\G"

         WORKER_ID: 1
 LAST_ERROR_NUMBER: 1062
LAST_ERROR_MESSAGE: Could not execute Write_rows event on table shop.orders;
                    Duplicate entry '999' for key 'orders.PRIMARY', Error_code: 1062

ここが監視設計の要点です。SQLスレッドが止まったとき、Seconds_Behind_Sourceは「999999」のような大きな値ではなくNULLになります。「遅延が閾値を超えたら通知」という監視だけを組んでいると、NULLは閾値比較で真にならないため、完全に停止しているのに何の通知も飛びませんReplica_IO_RunningReplica_SQL_RunningYesであることも必ず監視対象に入れてください。

③止まっていてもレプリカは古いデータを平然と返す

停止に気づかないことの何が怖いのか。SQLスレッドが止まった状態のまま、ソースへ2件追加してみます。

--- source の件数 ---
11
--- replica の件数(止まっているのにエラーを返さない) ---
9

レプリカは接続を受け付け、クエリを正常終了させ、古い結果を返します。エラーも警告も出ません。参照系をレプリカに逃がしている構成なら、利用者には「なぜか反映されない」「データが消えた」と見えます。件数の差は、放置した時間だけ開き続けます。

復旧は、差分の原因になっている行を消してSQLスレッドを再開するだけです。

$ docker exec -e MYSQL_PWD=rootpass repl-replica mysql -uroot -e "
DELETE FROM shop.orders WHERE id=999;
START REPLICA SQL_THREAD;"

           Replica_IO_Running: Yes
          Replica_SQL_Running: Yes
        Seconds_Behind_Source: 0
               Last_SQL_Errno: 0

source : 11
replica: 11

④非同期はコミット済みのトランザクションを失う

「レプリカがあるからデータは消えない」を検証します。ネットワーク断を模擬してI/Oスレッドを止め、その状態でソースに書き込みます。

$ docker exec ... repl-replica mysql -uroot -e "STOP REPLICA IO_THREAD;"

$ docker exec ... repl-source mysql -uroot -e "
INSERT INTO shop.orders (item) VALUES ('important-order');
SELECT COUNT(*) AS committed_on_source FROM shop.orders WHERE item='important-order';"
committed_on_source
1

ソースはコミットに成功しています。アプリケーションには「登録できました」と返っています。このときの監視値を見ると、もうひとつの罠が現れます。

           Replica_IO_Running: No
          Replica_SQL_Running: Yes     ← SQLスレッドは正常なまま
        Seconds_Behind_Source: NULL

今度はI/Oスレッドだけが停止しています。SQLスレッドは受け取った分を処理し終えているのでYesのままです。片方だけを見ていると健全に見えます。ここでソースが突然死したとします。

$ docker kill repl-source
$ docker ps -a --filter name=repl-source --format "{{.Names}}: {{.Status}}"
repl-source: Exited (137) 2 seconds ago

--- レプリカを昇格させたら、そのコミットは残っているか ---
important_order_on_replica
0
total_rows
5

消えています。アプリケーションが「成功」を受け取った注文が、フェイルオーバー後には存在しません。これが非同期レプリケーションの正体で、RPO(許容できるデータ損失時間)はゼロではないということです。ゼロに近づけたいなら準同期レプリケーションを検討します。

なお今回はソースを起動し直せたので、I/Oスレッドを再開すると追いつきました。これは元のディスクが無事だったから戻っただけで、ディスクごと失われていれば復旧できません。

$ docker start repl-source
$ docker exec ... repl-replica mysql -uroot -e "START REPLICA IO_THREAD;"

           Replica_IO_Running: Yes
          Replica_SQL_Running: Yes
        Seconds_Behind_Source: 0
source : 6
replica: 6

誤操作から救う:遅延レプリケーション

冒頭の「8ミリ秒で消える」問題に対する、レプリケーション側からの答えが遅延レプリケーションです。レプリカにわざと遅れて適用させることで、事故に気づく猶予を作ります。

SOURCE_DELAY を設定する

$ docker exec -e MYSQL_PWD=rootpass repl-replica mysql -uroot -e "
STOP REPLICA;
CHANGE REPLICATION SOURCE TO SOURCE_DELAY=60;
START REPLICA;"

          Replica_SQL_Running: Yes
        Seconds_Behind_Source: 0
                    SQL_Delay: 60
          SQL_Remaining_Delay: NULL

SQL_Delayが設定した秒数です。SQL_Remaining_Delayは「いま待たされているトランザクションが、あと何秒で適用されるか」を示します。待つべきものがないときはNULLになります。

実測:60秒の猶予でデータは生き残る

この状態で、あらためてソースの全行を消します。

DELETEからの経過sourcereplicaSQL_Remaining_Delay
約2秒0件5件59
約10秒0件5件50
約30秒0件5件30

8ミリ秒だった猶予が60秒に伸びました。この間にレプリカのSQLスレッドを止めれば、削除は永久に適用されません。

$ docker exec -e MYSQL_PWD=rootpass repl-replica mysql -uroot -e "STOP REPLICA SQL_THREAD;"

--- 90秒待っても replica のデータは残るか ---
  replica: 5
           Replica_IO_Running: Yes
          Replica_SQL_Running: No
          SQL_Remaining_Delay: NULL

実務では60秒では短すぎます。気づいて手を動かすまでの時間を考えると、1時間(3600秒)から24時間程度を設定するのが一般的です。その分レプリカは古いデータを持つことになるため、負荷分散用のレプリカとは別に、遅延専用のレプリカを1台用意する構成になります。

救出時に踏むmysqldumpの罠

止めたレプリカからデータを吸い出してソースへ戻します。ところが、素直にmysqldumpして流し込むと失敗します。

$ docker exec ... repl-replica mysqldump -uroot --no-tablespaces shop orders > rescued.sql
$ docker exec -i ... repl-source mysql -uroot shop < rescued.sql
ERROR 3546 (HY000) at line 24: @@GLOBAL.GTID_PURGED cannot be changed:
the added gtid set must not overlap with @@GLOBAL.GTID_EXECUTED

原因はダンプの先頭に埋め込まれる2行です。

SET @@SESSION.SQL_LOG_BIN= 0;
SET @@GLOBAL.GTID_PURGED='2b1194ee-...:1-17, 2b146592-...:1-31';

GTIDが有効なサーバーからmysqldumpすると、「このダンプにはこれらのGTIDが含まれている」という宣言が自動で入ります。稼働中のソースは既にそれらを実行済みなので、重複を理由に弾かれます。

さらに厄介なのがSQL_LOG_BIN=0です。これは「このセッションの操作をバイナリログに書かない」という指定なので、仮にエラーを回避してリストアできても、その復旧内容がレプリカへ伝わりません。ソースだけ直ってレプリカが取り残される、という二次事故になります。

対処は--set-gtid-purged=OFFを付けるだけです。

$ docker exec ... repl-replica mysqldump -uroot --no-tablespaces --set-gtid-purged=OFF shop orders > rescued-fixed.sql

GTID_PURGED 行: 0 件(修正前: 1 件)
SQL_LOG_BIN 行: 0 件(修正前: 3 件)

$ docker exec -i ... repl-source mysql -uroot shop < rescued-fixed.sql
$ docker exec ... repl-source mysql -uroot -e "SELECT * FROM shop.orders;"
id	item	created_at
1002	apple	2026-08-02 15:27:39.167
1003	banana	2026-08-02 15:27:39.167
1004	cherry	2026-08-02 15:27:39.167
1005	durian	2026-08-02 15:27:39.167
1006	elderberry	2026-08-02 15:27:39.167

復旧できました。最後に遅延を解除してレプリケーションを再開すると、両者が一致します。

$ docker exec ... repl-replica mysql -uroot -e "
STOP REPLICA;
CHANGE REPLICATION SOURCE TO SOURCE_DELAY=0;
START REPLICA;"

source : 5
replica: 5

mysqldumpのオプションはmysqldump|MySQL/MariaDBの論理バックアップを作成するダンプツールに一覧があります。PostgreSQLの場合はpg_dumpが対応します。

結局、何をどう組み合わせればよいか

ここまでの実測をふまえると、守りたい事故ごとに必要な備えは次のように整理できます。

起きる事故レプリケーション遅延レプリカバックアップ
サーバー・ディスクの故障有効有効有効(ただし復旧に時間がかかる)
データセンターの障害有効(別拠点に置けば)有効有効
誤った DELETE / DROP無効(8ミリ秒で伝播)有効(猶予内なら)有効
アプリのバグによる不正更新無効気づけば有効有効
ランサムウェア無効気づけば有効有効(隔離保管が前提)
数か月前の状態を見たい無効無効有効

最低限そろえるべきは「レプリケーション+定期バックアップ」の2本立て、そこに余力があれば遅延レプリカを足す、という順序になります。バックアップの自動実行はcronの設定方法|crontabの書き方・オプション・実行例が使えます。

そしてバックアップは「取れていること」ではなく「戻せること」を定期的に確かめてください。今回--set-gtid-purged=OFFで踏んだように、リストアは本番で初めて試すと失敗します。

よくある質問

レプリケーションとバックアップはどちらが必要ですか?

両方必要です。守る対象が違います。レプリケーションはサーバーが壊れる事故に、バックアップは人やソフトウェアがやらかす事故に対応します。実測では誤ったDELETEが8ミリ秒でレプリカにも伝わったため、レプリケーションだけでは誤操作から復旧できません。

レプリケーションの遅延はどれくらいですか?

今回の検証(同一ホスト上のDockerコンテナ間、小さなINSERT)では7〜10ミリ秒でした。これは条件が最良の場合の値です。ネットワーク越し、大量更新、レプリカ側のディスクが遅いといった条件では数秒から数分に開きます。SHOW REPLICA STATUSSeconds_Behind_Sourceで確認できます。

Seconds_Behind_Source が NULL になるのはなぜですか?

レプリケーションが正常に動いていないときです。実測では2つのケースを確認しました。ひとつはSQLスレッドがエラー(今回は重複キーの1062)で停止したとき、もうひとつはI/Oスレッドが停止してソースとの接続が切れたときです。NULLは「遅延ゼロ」ではなく「測定不能=異常」を意味します。

レプリケーションが止まっていることに気づく方法は?

Seconds_Behind_Sourceの値だけを監視してはいけません。停止時はNULLになるため、閾値による判定をすり抜けます。Replica_IO_RunningReplica_SQL_Runningが両方Yesであることを併せて監視してください。止まっていてもレプリカはエラーを返さず古いデータを返し続けるので、アプリ側からは異常に見えません。

レプリカに書き込んでしまったらどうなりますか?

ソースとの差分ができ、後からソース側で同じ主キーが使われた時点でエラー1062が発生してレプリケーションが停止します。復旧はレプリカ側の余分な行を削除してSTART REPLICA SQL_THREAD;で再開します。予防としては--read-onlyだけでなく--super-read-only=ONを設定してください。--read-onlySUPER権限を持つrootには効きません。

レプリケーションがあればデータは絶対に消えませんか?

消えます。MySQLの既定は非同期レプリケーションで、ソースはレプリカへの転送を待たずにコミットを返します。実測では、通信が切れた状態でコミットしたトランザクションがソースの停止によって失われました(アプリには成功が返っていた)。損失をゼロに近づけたい場合は準同期レプリケーションを使います。

マスター・スレーブという呼び方は使ってよいですか?

MySQLは8.0.22以降、用語をソース/レプリカに変更しました。新規に書く手順書やコードでは新しい用語を使うことが推奨されます。ただしSHOW SLAVE STATUSのような古いコマンドも当面は動作し、既存の資料は旧用語のままのものが多いため、両方を読めるようにしておくと困りません。

遅延レプリケーションは何秒に設定すべきですか?

「事故に気づいて手を動かせるまでの時間」で決めます。実測で使った60秒は動作確認用で、実務では短すぎます。1時間(3600秒)から24時間程度が一般的です。遅延レプリカはその分古いデータを持つため、参照負荷分散用のレプリカとは別に用意します。

まとめ

  • レプリケーションとは、データを別サーバーへ自動で複製し続ける仕組み。可用性の確保と参照の負荷分散のために使う
  • バックアップの代わりにはならない。実測では誤ったDELETE8ミリ秒でレプリカにも伝わった
  • 複製の遅延は同一ホスト上で7〜10ミリ秒。登録直後の参照をレプリカに逃がすと、まだ見えないことがある
  • --read-onlyはrootに効かない。--super-read-only=ONまで設定する
  • 停止時Seconds_Behind_SourceNULLになる。2つのスレッドのYesも監視する
  • 既定の非同期レプリケーションはコミット済みトランザクションを失うことがある
  • 誤操作対策には遅延レプリケーション(SOURCE_DELAYが有効。救出時のmysqldumpには--set-gtid-purged=OFFを付ける

検証環境はDocker上のmysql:8.0(実バージョン8.0.46)で、記事中の出力はすべて実行結果をそのまま掲載しています。同じdocker-compose.ymlで手元に再現できるので、本番に触る前にここで一度失敗しておくことをおすすめします。

関連記事

Bash玄

はじめまして!Bash玄です。

エンジニアとしてシステム運用に携わる中で、手作業の多さに限界を感じ、Bashスクリプトを活用して業務を効率化したのがきっかけで、この道に入りました。「手作業は負け」「スクリプトはシンプルに」をモットーに、誰でも実践できるBashスクリプトの書き方を発信しています。

このサイトでは、Bashの基礎から実践的なスクリプト作成まで、初心者でもわかりやすく解説しています。少しでも「Bashって便利だな」と思ってもらえたら嬉しいです!

# 好きなこと
- シンプルなコードを書くこと
- コマンドラインを快適にカスタマイズすること
- 自動化で時間を生み出すこと

# このサイトを読んでほしい人
- Bashに興味があるけど、何から始めればいいかわからない人
- 定型業務を自動化したい人
- 効率よくターミナルを使いこなしたい人

Bashの世界に一歩踏み出して、一緒に「Bash道」を極めていきましょう!

Bash玄をフォローする

コメント