MySQLの外部レプリケーションを使って最小限のダウンタイムでAWS RDSに移行する

この記事は[ニフティグループ Advent Calendar 2019](https://qiita.com/advent-calendar/2019/nifty)の20日目の記事です。

---

# はじめに
弊社ではWebのインフラとしてAWSの導入を進めており、様々なサービスがAWSに移行されつつあります。
先日、私の担当サービスも無事にAWSへの移行が完了しました。その時に苦戦したDBの移行方法を共有いたします。
なお、`AWS CLI`のインストールやRDSインスタンスの作成手順などは割愛します。
また、この方法では移行先DBから移行元DBへの疎通が通っていることが前提となります。技術的、セキュリティルール的に疎通を通すことが不可能である場合は、以下の手順は実行できませんのでご了承下さい。

# TL;DR
今回移行したDBは以下のようなものです。

- MySQL 5.6
- データ量約200GB
- 常にデータ更新がかかっている（DBの停止=サービス停止）

データ量が多い点と、常に更新が掛かっている点がネックでした。単純にdump/restoreを実行すると約10時間ほどのサービス停止が必要になり、より短い時間での移行を検討する必要がありました（停止しないと移行中も新しいデータが追加されるため、差分が生まれてしまう）。
そこで、MySQLの外部レプリケーション機能を使って、dump/restore後に移行元/移行先のDBの差分を常に更新し続け、DBを切り替えるタイミングでレプリケーションを停止する手法を採用しました。

# 移行方法

今回行った移行手順を図にすると以下のようになります。NATGatewayを使ってグローバル通信をする図になっていますが、VPN経由でも問題ないと思われます。
順を追って説明いたします。
![スクリーンショット 2019-12-05 17.56.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215784/435c4694-e1d5-3dc8-d9ab-dcbb2b691319.png)



## ①移行元DBのバイナリログ保持期間を変更する

さて、レプリケーションを使ってDB間の同期をするためには、バイナリログ（DBへの変更操作がすべて記述されたログ）が必要になります。このログは保持期間が決められており、古いものから順に削除されてしまいます。最低でもdumpを開始してからレプリケーションをスタートさせるまでの間はログが残っていないとなりません。
dumpを開始する前にバイナリログ保持期間を変更し、ログが保持されることを確実にしておきましょう。
通常はMySQLのシステム変数である`expire_logs_days`を以下のように変更することで、期間を変更できます。
一部のマネージドサービスではこの変数が無視され、設定画面からのみ変更することが可能な場合もあるのでお気を付け下さい。

```
MySQL [(none)]> SET GLOBAL expire_logs_days = 2;
```

## ②移行元DBをdumpする
まず移行元のDBをdumpします。データ量が大きいDBの場合は、dumpファイルをS3上に生成することによって作業しているサーバのディスクサイズを気にしなくてよくなるのでおすすめです。

以下のコマンドを実行します。`HOSTNAME`、`USERNAME`はそれぞれDBに接続するためのホスト名、ユーザ名。`DATABASE1 DATABASE2 ...`はdump対象のデータベース名です。
パイプを使って標準出力を`aws s3 cp`コマンドに渡すことによって、直接S3にコピーします。

```
$ mysqldump -h HOSTNAME -u USERNAME -p --databases DATABASE1 DATABASE2 ... --master-data=2 --single-transaction --routines | aws s3 cp - s3://your_s3_bucket/hoge.dump
```

ここでポイントとなるオプションが2つあります。

### `--master-data`
このオプションが1にセットされていると、dumpを実行した瞬間のバイナリログ座標が`CHANGE MASTER TO`ステートメントとしてdumpファイルに書き込まれます。
2にセットされていると、`CHANGE MASTER TO`ステートメントがコメントとしてファイルに書き込まれます。0にセットされているとこれが書き込まれないので、必ず1か2を指定します。今回は`CHANGE MASTER TO`ステートメントではなく、RDSのシステムストアドプロシージャ（後述）を使うので、2をセットしておきます。
参考: https://dev.mysql.com/doc/refman/5.6/ja/mysqldump.html#option_mysqldump_master-data

### `--single-transaction`
このオプションをつけないと、移行元DBへの書き込みがロックされてしまいます。障害につながる恐れがあるので必ずつけましょう。
参考: https://dev.mysql.com/doc/refman/5.6/ja/mysqldump.html#option_mysqldump_single-transaction

## ③移行先DBにデータをrestoreする
dumpが完了したら移行先のDBにデータを流し込みます。先ほどは逆に、`aws s3 cp`の標準出力をmysqlコマンドに渡してあげます。
今度は`HOSTNAME`、`USERNAME`に移行先DBのホスト名、ユーザ名を指定します。


```
aws s3 cp s3://your_s3_bucket/hoge.dump - | mysql -h HOSTNAME -u USERNAME -p -f
```

## ④レプリケーションの実行

restoreまで完了したら、ついにレプリケーションを実行します。これにより、dumpを開始してからrestoreが完了するまでの更新や、それ移行の更新も同期し続けることができます。

まずはじめに、dumpファイルに書き込まれているバイナリログ座標を確認します。

```
aws s3 cp s3://your_s3_bucket/hoge.dump - | grep -i "change master to"
-- CHANGE MASTER TO MASTER_LOG_FILE='mysql-bin-changelog.123456', MASTER_LOG_POS=1234567;
```

`MASTER_LOG_FILE`と`MASTER_LOG_POS`の値をそれぞれ控えておきます。

次に移行先DBに接続し、以下のコマンドを使用してマスターとなるDB（移行元のDB）を指定します。
RDSでは通常のSQLステートメントではなく、[システムストアドプロシージャ](https://docs.aws.amazon.com/ja_jp/AmazonRDS/latest/UserGuide/Appendix.MySQL.SQLRef.html)と呼ばれるコマンドを実行します。

```
MySQL [(none)]> CALL mysql.rds_set_external_master (
  'HOSTNAME',
  'PORT',
  'USERNAME',
  'PASSWORD',
  'mysql-bin-changelog.123456',
  1234567,
  0
);
```

`mysql.rds_set_external_master`のパラメータには移行元DBに接続するための情報、バイナリログ座標、最後にSSLの使用の有無を指定します。

パラメータに関する詳細は、以下の公式ドキュメントもご参照下さい。
https://docs.aws.amazon.com/ja_jp/AmazonRDS/latest/UserGuide/mysql_rds_set_external_master.html

ここまで完了したら、以下のコマンドを実行することによって、レプリケーションがスタートします。

```
MySQL [(none)]> CALL mysql.rds_start_replication;
```

## DBの切り替え
正しくレプリケーションが行われてるかは以下のコマンドによって確認できます。

```
MySQL [(none)]> show slave status\G
...
Slave_SQL_Running_State: Slave has read all relay log; waiting for the slave I/O thread to update it
...
```

`Slave has read all relay log`とあるので、レプリケーションによるコピーは最新の状態に追いついているようです。

サービスを一時的にメンテモードにしてから[^1]以下のコマンドによってレプリケーションを停止と設定の削除をしましょう。


```
MySQL [(none)]> CALL mysql.rds_stop_replication;
MySQL [(none)]> CALL mysql.rds_reset_external_master;
```

最後に、Route53のプライベートホストゾーンや、アプリケーションのconfを新しいDBに書き換えるなどして、切り替え完了です。

# 最後に
ダウンタイムは最後の切替時に行うメンテモードの間だけなので、レプリケーション停止から切り替えまでのわずかな時間のみとなります。切り替えの手順をしっかり確認しておけば、数分間の停止で移行を完了させられるかと思います。
移行元/移行先の疎通を通さないといけないというハードルはありますが、条件さえ揃えばメリットの多い移行方法だと思います。

[^1]: レプリを停止してからDB切り替えまでの数分間の間にDBへの書き込みが無い場合には、メンテモードにする必要はないです。その場合は完全に無停止で移行することができます。

明日は@shotawsさんが何か書いてくれるそうです。