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

僕と MySQL と時々 MariaDB

MySQL 8.0.19 と docker-compose 環境下で Single-Primary InnoDB Cluster を構築する

はじめに

InnoDB Cluster がどういうものか知ってはいるが構築したことがなかったのでとりあえず構築してみた。
github.com
今回は MySQL Shell をホストマシンに置き、MySQL Server x 3, MySQL Router を docker コンテナで建てる。
MySQL Shell のコンテナを使っても良いが、手動で行いたいため今回はホストマシン上で操作する。またこの都合により、MySQL Shellが関連する docker コンテナには固定で IPv4 のアドレスを割り当てている。

環境

  • mysql 8.0.19
  • ubuntu 18.04
  • docker 19.03.5
  • docker-compose 1.23.0

参考資料

dev.mysql.com

InnoDB Cluster の構築

InnoDB Cluster とは

MySQL InnoDB Cluster とは MySQL Group Replication + MySQL Router + MySQL Shell で構築可能な高可用性ソリューションを指す。


Group Replication がベースとなっているため、構築時における要求や制限は Group Replication のものに左右される。*1
また基本的に InnoDB Cluster の基幹は Group Replication であるがそれを InnoDB Cluster として、構成し管理するための AdminAPI が MySQL Shell により提供されるものを利用する。これにより Group Replication を直接操作することなく InnoDB Cluster
を操作出来る。

要件

まず Group Replication を使用するため、サーバインスタンスも Group Replication と同様の要件を満たす必要がある。実際に要件を満たしているかどうかを AdminAPI の dba.checkInstanceConfiguration() で確認する。

ストレージエンジンに関しては InnoDB 以外の MyISAM 等を選択することが Group Replication では可能であるが InnoDB Cluster 構築時は InnoDB のみでメンバを構成する必要がある。またそれと同時に全てのインスタンスでパフォーマンススキーマを有効にする必要がある。

また、バージョン 8.0.17 以降では server_id を通常のレプリケーション構築時と同様に InnoDB Cluster 内で一意に設定する必要がある。

MySQL Shell の導入

手元に MySQL Shell がなかったので以下のドキュメントを参考に導入する。
dev.mysql.com

各サーバインスタンスの設定を修正する

MySQL Shell で dba.checkInstanceConfiguration() を実行することで確認できる。

 mysqlsh -u root -h 127.0.0.1 -p -P 33060
Please provide the password for 'root@127.0.0.1:33060': ****
MySQL Shell 8.0.19

Copyright (c) 2016, 2019, Oracle and/or its affiliates. All rights reserved.
Oracle is a registered trademark of Oracle Corporation and/or its affiliates.
Other names may be trademarks of their respective owners.

Type '\help' or '\?' for help; '\quit' to exit.
Creating a session to 'root@127.0.0.1:33060'
Fetching schema names for autocompletion... Press ^C to stop.
Your MySQL connection id is 13
Server version: 8.0.19 MySQL Community Server - GPL
No default schema selected; type \use <schema> to set one.
 MySQL  127.0.0.1:33060 ssl  JS > dba.checkInstanceConfiguration();
Validating local MySQL instance listening at port 33060 for use in an InnoDB cluster...

This instance reports its own address as 7147fa7c00f5:33060
Clients and other cluster members will communicate with it through this address by default. If this is not correct, the report_host MySQL system variable should be changed.

Checking whether existing tables comply with Group Replication requirements...
No incompatible tables detected

Checking instance configuration...

NOTE: Some configuration options need to be fixed:
+--------------------------+---------------+----------------+--------------------------------------------------+
| Variable                 | Current Value | Required Value | Note                                             |
+--------------------------+---------------+----------------+--------------------------------------------------+
| binlog_checksum          | CRC32         | NONE           | Update the server variable                       |
| enforce_gtid_consistency | OFF           | ON             | Update read-only variable and restart the server |
| gtid_mode                | OFF           | ON             | Update read-only variable and restart the server |
+--------------------------+---------------+----------------+--------------------------------------------------+

Some variables need to be changed, but cannot be done dynamically on the server.
NOTE: Please use the dba.configureInstance() command to repair these issues.

{
    "config_errors": [
        {
            "action": "server_update", 
            "current": "CRC32", 
            "option": "binlog_checksum", 
            "required": "NONE"
        }, 
        {
            "action": "server_update+restart", 
            "current": "OFF", 
            "option": "enforce_gtid_consistency", 
            "required": "ON"
        }, 
        {
            "action": "server_update+restart", 
            "current": "OFF", 
            "option": "gtid_mode", 
            "required": "ON"
        }
    ], 
    "status": "error"
}

