MySQL GTID レプリケーション素振り

従来のレプリケーションとの違いをざっくりと。

  • ノードに UUID が付与されて「UUID+連番」ですべてのトランザクションに一意なID(GTID)がつく
  • 各ノードが「適用済の GTID」を持っているので循環レプリケーションやマスター切り替えが容易にできる
  • 「適用済の GTID」はバイナリログに書かれているのでスレーブでもバイナリログ必須(log-slave-updates
    • 追記 2019-11-15 というのは 5.6 だけで 5.7 以降はマスターに昇格するつもりのないスレーブならバイナリログ無効でも OK のようです
  • GTID はトランザクション単位で付与されるのでトランザクションテーブル必須(MyISAM 不可)

GTID レプリケーションをセットアップ

docker-compose.yml

version: '3.4'

services:
  sv01:
    image: mysql:8
    environment:
      MYSQL_ALLOW_EMPTY_PASSWORD: 1
      MYSQL_DATABASE: test
    networks:
      mysql:
        ipv4_address: 192.168.88.11
    command:
      - --default_authentication_plugin=mysql_native_password
      - --skip-name-resolve
      - --character-set-server=utf8
      - --innodb-file-per-table
      - --log-bin=mysql-bin
      - --sync-binlog=1
      - --relay-log=relay-bin
      - --log-slave-updates
      - --skip-slave-start
      - --binlog-format=row
      - --binlog-do-db=test
      - --slave-exec-mode=IDEMPOTENT
      - --master-info-repository=TABLE
      - --relay-log-info-repository=TABLE
      - --relay-log-recovery=ON
      - --gtid-mode=ON
      - --enforce-gtid-consistency
      - --server-id=1

  sv02:
    image: mysql:8
    environment:
      MYSQL_ALLOW_EMPTY_PASSWORD: 1
      MYSQL_DATABASE: test
    networks:
      mysql:
        ipv4_address: 192.168.88.12
    command:
      - --default_authentication_plugin=mysql_native_password
      - --skip-name-resolve
      - --character-set-server=utf8
      - --innodb-file-per-table
      - --log-bin=mysql-bin
      - --sync-binlog=1
      - --relay-log=relay-bin
      - --log-slave-updates
      - --skip-slave-start
      - --binlog-format=row
      - --binlog-do-db=test
      - --slave-exec-mode=IDEMPOTENT
      - --master-info-repository=TABLE
      - --relay-log-info-repository=TABLE
      - --relay-log-recovery=ON
      - --gtid-mode=ON
      - --enforce-gtid-consistency
      - --server-id=2

  sv03:
    image: mysql:8
    environment:
      MYSQL_ALLOW_EMPTY_PASSWORD: 1
      MYSQL_DATABASE: test
    networks:
      mysql:
        ipv4_address: 192.168.88.13
    command:
      - --default_authentication_plugin=mysql_native_password
      - --skip-name-resolve
      - --character-set-server=utf8
      - --innodb-file-per-table
      - --log-bin=mysql-bin
      - --sync-binlog=1
      - --relay-log=relay-bin
      - --log-slave-updates
      - --skip-slave-start
      - --binlog-format=row
      - --binlog-do-db=test
      - --slave-exec-mode=IDEMPOTENT
      - --master-info-repository=TABLE
      - --relay-log-info-repository=TABLE
      - --relay-log-recovery=ON
      - --gtid-mode=ON
      - --enforce-gtid-consistency
      - --server-id=3

networks:
  mysql:
    driver: bridge
    ipam:
      driver: default
      config:
        - subnet: 192.168.88.0/24

up してコンテナの中へ。

docker-compose up
docker-compose exec sv01 bash
docker-compose exec sv02 bash
docker-compose exec sv03 bash

すべてのノードにレプリケーション用のユーザーを作る。--binlog-do-db=test してるのでこれはレプリケーションされません。

# [sv01/sv02/sv03]
mysql mysql -v <<'SQL'
CREATE USER 'repl'@'%' IDENTIFIED BY 'pass';
GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%';
SQL

レプリケーションを設定する。sv01 -> sv02 -> sv03 -> sv01 のような循環レプリケーションします。

# [sv01]
mysql mysql -v <<'SQL'
change master to
  MASTER_HOST = '192.168.88.13',
  MASTER_USER = 'repl',
  MASTER_PASSWORD = 'pass',
  MASTER_AUTO_POSITION = 1;
start slave;
SQL

# [sv02]
mysql mysql -v <<'SQL'
change master to
  MASTER_HOST = '192.168.88.11',
  MASTER_USER = 'repl',
  MASTER_PASSWORD = 'pass',
  MASTER_AUTO_POSITION = 1;
start slave;
SQL

# [sv03]
mysql mysql -v <<'SQL'
change master to
  MASTER_HOST = '192.168.88.12',
  MASTER_USER = 'repl',
  MASTER_PASSWORD = 'pass',
  MASTER_AUTO_POSITION = 1;
start slave;
SQL

適当なノードでテーブルを作ったり行を挿入したりすると、すべてのノードにレプリケーションされます。

# [sv01/sv02/sv03]
mysql test
/* [sv03] */
create table t (id int not null primary key, no int not null);

/* [sv01] */
insert into t values (1, 111);

/* [sv02] */
insert into t values (2, 222);

/* [sv01/sv02/sv03] */
select * from t;

slave-exec-mode=IDEMPOTENT

(GTID とは関係ないですが)slave-exec-mode=IDEMPOTENT の効果により、行ベースレプリケーションで行の競合がエラーにならなくなっています。

試しに行の挿入で競合を発生させてみます。

/* [sv01/sv02/sv03] */
stop slave;

/* [sv01] */
insert into t values (9, 901);

/* [sv02] */
insert into t values (9, 902);

/* [sv03] */
insert into t values (9, 903);

/* [sv01/sv02/sv03] */
start slave;
select * from t;
show slave status \G

普通ならSQLスレッドでエラーになってレプリケーションが止まるところですが slave-exec-mode=IDEMPOTENT の効果によってエラーになりません。

ちなみにノードごとに行の値は次のようになっていました。

# [sv01]
+----+-----+
| id | no  |
+----+-----+
|  9 | 902 |
+----+-----+

# [sv02]
+----+-----+
| id | no  |
+----+-----+
|  9 | 903 |
+----+-----+

# [sv03]
+----+-----+
| id | no  |
+----+-----+
|  9 | 901 |
+----+-----+

この結果から推測するに、競合した場合は後勝ちで上書きされるようです。

なので例えば2台で双方向レプリケーションしているときに両方に同じ主キーのレコードを書き込むと、ノード1にはノード2に書き込んだ内容が、ノード2にはノード1に書き込んだ内容がのこることになりますね、うーん?

マスターの切り替え

GTID ならマスターの切り替えも簡単です。試しに sv02 が死んだと仮定して sv03 のマスターを sv01 に変えてみます。

/* [sv02] */
stop slave;

/* [sv03] */
stop slave;
change master to MASTER_HOST = '192.168.88.11';
start slave;

sv01 を更新すると sv03 にレプリケーションされます。

/* [sv01] */
insert into t values (4, 444);

/* [sv03] */
select * from t;
show slave status \G

従来のレプリケーションだとマスターが変わればログポジションも変わるので mysqldump で再同期が必要なケースです(MHA なんかはこの辺うまくやってた気がする)。

戻すのも簡単です。

/* [sv02] */
start slave;

/* [sv03] */
stop slave;
change master to MASTER_HOST = '192.168.88.12';
start slave;

競合したときのリカバリ

slave-exec-mode=IDEMPOTENT の効果によって行ベースの競合はエラーになりませんが、DDL なら簡単に競合によってレプリケーションが停止します。

/* [sv01/sv02] */
stop slave;

/* [sv01] */
create table x (sv01 int not null primary key);

/* [sv02] */
create table x (sv02 int not null primary key);

/* [sv01/sv02] */
start slave;

/* [sv01/sv02/sv03] */
show slave status \G

sv01 や sv02 で次のようなエラーでレプリケーションが止まっています。

Error 'Table 'x' already exists' on query. Default database: 'test'. Query: 'create table x (sv02 int not null primary key)'

従来のレプリケーションなら SET GLOBAL SQL_SLAVE_SKIP_COUNTER = 1 でスキップするステートメントの数を指定するのですが、GTID レプリケーションではこれはできません。

GTID の場合はまず show slave statusRetrieved_Gtid_SetExecuted_Gtid_Set でエラーになっている GTID を特定します。例えば次のようにでした。

Retrieved_Gtid_Set:
  c7399d3d-72d0-11e8-a858-0242c0a8580c:1-4,
  c7502877-72d0-11e8-b3c2-0242c0a8580d:1-3
Executed_Gtid_Set:
  c7399d3d-72d0-11e8-a858-0242c0a8580c:1-3,
  c7502877-72d0-11e8-b3c2-0242c0a8580d:1-3,
  c75314e4-72d0-11e8-a322-0242c0a8580b:1-5

Retrieved_Gtid_Set にあって Executed_Gtid_Set に無いものがレプリケーションされていない GTID です。比較すると UUID c7399d3d-72d0-11e8-a858-0242c0a8580c について Retrieved_Gtid_Set1-4Executed_Gtid_Set1-3 なので、エラーになっているのは c7399d3d-72d0-11e8-a858-0242c0a8580c:4 だということがわかります。

このエラーの原因となっている GTID を次の手順で空のトランザクションで上書きします。

SET GTID_NEXT='c7399d3d-72d0-11e8-a858-0242c0a8580c:4';
BEGIN;
COMMIT;
SET GTID_NEXT='AUTOMATIC';

この時点でスレーブを開始すればレプリケーションは復帰しますが、この手順で復帰させたときは同じ GTID のバイナリログがノードによって異なる内容になっているため、誤動作の元になるかもしれないので次のようにバイナリログを削除しておくのが良いらしいです。

/* バイナリログの一覧を表示して最後のログをメモる */
show master logs;

/* バイナリログをローテート */
FLUSH LOGS;

/* バイナリログの削除 */
PURGE BINARY LOGS TO 'mysql-bin.000003';

この後、スレーブを開始できます。

start slave;
show slave status \G

もちろん、テーブルがノードによって異なる定義になってしまっている問題は残ったままなので、その問題の解決はまた別に行う必要があります。

スレーブの追加や再同期

スレーブの追加や再同期は mysqldump で流し込んだ後に change master tostart slave するだけで良いです。

# [sv04]
mysqldump -h sv03 --all-databases --triggers --routines --events | mysql

# [sv04]
mysql mysql -v <<'SQL'
change master to
  MASTER_HOST = '192.168.88.13',
  MASTER_USER = 'repl',
  MASTER_PASSWORD = 'pass',
  MASTER_AUTO_POSITION = 1;
start slave;
SQL

GTID が有効なサーバからの mysqldump では GTID レプリケーションに必要な↓みたいなのがデフォで含まれています。

--
-- GTID state at the beginning of the backup
--

SET @@GLOBAL.GTID_PURGED=/*!80000 '+'*/ 'c1ea378a-72d5-11e8-a345-0242c0a8580d:1,
c7399d3d-72d0-11e8-a858-0242c0a8580c:1-6,
c7502877-72d0-11e8-b3c2-0242c0a8580d:1-3,
c75314e4-72d0-11e8-a322-0242c0a8580b:1-6';

「これらの GTID のバイナリログは削除されてるので無いです、でもかつてあったということは覚えていて」みたいなニュアンスかと思います。

その他のメモ

binlog-do-db とか replicate-do-db とか

binlog-do-dbbinlog-ignore-db はマスターのバイナリログの出力に作用するオプションなので、マスターでバイナリログへの出力の段階でフィルタされ、スレーブへ転送されるログやスレーブで適用されるログは、マスターでフィルタされたバイナリログのみになる。

一方で replication-do-dbreplicate-ignore-db はスレーブのSQLスレッドの実行に作用するオプションなので、マスターでのバイナリログの出力やスレーブへ転送されるバイナリログの量には影響せず、マスターから転送されたバイナリログをスレーブがどれだけ実行するかの制御にしかならない。

なので、マスター~スレーブの転送量の削減のために replication-do-db は使えない。その目的なら binlog-do-db を使わなければならない。

ただし binlog-do-db だとその設定をおこなったマスタからレプリケーションするすべてのスレーブに設定が影響する。replication-do-db ならスレーブごとにレプリケーションするDBを選択できるのでフレキシブル。

ステートメントベースレプリケーションと行ベースレプリケーション

binlog-do-db とか replicate-do-db が作用するデータベースについて、ステートメントベースなら USE で選択されているカレントのデータベースによって制御される。

例えば binlog-do-db=test の場合、カレントデータベースが test であるクエリのみがレプリケーションの対象となる。実際に作用されたデータベースではないことに注意。例えば下記のようになる。

/* カレントが test なのでレプリケーションされる */
use test
insert into hoge.t_user values (1);

/* カレントが hoge なのでレプリケーションされない */
use hoge
insert into test.t_user values (1);

一方で、行ベースでは、カレントのデータベースではなく実際に作用するデータベースで制御される。そのため↑の例だと逆になる。

なお binlog-format=row と設定したからといって必ず行ベースになるわけではない。そもそも行ベースではないもの、例えば DDL は行ベースにならない(しようがない)のでステートメントベースになる。

なので binlog-format=row かつ replicate-do-db=test しているとき次のようになる。

use test
/* 行ベースなのでレプリケーションされない */
insert into hoge.t_user values (1);
/* ステートメントベースなのでレプリケーションされる */
create table hoge.t (id int not nul primary key);

use hoge
/* 行ベースなのでレプリケーションされる */
insert into test.t_user values (1);
/* ステートメントベースなのでレプリケーションされない */
create table test.t (id int not nul primary key);

一方で create database drop database alter databasereplicate-do-db で指定の通りに動く。つまりカレントデータベースではなく作用されるデータベースでフィルタされる( https://dev.mysql.com/doc/refman/8.0/en/replication-rules-db-options.html の下の方の Important の説明)。

さいごに

GTID レプリケーションではいくつか制限があります。

が、(既存の非GTIDからのマイグレーションとかはともかく)新規にやる分には問題なさそうな気がするので、今後 MySQL でレプリケーションするなら GTID は常に有効で良いと思う。