それが僕には楽しかったんです。

僕と MySQL と時々 MariaDB

docker-compose を使って MySQL8 の レプリケーションを構築する

はじめに

どうも、最近アローを見ていてフラッシュに出てくるアローのキャストと同じで感動してるけんつです。フェリシティいいよね。
ようやくアドカレが終わったので、気分転換にやろうやろうと思っていた MySQLレプリケーションを docker-compose で構築する方法でもまとめます。

qiita とかみたら実例があるけども、それを見たらあまり勉強にならないのでなるべく MySQL 8 の公式ドキュメントを見ながら構築していく。

今回はいつの間にか降ってきていた MySQL 8.0.18 の docker コンテナを使ってやっていく。

MySQLレプリケーション

そもそも MySQL におけるレプリケーションとはそもそも何なのかと言うと、Master のデータを1つ以上の Slave にコピーできる機能。
そしてこのレプリケーション機能は、デフォルトで非同期なので Slave は Master からのデータを受け取るために永続的に接続している必要がない。
また構成に応じて全てのデータベース、選択したデータベース、テーブルなどを選択して複製することができる。

レプリケーションを取る利点しては以下の通り。

  • スケールアウト: 複数のスレーブに負荷を分散する。書き込みに関することはマスターで行う必要があるが。読み込みに関してはスレーブを利用させることで負荷を減らす。
  • データセキュリティ: データはスレーブに複製され、スレーブはレプリケーションプロセスを一時停止させることができるためマスターデータを壊すこと無くスレーブでバックアップを実行することができる。
  • 分析; 理由はスケールアウトと似ている。負荷の高い情報の読み取りを行うような分析に関することではマスターの負荷を上げることなくスレーブで実行できる。
  • 長距離データ配布: マスターへの永続的なアクセスを必要とせず、レプリケーションを使用してデータをローカルコピーできる。

レプリケーション形式

MySQL には 3 つのレプリケーション形式が存在する。
ひとつは Statement Based Replication(SBR) と呼ばれるもので、 SQL ステートメント全体を複製する。
もうひとつは Row Based Replication(RBR) と呼ばれるもので、変更があった行だけを複製する。
3つ目として、これら2つを状況に応じてキリ開ける Mixed Based Replication というものがある。

デフォルトでは SBR が有効になる。

目的とする構成

まず、Master 1 台に対して Slave 1 台の構成を目指す。
Slave2 という設定が github レポジトリにはあるがそれは今後 GTID レプリケーションを構成する用の設定なので無視していい

成果物

github.com

Binary Log File Position Based Replication の構築

Master サーバの設定

レプリケーションを構築するためにはマスターでバイナリロギングが有効になっている必要がある。MySQL 8ではデフォルトで有効になっている。MySQL 5.7 以下ではデフォルトで有効にならないため、設定する必要がある。
バイナリログはデフォルトで /var/lib/mysql/ に binlog*** という命名で作成される。これは log_bin_basename で変更可能。
また、server_id という 1~2^32 - 1 の正の整数値で指定するサーバを一意に求めることの出来る ID を設定する必要がある。また、これは設定しない場合はデフォルトで 0 が設定されているがその状態だとマスターはスレーブからの接続を拒否するため必ず設定する必要がある。
それらを踏まえて my.cnf(master.cnf) に以下の設定を追加する。

[mysqld]
...
server_id=1

確認する。

MySQL [(none)]> select @@global.log_bin;
+------------------+
| @@global.log_bin |
+------------------+
|                1 |
+------------------+
1 row in set (0.00 sec)

MySQL [(none)]> select @@global.server_id;
+--------------------+
| @@global.server_id |
+--------------------+
|                  1 |
+--------------------+
1 row in set (0.00 sec)

おっけーっぽい。

Slave の設定

こちらも同様に server_id を設定する必要がある。
レプリケーションを取りたい MySQL サーバ同士で ID が被らないように以下の様に設定を追加する。

slave1.cnf

[mysqld]
...
server_id=2

設定を確認する。

MySQL [(none)]> select @@global.log_bin;
+------------------+
| @@global.log_bin |
+------------------+
|                1 |
+------------------+
1 row in set (0.00 sec)

MySQL [(none)]> select @@global.server_id;
+--------------------+
| @@global.server_id |
+--------------------+
|                  2 |
+--------------------+
1 row in set (0.00 sec)

一応 log_bin の設定値を確認したが Slave でバイナリロギングを有効にする必要は実はない。
有効になっていれば、データバックアップとクラッシュリカバリに Slave のバイナリロギングを利用することができるらしい。

レプリケーションユーザを作成する

必ずレプリケーション専用のユーザを作らないといけないわけではないが、レプリケーションのユーザ名とパスワードは mysql.slave_master_info にプレーンテキストとして保存されるため必要最低限の権限を与えた専用ユーザを作成するべきらしい。
といってもやることはユーザを作成して REPLICATION SLAVE の GRANT を付与するだけ。