binlog_checksum, enforce_gtid_consistency, gtid_mode の設定を修正する必要があるみたい。

binlog_checksum

このオプションではバイナリログの各イベントにチェックサムを付与することが可能になる。デフォルトは CRC32。
マスタースレーブ環境下でスレーブに認識されない異なるチェックサムの形式を指定している場合にスレーブが自身の binlog_checksum を NONE に変更しレプリケーションを停止する。後方互換性において懸念がある場合は明示的に NONE を設定する。
https://dev.mysql.com/doc/refman/8.0/en/replication-options-binary-log.html#sysvar_binlog_checksum

enforce_gtid_consistency

GTID を使用したレプリケーションを行う場合にログを確実に記録できる構文のみ実行を許可することで強制的に GTID の一貫性を確保したい場合に設定する。ON, OFF 以外に WARN があり、その場合は一貫性に反することが許容されるが警告が出るようになる。
https://dev.mysql.com/doc/refman/8.0/en/replication-options-gtids.html#sysvar_enforce_gtid_consistency

gtid_mode

GTID ベースのログを有効にするかどうかの設定を行う。
https://dev.mysql.com/doc/refman/8.0/en/replication-options-gtids.html#sysvar_gtid_mode

InnoDB Cluster の構築

InnoDB Cluster は MySQL Shell から、AdminAPI を使いサーバインスタンスを操作することで構築可能となっている。
手順自体は簡単で

  1. dba.createCluster('hoge'); でクラスタを作成
  2. dba.addInstance(other_node_option) でノードを追加する

これだけとなっている。
ただここにたどり着くまでの設定などで docker を前提とする場合いくつかの罠が存在したり
公式ドキュメントにはないが Group Replication の設定を追加する必要がある。それは最後にまとめて記述する。

まずは InnoDB クラスタを作成する。

 MySQL  172.30.0.60:3306 ssl  JS > var cluster = dba.createCluster('singlePrimaryCluster');
A new InnoDB cluster will be created on instance '172.30.0.60:3306'.

Validating instance configuration at 172.30.0.60:3306...

This instance reports its own address as 172.30.0.60:3306

Instance configuration is suitable.
NOTE: Group Replication will communicate with other members using '172.30.0.60:33061'. Use the localAddress option to override.

Creating InnoDB cluster 'singlePrimaryCluster' on '172.30.0.60:3306'...

Adding Seed Instance...
Cluster successfully created. Use Cluster.addInstance() to add MySQL instances.
At least 3 instances are needed for the cluster to be able to withstand up to
one server failure.

createCluster は無事に完了したのでステータスを確認する。

 MySQL  172.30.0.60:3306 ssl  JS > cluster.status();
{
    "clusterName": "singlePrimaryCluster", 
    "defaultReplicaSet": {
        "name": "default", 
        "primary": "172.30.0.60:3306", 
        "ssl": "REQUIRED", 
        "status": "OK_NO_TOLERANCE", 
        "statusText": "Cluster is NOT tolerant to any failures.", 
        "topology": {
            "172.30.0.60:3306": {
                "address": "172.30.0.60:3306", 
                "mode": "R/W", 
                "readReplicas": {}, 
                "replicationLag": null, 
                "role": "HA", 
                "status": "ONLINE", 
                "version": "8.0.19"
            }
        }, 
        "topologyMode": "Single-Primary"
    }, 
    "groupInformationSourceMember": "172.30.0.60:3306"
}

大丈夫っぽい。

次にこのプライマリに対してノードを2つ追加する。

 MySQL  172.30.0.60:3306 ssl  JS > cluster.addInstance('root@172.30.0.68:3308')

WARNING: A GTID set check of the MySQL instance at '172.30.0.68:3308' determined that it contains transactions that do not originate from the cluster, which must be discarded before it can join the cluster.

172.30.0.68:3308 has the following errant GTIDs that do not exist in the cluster:
98f05a53-691a-11ea-9705-0242ac1e0044:1-9

WARNING: Discarding these extra GTID events can either be done manually or by completely overwriting the state of 172.30.0.68:3308 with a physical snapshot from an existing cluster member. To use this method by default, set the 'recoveryMethod' option to 'clone'.

