docker-composeでMaster/Slave構成のMySQLを手に入れる

[OPENLOGI](https://www.openlogi.com/)のtakyamです。
みんなからは、たくやむさんと呼ばれてます。何でこんな読みづらいHNにしたんだろう。

[OPENLOGI AdventCalendar](https://qiita.com/advent-calendar/2017/openlogi)の2日目です。購読してね。

1日目は @guai3 の [スマートスピーカーを使って業務システムを作る(考察編)](https://qiita.com/guai3/items/b7b21e636c84ff23d041) でした。
OPENLOGIは物流プラットフォームということで、普通のWEBサービスではなかなか扱わない技術ネタもあったりするのですが、2日目は普通のWEBサービスらしい技術ネタになります:flushed:
(1日目からあんなガチで来られると思ってなかったんや:innocent:）

さて、

普段のローカル開発ではDocker（docker-compose)を使っています。
開発中はMasterだけで十分なのですが、たまにSlave遅延時の挙動を確認したくなったり、ライブラリの挙動を確認するためにMaster-Slave環境が欲しくなることありますよね。

というわけで普段利用しているdocker-compose.ymlのMySQL周りのファイルを共有です。

※流石にそのまま共有できないので必要な部分だけコピペ&改変してます

なお、レプリケーション設定まわりのノウハウはド素人レベルなので、変なことやってるかもしれませんがご容赦ください:pray:

## 必要なファイル

※この例では同じディレクトリに配置してますけどいい感じの場所に置換してください

```
./master.cnf
./slave.cnf
./docker-compose.yml
./start-slave.sh
```

[サンプルをGithubにおいてます](https://github.com/takyam-git/docker-mysql-replication-example)ので、そちらをcloneしてもらう感じでもOKです。

### my.cnf系

```master.cnf
[mysqld]
character-set-server=utf8
collation-server=utf8_general_ci

# replicate-do-db=特定のDBだけレプリケーションしたい場合は指定する

log-bin=/var/log/mysql/bin-log
server-id=1
```

```slave.cnf
[mysqld]
character-set-server=utf8
collation-server=utf8_general_ci

# replicate-do-db=特定のDBだけレプリケーションしたい場合は指定する

log-bin=/var/log/mysql/bin-log
server-id=2

read_only=1
```

masterとslaveでわけてます。
server-id変えてるだけです。　

### docker-compose.yml

```docker-compose.yml
version: '2'
services:
  mysql:
    image: mysql:5.7
    ports:
      - "3306:3306"
    volumes:
      - mysql-data:/var/lib/mysql #masterはvolumeをmount(永続化させる)
      - ./master.cnf:/etc/mysql/conf.d/my.cnf #master用のmy.cnfをコピー
    environment:
      MYSQL_ALLOW_EMPTY_PASSWORD: 1
      TZ: "Asia/Tokyo"
  mysql-slave:
    image: mysql:5.7
    ports:
      - '13306:3306' #portはmasterと変えておく
    depends_on:
      - mysql #masterに依存させておく
    tmpfs: /var/lib/mysql #tmpfsを使うと爆速・・・！メモリの空き容量とDBの容量と相談。
    volumes:
      - ./slave.cnf:/etc/mysql/conf.d/my.cnf #slave用のmy.cnfをコピー
      - ./start-slave.sh:/docker-entrypoint-initdb.d/start-slave.sh #初期化スクリプトをコピー
    environment:
      MYSQL_ALLOW_EMPTY_PASSWORD: 1
      TZ: "Asia/Tokyo"
  app:
    (略)
    depends_on:
      - mysql
volumes:
  mysql-data:
    external:
      name: mysql_volume
```

- `mysql` は普通のDBなので特に変わったことないですが、my.cnfをmaster.cnfで置換してます。
- `mysql-slave` の設定にはいくつかポイントがあります
    - `ports` でmasterとは別のポートを指定していること（hostからアクセスしないなら別にいいんですが）
    - `depends_on` にmaster(`mysql`)を指定することで、masterの起動を待っていること
    - `/docker-entrypoint-initdb.d` に `start-slave.sh` を置くことで、slave起動時にslave開始のスクリプトを実行していること
    - （これは趣味なんですが）どうせ起動時に同期しなおすので `tmpfs` にすることで爆速な気がすること
- `app` での `depends_on` にmaster（`mysql`）だけを指定しておくと、普段の開発時にはmasterだけしか起動させない、みたいなことも可能です。

### start-slave.sh

```sh:start-slave.sh
#!/bin/sh

# depends_onの設定しておけば気にならないけど念の為masterの起動を待つ
while ! mysqladmin ping -h mysql --silent; do
  sleep 1
done

# masterをロックする
mysql -u root -h mysql -e "RESET MASTER;"
mysql -u root -h mysql -e "FLUSH TABLES WITH READ LOCK;"

# masterのDB情報をDumpする
# ここでは --all-databases にしてるけど用途に応じて必要なDBだけにしていいと思う
mysqldump -uroot -h mysql --all-databases --master-data --single-transaction --flush-logs --events > /tmp/master_dump.sql
# 特定のDBだけにする場合はこんな感じ(my.cnfのreplica-do-dbも忘れずに設定すること)
# mysqldump -uroot -h mysql データベース名 --master-data --single-transaction --flush-logs --events > /tmp/master_dump.sql

# dumpしたmasterのDBをslaveにimportする
mysql -u root -e "STOP SLAVE;";
mysql -u root < /tmp/master_dump.sql

# masterに繋いで bin-logのファイル名とポジションを取得する
log_file=`mysql -u root -h mysql -e "SHOW MASTER STATUS\G" | grep File: | awk '{print $2}'`
pos=`mysql -u root -h mysql -e "SHOW MASTER STATUS\G" | grep Position: | awk '{print $2}'`

# slaveの開始
mysql -u root -e "RESET SLAVE";
mysql -u root -e "CHANGE MASTER TO MASTER_HOST='mysql', MASTER_USER='root', MASTER_PASSWORD='', MASTER_LOG_FILE='${log_file}', MASTER_LOG_POS=${pos};"
mysql -u root -e "start slave"

# masterをunlockする
mysql -u root -h mysql -e "UNLOCK TABLES;"

```

* hostに `mysql` を指定するとmaster、指定しない（あるいはlocalhost）と `slave` を参照するので、slave上でmasterのdumpをとってきたりロックしたりして、slaveを開始するスクリプトです
* いろいろググって書いたので何か余計なことしてるかもしれないのでツッコミあればおねしゃす :bow: 

## 使い方

上記の例の場合、`docker-compose up -d app` みたいな形でアプリを起動させると、`depends_on`に指定されてるmaster(`mysql`)だけが起動し、普段はmasterだけで開発します。
「slaveほしいでござる」って思ったタイミングで `docker-compose up -d mysql-slave` することで、slaveが起動し、そのタイミングのmasterと同期したうえで`start slave`してくれる感じです。

mysqldumpからのインポート処理があるので、起動速度がDBの容量に依存するかたちになるので、そこは注意です。
tmpfs使えるかどうかも、DBの容量とマシンのメモリとの兼ね合いで変わってくる（＆デフォだとDockerのメモリが2GBとかな気がするのでそこの設定変更も必要かも）ので注意。

## 余談
半年前まではdocker-for-macで開発してたんですが、[docker-for-macのパフォーマンスがあまりにも悪く](https://github.com/docker/for-mac/issues/77)、一旦Vagrantに移行しました。
もちろん`:cached`も試したんですが、inotifyが動かなくなることがあったんで断念。

Mac上の Vagrant に [BargeOS](https://github.com/bargees/barge-os) いれて、`fs-notify`やら`rsync` やらくつか試したのですが、これは楽な反面、過剰な同期であったり、いまいちinotifyがうまく動かなかったりしたので、これも断念。

一旦BargeとMacのそれぞれにレポジトリをチェックアウトしておいて、開発開始時にそれぞれブランチの先頭を合わせておき、あとはIntellijのDeployツールでファイル保存時（Cmd+S）にSCPで転送するような形に落ち着きました。

その形でだいぶ落ち着いて開発できていたんですが、それでもやっぱり若干面倒だったので、最終的に現在はThinkpadにUbuntuいれて、Ubuntu上で開発しています。同期に戸惑うこともなく、ノンストレスでDockerが使えるので、Linux最高だな・・・！という思いで日々を過ごしてます:pray: