AWS Database Migration Serviceを使って水平分割されたDBの縮退に挑戦した話

# 概要

[AWS Database Migration Service](https://aws.amazon.com/jp/dms/) 略称DMSを使って、水平分割されたDBインスタンスを縮退しようと計画していましたが、検証中に不具合らしきものを見つけたので**中断**しました。この記事はDMSを検証中に引っかかったトラブルや対策について書いていたメモをまとめたものです。今回の記事中で説明する内容は実環境からぼかしている箇所がありますがご容赦ください。
※ この文章での「メンテナンス」という単語は、ゲームシステムのメンテナンスを指しており、AWS側のメンテナンスとは異なります。

## 動機

通常DBは基本的にはスペックが不足したらスケールアップを行い対応します。弊社では加えてより大規模な負荷でも耐えられるよう水平分割してスケールアウトも併用する構成になっております。規模の拡大時には問題無いのですが、必要とされる負荷が下がった場合や、サービスの使われ方が変わって別のサブシステム群に負荷がかかるようになった場合などに適切に規模を縮小する必要があります。

![dms.png](https://qiita-image-store.s3.amazonaws.com/0/87092/7d1012ae-8900-6aa4-fe09-3cd202d7d266.png)

今回の縮退はRDS MySQL5.6インスタンス4台を1つのAurora MySQL5.6クラスターへまとめる計画です。
負荷のかかる箇所が別のサブシステムになったため、このインスタンス群は使用率が劇的に下がったためスケールダウンを行ってきました。今回は、適切なスケールダウン先のインスタンスサイズが無くなったため、スケールインの計画が立案されました。DBのスケールダウンは短時間で終わるため、通常のメンテナンス時間中に問題なく行えるのですが、スケールインの場合に課題があります。
スケールインする場合は通常、メンテナンス開始後に削減対象インスタンスからmysqldumpし、残すインスタンスへimportする作業を行います。ですが今回の場合、元の台数も4台と多く、容量もDB1台につき150GBほど使用しておりDB内のレコード数もなかなかの規模がありました。mysqldumpを使ったスケールイン方法では十数時間かかる見込みとなり、許容できるメンテナンス時間内に終わらないという課題がありました。その時にDMSの存在を知り、このサービスを使って縮退できないかと思い、調査と計画を行いました。

## DMSとは

DBをオンラインのまま、ソースからターゲットへ移行できるサービスです。当初はオンプレミスのDBからRDSへの移行するためのサービスと思っていたのですが、いつの間にかどんどん機能が増えていました。例えば選択できるソースが現在では

* オンプレミスおよびEC2インスタンス上のDB
    * Oracle バージョン 10.2 以降、11g、12.2 まで
    * Microsoft SQL Server バージョン 2005、2008、2008R2、2012、2014、および 2016.
    * MySQL バージョン 5.5、5.6、5.7
    * MariaDB (MySQL 互換データソースとしてサポートされています)
    * PostgreSQL 9.4 以降
    * SAP Adaptive Server Enterprise (ASE) バージョン 12.5、15、15.5、15.7、16 以降
    * MongoDB バージョン 2.6.x およ 3.x 以降
* Microsoft Azure
    * Azure SQL Database（一部制限あり）
* Amazon RDS
    * Oracle バージョン 11g および 12c
    * Microsoft SQL Server バージョン 2008 R2、2012、2014（一部制限あり）
    * MySQL バージョン 5.5、5.6、5.7。（5.5は一部制限あり）
    * PostgreSQL 9.4 以降.（9.4.9未満と9.5.4は一部制限あり）
    * MariaDB (MySQL 互換データソースとしてサポートされています)
    * Aurora MySQL
* Amazon S3

[データ移行のソース](https://docs.aws.amazon.com/ja_jp/dms/latest/userguide/CHAP_Source.html)から抜粋

このように大抵のDBはソースにできます。MongoDBやS3までソースに出来るようになっていたのは驚きです。ターゲットは上記リストと大体同じなのですが差分があり

* ソースは可能だがターゲットは不可能
    * MongoDB
    * MS Azure上のAzure SQL Database
* ソースは不可能だがターゲットは可能
    * Aurora PostgreSQL
    * Redshift
    * DynamoDB

[データ移行のターゲット](https://docs.aws.amazon.com/ja_jp/dms/latest/userguide/CHAP_Target.html)から抜粋

このようになっております。[MongoDBからDynamoDBへの移行](https://aws.amazon.com/jp/about-aws/whats-new/2017/04/aws-database-migration-service-adds-support-for-mongodb-and-amazon-dynamodb)という用途にもDMSは使用可能ですし、マッピングを頑張れば[MySQLからDynamoDBへの移行](https://docs.aws.amazon.com/ja_jp/dms/latest/userguide/CHAP_Target.DynamoDB.html)も可能です。

### DMSで重要な概念

DMSのドキュメントは[ボリューム満点](https://docs.aws.amazon.com/ja_jp/dms/latest/userguide/Welcome.html)です。全部読むのは辛いのでここだけは抑えておきたい、という概念や箇所を説明します。

* [レプリケーションインスタンス](https://docs.aws.amazon.com/ja_jp/dms/latest/userguide/CHAP_ReplicationInstance.html)
    * ソースから読み取り、ターゲットへ書き出すインスタンス
        * 正確にはレプリケーションタスクを動かすためのインスタンス
    * DMSで移行をするためには必ず必要となります
    * ソースとターゲット両方にアクセスできるセキュリティーグループを設定する必要があります
* [タスク](https://docs.aws.amazon.com/ja_jp/dms/latest/userguide/CHAP_Tasks.Creating.html)
    * レプリケーションインスタンス上で動作する、移行の定義
    * ソース, レプリケーションインスタンス, ターゲットを三つ組で指定します
    * タスクには3種類の移行方法が選択できます
        * データをターゲットデータベースに移行する(Full Load)
            * タスク開始時のソースの中身をターゲットへ移行する、これはフルロード(FL)と呼ばれます
        * 移行中に変更をキャプチャする(full-load-and-cdc)
            * フルロード後、継続的なレプリケーションを開始する、このレプリケーションをドキュメントでは変更データキャプチャ(CDC)と呼んでいます
            * オンラインで移行している場合、フルロード中に起きたソース側の変更は当然ターゲットには移行されません
                * その変更をレプリケーションインスタンスはキャッシュしており、フルロード後キャッシュされた変更を適用後レプリケーションを開始します
        * データ変更のみソースデータベースにレプリケーションする
            * タスク開始後からのソース側の変更をターゲットにレプリケーションします、移行用途では使えないので今回は説明しません
* フルロード(FL)
    * ターゲットがMySQLもしくはAurora MySQLの場合、ソースからのデータをCSVで取得し[LOAD DATA INFILE](https://dev.mysql.com/doc/refman/5.6/ja/load-data.html)している挙動を示している
* 変更データキャプチャ(CDC)
    * 進行状況はCloudWatchのメトリクスである程度把握することができます

    > たとえば、CDCLatencySource 値と CDCLatencyTarget 値を組み合わせることで、タスクの合計レイテンシー (レプリカラグ) を調べることができます。[データ移行サービスメトリクス](https://docs.aws.amazon.com/ja_jp/dms/latest/userguide/CHAP_Monitoring.html#CHAP_Monitoring.Metrics)
* [タスクの検証](https://docs.aws.amazon.com/ja_jp/dms/latest/userguide/CHAP_Validating.html)
    * タスク単位で有効か無効かを選択できるオプション、デフォルトでは無効
    * 有効にするとFL後、ソースデータとターゲットデータの検証を開始し、行単位で比較し不一致を検出します
        * CDC時の増分変更も検証します
    * 有効にするとソースとターゲットに対してクエリを発行しタスク側で比較するので、ソース, レプリケーションインスタンス, ターゲットの負荷は無効に比べて上昇します
* [テーブルマッピング](https://docs.aws.amazon.com/ja_jp/dms/latest/userguide/CHAP_Tasks.CustomizingTasks.TableMapping.html)
    * タスク単位で有効か無効かを選択できるオプション、デフォルトでは無効
    * 通常DMSではデフォルトではソースの（MySQLで言う）データベースやテーブルを全てそのまま移行します
        1. そのまま移行するとデータベース名やテーブル名が衝突してしまう
        2. 移行の必要がないデータベースやテーブルがある
            * このような場合に指定します
    * 今回の移行のケースでは移行する必要があるデータベースのみ対象になるテーブルマッピングを設定しました

## DMSの制約

ここまで読むとDMSを使用し

1. 「移行中に変更をキャプチャする」移行方法でソース4台からターゲット1台へ移行する
2. FL完了しCDCでレプリケーションに入りソースにターゲットが追いついたら、メンテナンスに突入、ソースへの更新が止まる
3. 「タスクの検証」で正しく移行できたこと、CDCのレプリカラグが0であることを確認する
4. ソースのAUTO INCREMENT値をターゲットへ移行する
5. アプリケーション側のDBへのエンドポイントをソースからターゲットへ書き換える
6. 動作確認後、メンテナンス終了

と短時間のメンテナンスで済むような素晴らしい機能に見えるのですが、DMS自体の制約が多々あるのでそこまで簡単ではありません。

>AWS DMS は、移行の実行に必要なターゲットスキーマオブジェクトを作成します。ただし、AWS DMS は最小限のアプローチを採用するため、データの効率的な移行に必要なオブジェクトのみ作成します。つまり、AWS DMS は、テーブル、プライマリキー、場合によっては一意のインデックスを作成しますが、効率的にソースからデータを移行するために必要ではない他のオブジェクトは作成されません。たとえば、セカンダリインデックス、非プライマリキーの制約、データデフォルトは作成されません。

[AWS DMS: の仕組み](https://docs.aws.amazon.com/ja_jp/dms/latest/userguide/CHAP_Introduction.html)から
つまり、DMSが用意してくれるのは中身は統合済ですが、プライマリキー以外のインデックスが全く無い、デフォルト値も設定されていないデータベースなのです。そのまま移行したらあっという間にシステムダウンです。

私達が把握しているMySQL5.6からAurora MySQL5.6へのDMS移行で移行されないものは

1. セカンダリインデックス（普段はインデックスとだけ呼ぶことが多いです）
2. デフォルト値
3. 列の AUTO_INCREMENT 属性（[MySQL 互換データベースの AWS DMS のソースとしての使用](https://docs.aws.amazon.com/ja_jp/dms/latest/userguide/CHAP_Source.MySQL.html))
4. AUTO_INCREMENT 値
5. パーティショニング
6. ビュー
7. トリガー
8. ストアドプロシージャ

があり、まだ漏れはあるかもしれませんが、我々のシステムで使っていない属性も多いため今回は使用している1から4までを私達で移行することにしました。

### DMSで移行されないものの移行

まず、移行されないこれら4つはDDL操作に属するので、MySQLリファレンスマニュアルの[14.11.1 オンライン DDL の概要](https://dev.mysql.com/doc/refman/5.6/ja/innodb-create-index-overview.html)に目を通しましょう。まず以下2つは

* デフォルト値
    * カラムのデフォルト値を設定する
        * データファイルではなく、.frm ファイルのみを変更します。
* AUTO_INCREMENT値
    * カラムの自動インクリメント値を変更する
        * データファイルではなく、メモリーに格納された値を変更します。

であるため、一瞬で終わります。AUTO_INCREMENT値は整合性を保つため、メンテナンスに入ってソース側に更新が無くなったことを確認してからターゲットへ設定した方が良いでしょう。

* 列の AUTO_INCREMENT 属性

これはプライマリキーのカラムにAUTO_INCREMENT属性を付与するタスクです。DMSでもプライマリキーは移行してくれます。これも正直一瞬で終わるものだと思っていたのですが、行っていることはALTER TABLE tbl MODIFY col type AUTO_INCREMENTですので、先程のオンラインDDLの概要の中の「カラムのデータ型を変更する」にあたり強制的にテーブルコピーになります。以下がshow processlistの結果です

    362	dms	192.0.2.1:4397	shard-A-0	Query	3	copy to tmp table	ALTER TABLE `example1` MODIFY `id` int(11) NOT NULL AUTO_INCREMENT
対策としてあらかじめソースのスキーマをmysqldump --no-dataで取得し、ターゲットに流し込んでおいてから、タスク定義で「既にターゲットにテーブルが存在したらTruncateしてから実行する」を選択することにより、列のAUTO_INCREMENT属性を後から時間を掛けてALTER TABLEする必要が無くなりました。[^1]

残りはセカンダリインデックスだけなのですが、これについては次で詳しく説明します。

### ADD INDEXをどうすればメンテナンス時間を短くできるか

そもそもインデックス付与は重いタスクです。MySQL5.6（および互換のAurora MySQL)ではインプレースで付与が可能なのですが、それはサイズが小さいテーブルの場合だけで巨大なテーブルにはテーブルコピーが行われます。[^2]そしてなによりCPUパワーを必要とします。どのタイミングでADD INDEXすれば最適なのか検証しました。

1. FL→CDC→メンテナンス→ADD INDEX
    * メリット
        * 確実性が高い
            * 当初はこれで移行する予定でした
    * デメリット
        * ADD INDEXに長時間かかる
            * 当初見込みで20時間
                * CPUがボトルネックだったので現在最高の16xlargeを使用したが思ったより短縮されなかった
        * ADD INDEXに時間がかかるとメンテナンス時間が長引くので不採用となった
2. スキーマ流し込み（index定義付き）→FL→CDC→レプリカラグ確認→メンテナンス
    * メリット
        * 最初からスキーマを流し込んでおけばあとはDMSが全て移行してくれる
        * メンテナンス時間がエンドポイント書き換えだけで済む
    * デメリット
        * フルロードが遅すぎて途中で止まる
            * 1番の通常方法のFLで10時間程度だったのが、この方法では100時間やっても6割移行できたかどうかという進捗
            * show engine innodb statusを見ると大量のトランザクション待ち
            * インデックス付きのテーブルに大量に挿入するのは無理があった
3. FL→ADD INDEX→CDC→レプリカラグ確認→メンテナンス[^3]
    * メリット
        * ADD INDEXが長引いてもシステムはオンラインなので問題ない
        * メンテナンス時間がエンドポイント書き換えだけで済む
    * デメリット
        * 列の AUTO_INCREMENT 属性付与が思ったより遅かった
4. スキーマ流し込み（index定義無し）→FL→ADD INDEX→CDC→レプリカラグ確認→メンテナンス
    * メリット
        * ADD INDEXが長引いてもシステムはオンラインなので問題ない
        * メンテナンス時間がエンドポイント書き換えだけで済む
        * 列のAUTO_INCREMENT属性時間の短縮
    * デメリット
        * とくになし

## 結果

ここまでの検証で前項の4番の手順で行えば短いメンテナンス時間で縮退できそうだ、と計画を立て本番前提の環境を構築していました。ですが構築途中に、ソースに本番の負荷がかかった状態でDMS移行すると検証エラーが大量に出ることが判明し、移行は一旦中断してAWSのサポートへ問い合わせしつつDMSを使わない縮退方法について検討中です…

### 備考

#### 気をつけるべきこと

* レプリケーションインスタンスにソースやターゲットへ接続できる適切なセキュリティグループが指定されてないと接続テストで失敗する
* RDS MySQL5.6をソースにする時の注意点
    * 自動バックアップを0日にすると、バイナリログがそもそも作成されなくなるのでソースとして指定しても動かなくなる
    * CDCを使用するには、バイナリログ保持時間を伸ばす必要がある
        * `call mysql.rds_set_configuration('binlog retention hours', 24);`
            * このクエリを投げるのには特権ユーザである必要がある
            * バイナリログを通常より保持するようになるので、ストレージをより消費するようになる
    * binlog_formatをROWにする必要がある
        * 設定はRDSのパラメーターグループから可能
            * DynamicなのでRDSの再起動無しで設定変更することは可能
        * デフォルトのMIXEDよりパフォーマンスが下がる可能性がある

#### 行っておいて良かったこと

* DMSタスクのログを有効にする
    * 複数タスク必要な場合なら1つでもいいから有効にしておくこと
        * タスクが開始されない、タスクエラーが起こったときに大変役立つ
        * 付けていないと何も情報を得られないことが多い
* タスクの検証を有効にする
    * 遅くはなるが、データ移行が正しく行われたことが誰の目にも分かりやすく確認できる

#### 今後検討すべきこと

* 負荷が適切に掛かっているソースを使用して検証環境を作る
    * 本番インスタンスにリードレプリカを作り、そこをソースにするのが良いと思われる
        * 本番と別のパラメーターグループを設定することもできる
    * RDS MySQLでは簡単にAuroraリードレプリカを作成できるので、Aurora to Auroraも試してみる価値はある
* AUTO_INCREMENT属性の付与は遅いので避けたいと思っていたが、そもそもALTER TABLE文は1テーブルに対して複数の変更を書ける[13.1.7 ALTER TABLE 構文](https://dev.mysql.com/doc/refman/5.6/ja/alter-table.html)
    * ADD INDEXやADD UNIQUE KEYは1文にまとめていたが、MODIFY col type AUTO_INCREMENTもまとめて良かったのではないか

#### DMSでよく読むことになるドキュメント

* [AWS DMSの仕組み](https://docs.aws.amazon.com/ja_jp/dms/latest/userguide/CHAP_Introduction.html)
* [AWS DMS コンポーネント](https://docs.aws.amazon.com/ja_jp/dms/latest/userguide/CHAP_Introduction.Components.html)
* [ベストプラクティス](https://docs.aws.amazon.com/ja_jp/dms/latest/userguide/CHAP_BestPractices.html)
* [タスクの作成](https://docs.aws.amazon.com/ja_jp/dms/latest/userguide/CHAP_Tasks.Creating.html)
* [AWS Database Migration Service タスクの検証](https://docs.aws.amazon.com/ja_jp/dms/latest/userguide/CHAP_Validating.html)
* [MySQL 互換データベースの AWS DMS のソースとしての使用](https://docs.aws.amazon.com/ja_jp/dms/latest/userguide/CHAP_Source.MySQL.html)
* [AWS BlackBelt Online Seminar 2017 AWS Database Migration Service](https://www.slideshare.net/AmazonWebServicesJapan/20170919-aws-blackbelt2017dms)

[^1]: TruncateにしたのはAUTO INCREMENTの値をリセットした方が良さそうと思ったからで、もしかしたらDo nothingでも大丈夫かもしれません。DROPではせっかく投入したスキーマが消されるので無意味です。
[^2]: 小さいテーブルではインプレースのADD INDEXが可能で200MB程度のテーブルにADD INDEXをデフォルト(ALGORITHM=INPLACE)とALGORITHM=COPYを付けて実行してみたところ、デフォルトの方が7倍ほど速かったです。
[^3]: FL後CDC前のタイミングでインデックス付与して大丈夫なのか？と思ったのですが[公式のタスクの作成](https://docs.aws.amazon.com/ja_jp/dms/latest/userguide/CHAP_Tasks.Creating.html)の中に「全ロードの完了後にタスクを停止する」という設定項目で「キャッシュされた変更を適用する前に停止する - キャッシュされた変更を適用する前に、タスクを停止します。 これにより、変更の適用を高速化できるセカンダリインデックスを追加できます。」と書かれており、これはAWSの意図に沿う動作だと認識しました。