Having extra GTID events is not expected, and it is recommended to investigate this further and ensure that the data can be removed prior to choosing the clone recovery method.

Please select a recovery method [C]lone/[A]bort (default Abort): C
NOTE: Group Replication will communicate with other members using '172.30.0.68:33081'. Use the localAddress option to override.

Validating instance configuration at 172.30.0.68:3308...

This instance reports its own address as 172.30.0.68:3308

Instance configuration is suitable.
A new instance will be added to the InnoDB cluster. Depending on the amount of
data on the cluster this might take from a few seconds to several hours.

Adding instance to the cluster...

Monitoring recovery process of the new cluster member. Press ^C to stop monitoring and let it continue in background.
Clone based state recovery is now in progress.

NOTE: A server restart is expected to happen as part of the clone process. If the
server does not support the RESTART command or does not come back after a
while, you may need to manually start it back.

* Waiting for clone to finish...
NOTE: 172.30.0.68:3308 is being cloned from 172.30.0.60:3306
** Stage DROP DATA: Completed
** Clone Transfer  
    FILE COPY  ############################################################  100%  Completed
    PAGE COPY  ############################################################  100%  Completed
    REDO COPY  ############################################################  100%  Completed
** Stage RECOVERY: \
NOTE: 172.30.0.68:3308 is shutting down...

* Waiting for server restart... ready
* 172.30.0.68:3308 has restarted, waiting for clone to finish...
* Clone process has finished: 68.01 MB transferred in about 1 second (~68.01 MB/s)

Incremental state recovery is now in progress.

* Waiting for distributed recovery to finish...
NOTE: '172.30.0.68:3308' is being recovered from '172.30.0.60:3306'
* Distributed recovery has finished

The instance '172.30.0.68:3308' was successfully added to the cluster.

 MySQL  172.30.0.60:3306 ssl  JS > cluster.addInstance('root@172.30.0.69:3309')

WARNING: A GTID set check of the MySQL instance at '172.30.0.69:3309' determined that it contains transactions that do not originate from the cluster, which must be discarded before it can join the cluster.

172.30.0.69:3309 has the following errant GTIDs that do not exist in the cluster:
99fe015f-691a-11ea-bc83-0242ac1e0045:1-9

WARNING: Discarding these extra GTID events can either be done manually or by completely overwriting the state of 172.30.0.69:3309 with a physical snapshot from an existing cluster member. To use this method by default, set the 'recoveryMethod' option to 'clone'.

Having extra GTID events is not expected, and it is recommended to investigate this further and ensure that the data can be removed prior to choosing the clone recovery method.

Please select a recovery method [C]lone/[A]bort (default Abort): C
NOTE: Group Replication will communicate with other members using '172.30.0.69:33091'. Use the localAddress option to override.

Validating instance configuration at 172.30.0.69:3309...

This instance reports its own address as 172.30.0.69:3309

Instance configuration is suitable.
A new instance will be added to the InnoDB cluster. Depending on the amount of
data on the cluster this might take from a few seconds to several hours.

Adding instance to the cluster...

Monitoring recovery process of the new cluster member. Press ^C to stop monitoring and let it continue in background.
Clone based state recovery is now in progress.

NOTE: A server restart is expected to happen as part of the clone process. If the
server does not support the RESTART command or does not come back after a
while, you may need to manually start it back.

* Waiting for clone to finish...
NOTE: 172.30.0.69:3309 is being cloned from 172.30.0.60:3306
** Stage DROP DATA: Completed
** Clone Transfer  
    FILE COPY  ############################################################  100%  Completed
    PAGE COPY  ############################################################  100%  Completed
    REDO COPY  ############################################################  100%  Completed
** Stage RECOVERY: \
NOTE: 172.30.0.69:3309 is shutting down...

* Waiting for server restart... ready
* 172.30.0.69:3309 has restarted, waiting for clone to finish...
* Clone process has finished: 68.01 MB transferred in about 1 second (~1.00 B/s)

Incremental state recovery is now in progress.

* Waiting for distributed recovery to finish...
NOTE: '172.30.0.69:3309' is being recovered from '172.30.0.60:3306'
* Distributed recovery has finished

The instance '172.30.0.69:3309' was successfully added to the cluster.