以下のシェルスクリプトを slave 用 mysql コンテナの docker-entrypoint-initdb.d 以下にマウントする。
またレプリケーション専用アカウントの情報は適宜 docker-compose yaml に追加する

#!/bin/sh

mysql -u root -v mysql <<SQL
        CREATE USER '${MYSQL_REPLICATION_USER:+repl}'@'%' IDENTIFIED BY '${MYSQL_REPLICATION_PASSWORD:+repl}';
        GRANT REPLICATION SLAVE ON *.* TO '$MYSQL_REPLICATION_USER'@'%';
SQL
余談

MySQL のコンテナを使う場合に、MYSQL_USER 等が設定されていると自動でユーザが作成されるが
MySQL_DATABASE が設定されていない場合は USAGE が、設定されている場合はその Database に対して ALL が GRANT として設定されるらしい。

レプリケーションマスターバイナリログのポジション取得

まず、レプリケーションを構築するにはやることがある。
初回起動時は docker-compose yaml の値をもとにユーザなどが作成されるがバイナリログを有効にしているためこれらのログが存在する。
そのため、master のバイナリログをリセットする必要がある。*1

その前提の元に、データスナップショットを取るために必要な手順を追っていく。

まずはバイナリログのどこからレプリケーションを開始するのが正しいかを判断するためにバイナリログのポジションを取得する。
この際に、更新が走ってしまうとバイナリログの位置が変わってしまうため READ LOCK をかける。

この2つを行うため、さっきのシェルスクリプトに以下の SQL を追加する。

#!/bin/sh

...

mysql -u root -v mysql <<SQL
    RESET MASTER;
    FLUSH TABLES WITH READ LOCK;
SQL

次に SHOW MASTER STATUS を参照してバイナリログファイルとポジションを取得する。
それらの情報は Slave を Master に接続するために必要となる。

#!/bin/sh
...

binlogfile=`mysql -u root -h master -e "SHOW MASTER STATUS\G" | grep File: | awk '{print $2}'`
position=`mysql -u root -h master -e "SHOW MASTER STATUS\G" | grep Position: | awk '{print $2}'`

mysqldump を使って master のスナップショットを作成する

次に、スナップショットを作成する。今回は全てのデータベースを対称にするため以下のようにする。
めんどうなのでついてに master のスナップショットを slave に突っ込む。

#!/bin/sh
...
mysqldump -u root -h master --all-databases --master-data > /tmp/master.sql
mysql -u root < /tmp/master.sql

slave1.cnf

[mysqld]
...
skip-slave-start
    • skip-slave-start が無いと、この mysqldump を実行した段階でスレーブが開始してしまうためスレーブ側で STOP SLAVE を実行する必要がある。

Slave を開始する

CHANGE MASTER TO で、接続先と開始するログファイルにポジションを指定して START SLAVE によって開始する。
そのあとで Master の READ LOCK を取っているからそれをアンロックする。

なぜか skip-slave-start が有効にならなかったので STOP SLAVE をしている

#!/bin/sh
...

mysql -u root -v -e "STOP SLAVE";
mysql -u root -v -e "RESET SLAVE";
mysql -u root -v -e "CHANGE MASTER TO MASTER_HOST='master', MASTER_USER='root', MASTER_PASSWORD='', MASTER_LOG_FILE='${binlogfile}', MASTER_LOG_POS=${position};"
mysql -u root -v -e "START SLAVE;"

mysql -u root -h master -v -e "UNLOCK TABLES;"

動作確認

$ docker-compose exec slave1 mysql -e 'show slave status \G'
...
             Slave_IO_Running: Yes
            Slave_SQL_Running: Yes
...
$ docker-compose exec slave1 mysql -e "select * from sample.hello;"
+----+--------+
| id | lang   |
+----+--------+
|  1 | Golang |
|  2 | SQL    |
|  3 | PHP    |
|  4 | Java   |
|  5 | C      |
+----+--------+
$ docker-compose exec master mysql -e "insert into sample.hello(lang) values ('Scala')"
$ docker-compose exec slave1 mysql -e "select * from sample.hello;"+----+--------+
| id | lang   |
+----+--------+
|  1 | Golang |
|  2 | SQL    |
|  3 | PHP    |
|  4 | Java   |
|  5 | C      |
|  6 | Scala  |
+----+--------+

できてるっぽい。

おわりに

もう少しこのレプリケーション周りのオプションだったり、バイナリログ周りは追っていく必要がありそうだけどとりあえずできた。

*1:MySQL :: MySQL 8.0 Reference Manual :: 13.4.1.2 RESET MASTER Statement https://dev.mysql.com/doc/refman/8.0/en/reset-master.html