MySQL8.0 の CLONE プラグインで色々試してみる

この記事は、[MySQL Advent Calendar 2019](https://qiita.com/advent-calendar/2019/mysql)の5日目の記事です。

MySQL 8.0.17 で実装された「[CLONEプラグイン](https://dev.mysql.com/doc/refman/8.0/en/clone-plugin.html)」ですが、登場して以来多くの反響が見受けられます。
その証拠に、既に以下のようにCLONEを取り上げる記事が多く書かれています。

+ [Clone: Create MySQL instance replica](https://mysqlserverteam.com/clone-create-mysql-instance-replica/)
+ [MySQL 8.0.17 Clone Plugin: How to Create a Slave from Scratch](https://www.percona.com/blog/2019/08/01/mysql-8-0-17-clone-plugin-how-to-create-a-slave-from-scratch/)
+ [MySQL 8.0.17でついにCloneプラグインが入った](https://yoku0825.blogspot.com/2019/07/mysql-8017clone.html)
+ [MySQL8.0.17で導入されたCLONEプラグインについて](https://www.s-style.co.jp/blog/2019/09/5090/)

基本的なCLONE機能の使い方については上記で取り上げられているので、この記事ではCLONE機能の細かな部分に着目してみたいと思います。

## 0. 検証環境

今回試す環境は、CentOS7サーバ上に[dbdeployer](https://www.dbdeployer.com/)を用いてお手軽に構築しています。

```sh
$ dbdeployer deploy replication 8.0.18
$ ./rsandbox_8_0_18/use_all -e "INSTALL PLUGIN clone SONAME 'mysql_clone.so'"
```

ちなみにdbdeployerでもCLONE機能を正式サポートしていて、レプリケーションスレーブの作成などに利用できます。
→ 詳しくは公式ページの『Cloning databases』の章などを読んでください

デプロイ後、簡単にテストデータを作成します。

```sh
$ ./rsandbox_8_0_18/master/use -e "CREATE DATABASE d1"
$ ./rsandbox_8_0_18/master/use d1
```

```mysql
mysql> CREATE TABLE t1 (id int, col1 char(10));
mysql> CREATE TABLE t2 (id int, col2 char(10), col3 text);
mysql> INSERT INTO t1 VALUES (1,"aaa"),(2,"bbb");
mysql> INSERT INTO t2 VALUES (10,"A","This is test."),(20,"X","これはテストです。");
```

## 1. CLONEデータを部分リストアしてみる

CLONE機能では以下のバックアップ（スナップショット）を取得します。対象はInnoDBテーブルに限定されます。

・スキーマ
・テーブル
・テーブルスペース
・データディクショナリのメタデータ

また、この時の取得範囲は必ず「全テーブル」となりますが、取得したCLONEデータの一部のテーブルのみを復元させることができないか試してみました。

```sh
$ mkdir ./rsandbox_8_0_18/clone_data
$ ./rsandbox_8_0_18/master/use -u root -e "CLONE LOCAL DATA DIRECTORY '<path to sandbox>/rsandbox_8_0_18/clone_data'"
$ ls ./rsandbox_8_0_18/clone_data/
#clone  d1  ib_buffer_pool  ib_logfile0  ib_logfile1  ibdata1  mysql  mysql.ibd  sys  undo_001  undo_002
```

このうち、d1.t2テーブルのみを新しくデプロイしたインスタンス（以下、新DB）にリストアします。

```sh
$ dbdeployer deploy single 8.0.18
```

(1) 単純に`t2.ibd`をコピーしてみる

まずは単純に新DBのデータディレクトリ上に.ibdファイルをコピーしてみます。
ただ単にコピーしただけではそもそもコピーしてきた`d1`ディレクトリがデータベースとして認識されません 

```sh
$ ./msb_8_0_18/stop 
$ cp -r ./rsandbox_8_0_18/clone_data/d1/ ./msb_8_0_18/data/
$ rm ./msb_8_0_18/data/d1/t1.ibd
$ ./msb_8_0_18/start
...... sandbox server started
$ ./msb_8_0_18/use -e "SELECT * FROM d1.t2"
ERROR 1049 (42000) at line 1: Unknown database 'd1'
```

(2) システムテーブル情報をコピーする

各テーブルのメタ情報などはMySQLのシステムテーブルが保持しています。これは mysql データベースにまとまっているので、クローンデータからまとめてコピーしてみると、t2テーブルが動作しました。

```sh
$ ./msb_8_0_18/stop 
$ cp -r ./rsandbox_8_0_18/clone_data/mysql ./msb_8_0_18/data/
$ cp ./rsandbox_8_0_18/clone_data/mysql.ibd ./msb_8_0_18/data/
$ ./msb_8_0_18/start
.... sandbox server started
$ ./msb_8_0_18/use -e "INSERT INTO d1.t2 VALUES (100,'XXX','test test')"
$ ./msb_8_0_18/use -e "SELECT * FROM d1.t2"
+------+------+-----------------------------+
| id   | col2 | col3                        |
+------+------+-----------------------------+
|  100 | XXX  | test test                   |
|   10 | A    | This is test.               |
|   20 | X    | これはテストです。          |
+------+------+-----------------------------+
```

もちろん、既存のシステムテーブルが上書きされる点に注意してください。また、システムテーブル上は t1 テーブルも存在していることになっているので、気になる場合は DROP TABLE でメタデータを消してしまいましょう。

```sh

$ ./msb_8_0_18/use -e "SHOW TABLES FROM d1"
+--------------+
| Tables_in_d1 |
+--------------+
| t1           |
| t2           |
+--------------+
$ ./msb_8_0_18/use -e "SELECT * FROM d1.t1"
ERROR 1812 (HY000) at line 1: Tablespace is missing for table `d1`.`t1`.
$ ./msb_8_0_18/use -e "DROP TABLE d1.t1"
$ ./msb_8_0_18/use -e "SHOW TABLES FROM d1"
+--------------+
| Tables_in_d1 |
+--------------+
| t2           |
+--------------+
```

ちなみに、空のテーブルだけ用意してそこに.ibdファイルを上書きする方法ではダメでした。

```shell
$ ./msb_8_0_18/use -e "DROP DATABASE d1"
$ ./msb_8_0_18/use -e "CREATE DATABASE d1"
$ ./msb_8_0_18/use -e "CREATE TABLE d1.t2 (id int, col2 char(10), col3 text)"

$ ./msb_8_0_18/stop 
$ cp ./rsandbox_8_0_18/clone_data/d1/t2.ibd ./msb_8_0_18/data/d1/
$ ./msb_8_0_18/start 
................................................................................................................................................................................... sandbox server not started yet

```

(3) トランザクションログ（Redoログ）をコピーする

今回はDBにアクセスが無い状態でCLONEデータを取得したので、トランザクションログ（Redoログ）に更新がないので無視して構いません。ただ、実際の環境ではオンライン状態でCLONEを取得すると思うので、これらも合わせてコピーする必要がありそうです。

```
$ ls ./rsandbox_8_0_18/clone_data/
#clone  d1  ib_buffer_pool  ib_logfile0  ib_logfile1  ibdata1  mysql  mysql.ibd  sys  undo_001  undo_002
```

## 2. 他バージョンのCLONEデータをリストアしてみる

次にバージョンが異なるMySQLから取得したCLONEデータをリストアした場合にどのような挙動になるのかを、8.0.17 / 8.0.18 で確認してみました。

#### ■ 8.0.18 → 8.0.17

8.0.18で取得したCLONEデータを、8.0.17でリストアします。

※ CLONEデータは上記で作ったものを流用します

```sh
$ dbdeployer deploy single 8.0.17
$ ./msb_8_0_17/stop
$ cp -r ./rsandbox_8_0_18/clone_data/* ./msb_8_0_17/data/
$ ./msb_8_0_17/start
................................................................................................................................................................................... sandbox server not started yet
```

エラーで起動できませんでした。エラーログを見ると、データディクショナリのバージョンの不一致が原因と出ており、ダウングレードはできない仕様のようです。

```sh
$ tail ./msb_8_0_17/data/msandbox.err
...
2019-12-04T08:35:30.225093Z 1 [ERROR] [MY-013171] [InnoDB] Cannot boot server version 80017 on data directory built by version 80018. Downgrade is not supported
2019-12-04T08:35:35.318429Z 1 [ERROR] [MY-010334] [Server] Failed to initialize DD Storage Engine
2019-12-04T08:35:35.318880Z 0 [ERROR] [MY-010020] [Server] Data Dictionary initialization failed.
2019-12-04T08:35:35.319736Z 0 [ERROR] [MY-010119] [Server] Aborting
```

#### ■ 8.0.17 → 8.0.18

次に 8.0.17 のCLONEデータを8.0.18でリストアしてみます。

```sh
$ dbdeployer delete ./msb_8_0_17/
$ dbdeployer deploy single 8.0.17
$ ./msb_8_0_17/use -e "INSTALL PLUGIN clone SONAME 'mysql_clone.so'"
$ ./msb_8_0_17/use -e "CREATE DATABASE d1"
$ ./msb_8_0_17/use -e "CREATE TABLE d1.t1 (id int)"
$ ./msb_8_0_17/use -e "INSERt INTO d1.t1 VALUES (100)"
$ ./msb_8_0_17/use -u root -e "CLONE LOCAL DATA DIRECTORY '<path to sandbox>/msb_8_0_17/clone_data'"

$ dbdeployer delete ./msb_8_0_18/
$ dbdeployer deploy single 8.0.18
$ ./msb_8_0_18/stop
$ cp -r ./msb_8_0_17/clone_data/* ./msb_8_0_18/data/
$ ./msb_8_0_18/start
.............. sandbox server started
```

このパターンでは起動できました。どうやら、8.0.17 → 8.0.18 のようなアップグレードのケースではCLONEが利用できそうです。エラーログにも問題なくアップグレードできたメッセージが記録されていました。

```sh
$ ./msb_8_0_18/use -e "SELECT * FROM d1.t1"
+------+
| id   |
+------+
|  100 |
+------+

$ tail ./msb_8_0_18/data/msandbox.err 
…
2019-12-04T09:17:03.597265Z 4 [System] [MY-013381] [Server] Server upgrade from '80017' to '80018' started.
2019-12-04T09:17:10.635010Z 4 [System] [MY-013381] [Server] Server upgrade from '80017' to '80018' completed.
```

なお、以下の公式マニュアルの通り、リモートでCLONEを実行する場合はDONORとRECIPIENTのバージョンは統一する必要があるようです。

[5.6.7.13 Clone Plugin Limitations](https://dev.mysql.com/doc/refman/8.0/en/clone-plugin-limitations.html)
> An instance cannot be cloned from a different MySQL server version. The donor and recipient must have the same MySQL server version.

## 3. CLONE実行中に設定を変更してみる

最後はCLONE実行中にCLONE関連のパラメータ変数を変更するとどのような挙動になるか確認したいと思います。

今回は、[clone_max_concurrency](https://dev.mysql.com/doc/refman/8.0/en/clone-plugin-options-variables.html#sysvar_clone_max_concurrency)変数をCLONE実行中に変更して、データ転送の速度を制御できないか試してみます。

まずは大きめのテストデータを生成しましょう。

```sh
$ ./msb_8_0_18/use d1 -e "CREATE TABLE large_t \
  (id int auto_increment primary key, col1 text, col2 text, col3 text)"
$ ./msb_8_0_18/use d1 -e "INSERT INTO large_t(col1,col2,col3) SELECT \
  SUBSTRING(MD5(RAND()),1,100),SUBSTRING(MD5(RAND()),1,100),SUBSTRING(MD5(RAND()),1,100)"

### 以下を何度か繰り返す

$ ./msb_8_0_18/use d1 -e "INSERT INTO large_t(col1,col2,col3) SELECT \
  SUBSTRING(MD5(RAND()),1,100),SUBSTRING(MD5(RAND()),1,100),SUBSTRING(MD5(RAND()),1,100) \
  FROM large_t"
...
$ ./msb_8_0_18/use d1 -e "SELECT count(*) FROM large_t"
+----------+
| count(*) |
+----------+
| 33554432 |
+----------+
```

clone_max_concurrencyを"1"にした後にCLONEをバックグラウンド実行し、同変数を"16"（デフォルト）に戻した時の挙動を確認します。
また、この時CLONE操作の自動拡張（[clone_autotune_concurrency](https://dev.mysql.com/doc/refman/8.0/en/clone-plugin-options-variables.html#sysvar_clone_autotune_concurrency))もOFFにしておきます。

※ これらの変数はリモートCLONE実行時のRECIPIENTノードで機能します

```sh
### 別のMySQL8.0.18のDONOR関連の設定を変更する

$ ./rsandbox_8_0_18/node2/use -e "SET GLOBAL clone_max_concurrency = 2; SET GLOBAL clone_autotune_concurrency=OFF"
$ ./rsandbox_8_0_18/node2/use -e "SHOW GLOBAL VARIABLES LIKE 'clone_max_concurrency'"
+-----------------------+-------+
| Variable_name         | Value |
+-----------------------+-------+
| clone_max_concurrency | 2     |
+-----------------------+-------+

### リモートCLONEをバックグラウンド実行する

$ ./rsandbox_8_0_18/node2/use -e "SET GLOBAL clone_valid_donor_list = '127.0.0.1:8018'"
$ ./rsandbox_8_0_18/node2/use -e "CLONE INSTANCE FROM root@127.0.0.1:8018 IDENTIFIED BY 'msandbox'" &

### cloneを実行する（Sending to client）スレッド数が"2"であることを確認する

$ ./msb_8_0_18/use -e "SHOW PROCESSLIST"
+-----+-----------------+-----------------+------+---------+------+------------------------+------------------+
| Id  | User            | Host            | db   | Command | Time | State                  | Info             |
+-----+-----------------+-----------------+------+---------+------+------------------------+------------------+
|   5 | event_scheduler | localhost       | NULL | Daemon  | 7449 | Waiting on empty queue | NULL             |
| 122 | root            | localhost:33624 | NULL | clone   |   18 | Sending to client      | NULL             |
| 123 | root            | localhost:33626 | NULL | clone   |   17 | Receiving from client  | NULL             |
| 124 | root            | localhost:33644 | NULL | clone   |   16 | Sending to client      | NULL             |
| 127 | msandbox        | localhost       | NULL | Query   |    0 | starting               | SHOW PROCESSLIST |
+-----+-----------------+-----------------+------+---------+------+------------------------+------------------+

### clone_max_concurrencyを16に戻す

$ ./rsandbox_8_0_18/node2/use -e "SET GLOBAL clone_max_concurrency = 16"
$ ./msb_8_0_18/use -e "SHOW PROCESSLIST"
+-----+-----------------+-----------------+------+---------+------+------------------------+------------------+
| Id  | User            | Host            | db   | Command | Time | State                  | Info             |
+-----+-----------------+-----------------+------+---------+------+------------------------+------------------+
|   5 | event_scheduler | localhost       | NULL | Daemon  | 7469 | Waiting on empty queue | NULL             |
| 122 | root            | localhost:33624 | NULL | clone   |   38 | Sending to client      | NULL             |
| 123 | root            | localhost:33626 | NULL | clone   |   37 | Receiving from client  | NULL             |
| 124 | root            | localhost:33644 | NULL | clone   |   36 | Sending to client      | NULL             |
| 132 | msandbox        | localhost       | NULL | Query   |    0 | starting               | SHOW PROCESSLIST |
+-----+-----------------+-----------------+------+---------+------+------------------------+------------------+
```

流石にCLONE実行中に設定を変更しても即時反映されることは無さそうです。事前に必要なネットワーク帯域などを考慮する必要がありますね。

## おわりに

今回はパッと思いつくアイデアを試してみましたが、CLONE機能にはまだまだ判明していない挙動や仕様がありそうでワクワクします。
是非、積極的に試してみて各種ブログなどでシェアしてみてください！（特に本番環境での利用実績！）