これで良い。 設定さえ問題なければ cluster.addInstance を実行するだけでノードは追加できる。

 MySQL  172.30.0.60:3306 ssl  JS > cluster.status();
{
    "clusterName": "singlePrimaryCluster", 
    "defaultReplicaSet": {
        "name": "default", 
        "primary": "172.30.0.60:3306", 
        "ssl": "REQUIRED", 
        "status": "OK", 
        "statusText": "Cluster is ONLINE and can tolerate up to ONE failure.", 
        "topology": {
            "172.30.0.60:3306": {
                "address": "172.30.0.60:3306", 
                "mode": "R/W", 
                "readReplicas": {}, 
                "replicationLag": null, 
                "role": "HA", 
                "status": "ONLINE", 
                "version": "8.0.19"
            }, 
            "172.30.0.68:3308": {
                "address": "172.30.0.68:3308", 
                "mode": "R/O", 
                "readReplicas": {}, 
                "replicationLag": null, 
                "role": "HA", 
                "status": "ONLINE", 
                "version": "8.0.19"
            }, 
            "172.30.0.69:3309": {
                "address": "172.30.0.69:3309", 
                "mode": "R/O", 
                "readReplicas": {}, 
                "replicationLag": null, 
                "role": "HA", 
                "status": "ONLINE", 
                "version": "8.0.19"
            }
        }, 
        "topologyMode": "Single-Primary"
    }, 
    "groupInformationSourceMember": "172.30.0.60:3306"
}

ステータスにも反映されていることが確認できる。

ここまで出来たら次は、このクラスターへのアクセスに MySQL Router を挟む。

オプションの類

MySQL Router に関する色々をやるまえに、 ここまでに必要だった設定達をまとめる。
セカンダリとプライマリはほとんど同じ設定を使っているため、プライマリの設定を挙げて書く。

skip-name-resolve=ON
bind_address="172.30.0.60"
report-host="172.30.0.60"
loose-group_replication_local_address="172.30.0.60:3316"
loose-group_replication_local_seeds="172.30.0.60:3316"
loose-group_replication_start_on_boot=OFF

*** ネットワーク周り

ホスト名を使用しないで IP アドレスを使う。
ただし、今回のように docker コンテナとして存在する MySQL に対してホストマシンの MySQL Shell から操作する場合、createCluster 時に何故か名前解決しようとして詰まった。
これは report-host で IP アドレスを指定することで解決した。

group_replication 関連

start_on_boot は、サーバ起動時に自動的にグループレプリケーションを開始するかどうかというあれ。構築時は OFF にする必要があるらしい。
local_address はグループレプリケーションが使用するアドレスとポートを明記する。アドレスは大体サーバと同じだがポートは MySQL サーバが使用しているものとは別のものにする必要がある。

MySQL Router

MySQL Router はいい感じの Docker コンテナがあったのでそれを使う。

lrf141@lrf141-ThinkPad-X220:~/infraProject/mysql-innodb-cluster-dc$ docker exec -it mysql-innodb-cluster-dc_router_1_8c5f6a8ebeca bash
bash-4.2# mysqlrouter --bootstrap root@master1 --user=root
Please enter MySQL password for root: 
# Bootstrapping system MySQL Router instance...

- Creating account(s) (only those that are needed, if any)
- Verifying account (using it to run SQL queries that would be run by Router)
- Storing account in keyring
- Adjusting permissions of generated files
- Creating configuration /etc/mysqlrouter/mysqlrouter.conf

Existing configuration backed up to '/etc/mysqlrouter/mysqlrouter.conf.bak'

# MySQL Router configured for the InnoDB Cluster 'singlePrimaryCluster'

After this MySQL Router has been started with the generated configuration

    $ /etc/init.d/mysqlrouter restart
or
    $ mysqlrouter -c /etc/mysqlrouter/mysqlrouter.conf

the cluster 'singlePrimaryCluster' can be reached by connecting to:

## MySQL Classic protocol

- Read/Write Connections: localhost:6446
- Read/Only Connections:  localhost:6447

## MySQL X protocol

- Read/Write Connections: localhost:64460
- Read/Only Connections:  localhost:64470

これが表示されるならオッケーなはず。
MySQL Router を通して InnoDB Cluster を利用するなら、ここでは localhost:6446,6447 に対してリクエストを投げる。

終わりに

一応できたけども、InnoDB Cluster が正常に起動した状態で docker-compose down でコンテナを落としてから再起動すると
スタンドアロンモードでプライマリが起動し、セカンダリが自動で接続されない現象が起こってしまったのでどうにかしたい。