MySQL の準同期レプリケーション + マルチスレッドレプリケーションを ansible で組む

MySQL 5.6 から入った以下の２つの機能を試すための環境構築を自動化するために ansible で書いた。

- 準同期レプリケーション
- マルチスレッドレプリケーション

CentOS7 向けに書いてるので、CentOS7 が予め入った環境に `site.yml` を流すことで出来るだろう。なお、Ansible 2.3.2 を使ったが、もしかしたらもうちょっと低いバージョンでもいけるかもしれない。

```bash
# 必要に応じて --user と --private-key を入れる
ansible-playbook -i inventories/mysql site.yml
```

以下 ansible のコードを解説付きで記述する。

#### site.yml

MySQL ホストに MySQL ロールを流す。`inventories/mysql` の中身に依存しているので変更に弱い問題がある。

```yaml:site.yml
---
- hosts: mysql
  roles:
    - mysql
  become: yes
  serial: 1
```

#### group_vars/all.yml

内部 IP とレプリカのマスター IP のマッピング情報を格納する。

```yaml:group_vars/all.yml
internal_ip_map:
  mysql-01: "(マスターとなる内部の IP に差し替えること)"
  mysql-02: "(レプリカとなる内部の IP に差し替えること)"

replication_master_map:
  mysql-02: "(マスターとなる内部の IP に差し替えること)"
```

#### inventories/mysql

ホスト名 (`mysql-01` と `mysql-02`)は適当につけてるので、変更してよいが、`[mysql-master]` と `[mysql-replica]` の名前部分、`[mysql:children]` の名前部分及びその中身はいじらないこと（動かなくなる）。また、`ansible_host` の部分の書き換えもお忘れなく。

```ini:inventories/mysql
mysql-01 mysql_server_id=1 ansible_host=(マスターとなる IP アドレスに差し替えること)
mysql-02 mysql_server_id=2 ansible_host=(レプリカとなる IP アドレスに差し替えること)

[mysql-master]
mysql-01

[mysql-replica]
mysql-02

[mysql:children]
mysql-master
mysql-replica
```

#### roles/mysql/defaults/main.yml

パスワードはここではべた書きしてるが、本来は [Ansible Vault](http://docs.ansible.com/ansible/latest/playbooks_vault.html) 使って外部化するべきである。

```yaml:roles/mysql/defaults/main.yml
mysql_user_uid: 10000
mysql_user_gid: 10000

mysql_root_password: root
mysql_repl_password: repl
```

#### roles/mysql/handlers/main.yml

systemd を使って MySQL の再起動を行う

```yaml:roles/mysql/handlers/main.yml
---
- name: restart mysql
  systemd:
    name: mysqld.service
    state: restarted
    daemon_reload: yes
```

#### roles/mysql/tasks/main.yml

メインとなる部分。これは以下のフローで MySQL サーバを構築する。

- MySQL ユーザとグループの作成
- yum レポジトリ経由で MySQL 5.7 系の最新版のクライアント及びサーバをインストール
  - 最初から内蔵されてる MariaDB とのパッケージ衝突対策のため予め MariaDB のライブラリを削除する
- MySQL のデータディレクトリを確認し、初期化されてなければ初期化する
  - パッケージデフォルトの`/var/lib/mysql` 直指定
- サーバ側の my.cnf デプロイ
- MySQL サーバ起動
- root パスワード変更
- クライアント側の my.cnf デプロイ
- マスター及びレプリカの処理に分岐

```yaml:roles/mysql/tasks/main.yml
---
- name: create mysql group
  group:
    name: mysql
    gid: "{{ mysql_user_gid }}"
    state: present

- name: create mysql user
  user:
    name: mysql
    uid: "{{ mysql_user_uid }}"
    group: mysql
    shell: /bin/false
    state: present

- name: fetch yum repository
  yum:
    name: https://dev.mysql.com/get/mysql57-community-release-el7-11.noarch.rpm

- name: remove mariadb library due to confliction
  yum:
    name: mariadb-libs
    state: absent

- name: install mysql packages
  yum:
    name: "{{ item }}"
    state: installed
  with_items:
    - mysql-community-client
    - mysql-community-libs
    - mysql-community-libs-compat
    - mysql-community-server
    - MySQL-python

- name: check mysql database is initialized
  find:
    paths: /var/lib/mysql
  register: mysql_database_directory

- name: set initialization state
  set_fact:
    mysql_has_been_initialized: "{{ mysql_database_directory.files | length > 0 }}"

- name: deploy server my.cnf
  template:
    src: server-my.cnf.j2
    dest: /etc/my.cnf
    mode: 0600
    owner: mysql
    group: mysql
  notify: restart mysql

- name: ensure mysql database initialized
  shell: mysqld --user=mysql --initialize-insecure
  when: 'not mysql_has_been_initialized'

- name: start mysql server
  systemd:
    name: mysqld.service
    state: started
    daemon_reload: yes

- name: change root user password
  mysql_user:
    name: root
    password: "{{ mysql_root_password }}"
    state: present
    login_host: localhost
    login_user: root
    host_all: yes

- name: deploy client my.cnf for current user
  template:
    src: client-my.cnf.j2
    dest: "/home/{{ ansible_user }}/.my.cnf"
    owner: "{{ ansible_user }}"
    group: "{{ ansible_user }}"
    mode: 0600

- name: deploy client my.cnf for root user
  template:
    src: client-my.cnf.j2
    dest: "/root/.my.cnf"
    owner: root
    group: root
    mode: 0600

- include: master.yml
  when: '"mysql-master" in group_names'

- include: replica.yml
  when: '"mysql-replica" in group_names'
```

#### roles/mysql/tasks/master.yml

マスター側は以下の処理を行う

- レプリケーションユーザの作成
- 匿名ユーザとテスト用データベースの削除

```yaml:roles/mysql/tasks/master.yml
---
- name: create replication user
  mysql_user:
    name: repl
    password: "{{ mysql_repl_password }}"
    host: "{{ internal_ip_map[item] }}"
    priv: '*.*:SUPER,REPLICATION SLAVE'
    state: present
  with_items:
    - "{{ groups['mysql-replica'] }}"

- name: remove anonymous mysql user
  mysql_user:
    name: ""
    state: absent
    host_all: yes

- name: remove test database
  mysql_db:
    name: test
    state: absent
```

#### roles/mysql/tasks/replica.yml


レプリカ（スレーブとも）側は以下の処理を行う

- レプリケーション停止
- マスターのバイナリログのファイル名と位置を保存
- 上記の情報から `CHANGE MASTER` を発行
- レプリケーション再開


```yaml:roles/mysql/tasks/replica.yml
---
- name: stop replication
  mysql_replication:
    mode: stopslave

- name: get replication master status
  mysql_replication:
    mode: getmaster
    login_host: "{{ replication_master_map[inventory_hostname] }}"
    login_user: repl
    login_password: "{{ mysql_repl_password }}"
  register: replication_master_info

- name: change replication master
  mysql_replication:
    mode: changemaster
    master_host: "{{ replication_master_map[inventory_hostname] }}"
    master_log_file: "{{ replication_master_info.File }}"
    master_log_pos: "{{ replication_master_info.Position }}"
    master_user: repl
    master_password: "{{ mysql_repl_password }}"

- name: start replication
  mysql_replication:
    mode: startslave
```

#### roles/mysql/templates/client-my.cnf.j2

root ユーザでログインするための機密情報を格納している

```ini:roles/mysql/templates/client-my.cnf.j2
# {{ ansible_managed }}

[client]
user = root
password = {{ mysql_root_password }}
```

#### roles/mysql/templates/server-my.cnf.j2

今回のキモとなる部分。

準同期レプリケーションのために以下の項目を入れている。http://qiita.com/fetaro/items/f56c3f4e5efc66692705 を参考にした。

- `plugin_load = "rpl_semi_sync_master=semisync_master.so"`
- `rpl_semi_sync_master_enabled`
  - マスターでの準同期レプリケーションを有効かつ `plugin_load` により構築時から有効にする
- `plugin_load = "rpl_semi_sync_slave=semisync_slave.so"`
- `rpl_semi_sync_slave_enabled`
  - レプリカでの準同期レプリケーションを有効かつ `plugin_load` により構築時から有効にする

マルチスレッドレプリケーションのために以下の項目を入れている。マルチスレッドレプリケーションについての情報は http://qiita.com/smallpalace/items/e9fd924078b663d7616b がまとまってる。

- `slave_parallel_workers`
  - マルチスレッドレプリケーションが有効になる
- `slave_parallel_type`
- `slave_preserve_commit_order`
- `log_slave_updates`
- `gtid_mode`

あくまで準同期レプリケーションとマルチスレッドレプリケーションを有効にするための設定なので特にチューニングしていない

```ini:roles/mysql/templates/server-my.cnf.j2
# {{ ansible_managed }}

[mysqld]
server_id = {{ mysql_server_id }}
gtid_mode = on
enforce-gtid-consistency = on
skip_name_resolve = on
symbolic_links = 0

{% if 'mysql-master' in group_names %}
# replication master
log_bin = mysql-bin
expire_logs_days = 7
binlog_format = ROW
master_info_repository = TABLE

# semi-sync replication
plugin_load = "rpl_semi_sync_master=semisync_master.so"
rpl_semi_sync_master_enabled = 1
rpl_semi_sync_master_timeout = 10000
{% elif 'mysql-replica' in group_names %}
# replication replica
read_only = on
relay_log = relay-bin
relay_log_index = relay-bin.index
relay_log_info_repository = TABLE
relay_log_recovery = on

# semi-sync replication
plugin_load = "rpl_semi_sync_slave=semisync_slave.so"
rpl_semi_sync_slave_enabled = 1

# multi-threaded replication
slave_parallel_workers = 2
slave_parallel_type = LOGICAL_CLOCK
slave_preserve_commit_order = on
log_slave_updates = on
{% endif %}

# innodb
innodb_large_prefix = on
innodb_file_per_table = on

# performance schema
performance_schema = on
```