DBのint枯渇を目の前にした僕らは

MySQLの[int型](https://dev.mysql.com/doc/refman/5.6/ja/integer-types.html)は符号付きで　`-2147483647〜2147483647` の範囲をサポートし、レコードを記録する際にこの範囲を超えて記録しようとするともちろんエラーとなります。

これは、長い運用の末にデータが膨大になり、ついにintのサポート範囲が枯渇寸前となった話です。

# 方針

DBはAWS Auroraを使用しており、アプリケーションはRailsで構築されています。RailsのMigrationはデフォルトでidカラムをAUTO INCREMENTのint型で作成します[^1]。サービスの特徴としては他のサービスと比較すると高トラフィックに晒されるもので、DBに大量のログを記録する必要がありテーブルによっては1ヶ月で1億レコード以上記録されるものもあります。対処方法を検討し始めた時にはidは既に18億を超えており、やるべきことは対象のテーブルのidカラム、及びそのidを関連として保持しているテーブルのカラムの型をBigintに変換することでした。

ただカラムの型をBigintにするだけであれば通常はALTER TABLEを実行すれば良いだけですが、レコードが数億規模になってくるとALTER TABLEが完了するまでに時間が掛かりすぎ、その間[テーブルはロックされてしまう](http://nippondanji.blogspot.jp/2009/05/alter-table.html)ので、事実上サービスが停止してしまいます。

そこで、対象のカラムをBigint化した新しいテーブルを用意し、既存のデータを(リアルタイムに増えるものも含めて)コピーし、入れ替えることでサービスを停止することなくBigint化する方法を模索し始めました。

# 手段

候補として挙がったのはPERCONA社の[pt-online-schema-change](https://www.percona.com/doc/percona-toolkit/2.2/pt-online-schema-change.html)と、
[AWS Database Migration Service](https://aws.amazon.com/jp/dms/)(以下DMS)の2つでした。DMSを使う多くの目的はオンプレミスに運用しているDBをAWSのクラウド上に移行することだと思いますが、DMSはRDSからRDSへの移行もサポートしており、今回の目的を達成できる可能性があると踏みました。

そこで、今回のケースがDMSの用途としてふさわしいのかAWSのサポートに問い合わせたところ、同一データベース内での使用は検証した限りでは可能なようだが、本来の用途からは逸脱しており、結果想定しない挙動をする恐れがあり推奨しない、という返答をいただきました。

代替案として、RDSのインスタンスごと新たに立ち上げ、そのインスタンス間でDBを丸ごと移行する形にすれば本来の用途と合致するという案内を受けたので、チームで少し議論を交わし、コストは増すがpt-online-schema-changeを使用するよりマネージドなサービスを使うほうが人的コストを抑えられることと、いざというときにAWSのサポートを受けられる方が安心なことにより、今回はDMSでDBごと移行することにしました。

なお、同じくAWSが提供しており、DMSと合わせて使用するスキーマ変換ツールとして[Schema Conversion Tool](http://docs.aws.amazon.com/ja_jp/SchemaConversionTool/latest/userguide/CHAP_SchemaConversionTool.GettingStarted.Rel.html)がありますが、ソースデータベースとしてMySQLを選択した場合は、移行先のRDSのターゲットデータベースとしてPostgreSQLのみが選択可能という仕様により今回は使用しませんでした。そもそもAurora同士の移行であればターゲットデータベースからスキーマをダンプし、カラム型を変更してターゲットデータベース上に作成するだけで充分でした。

# 検証

まずはStagingのDBで検証を行いました。StagingのDBは7GBほどで、インスタンスは一番小さいdms.t2.micro[^3]、1タスクに全テーブルを指定し、Limited LOBモードで移行したところ、だいたい1時間ほどで完了しました。DMSにはリアルタイムに挿入や変更されたデータに追従してくれるCDCモードがありますが、こちらも正常に動作していました。

DMSのパフォーマンスに強く影響するのは[LOBの移行モード](http://docs.aws.amazon.com/ja_jp/dms/latest/userguide/CHAP_BestPractices.html#CHAP_BestPractices.LOBS)で、これをデフォルトのFull LOBサポートモードにしていると倍以上の時間が掛かりました。これはLimited LOBモードを使用することで回避できますが、こちらはMax LOB Sizeを指定する必要があり、これを超えるデータが存在した場合は切り捨てられてしまうので注意が必要です。テーブルのLOB列の量によりますが、多くの場合ではLOBデータが64KBに収まっていれば高速化が望めるそうです。DMSがLOBとみなすMySQLのデータ型は[こちら](http://docs.aws.amazon.com/ja_jp/dms/latest/userguide/CHAP_Reference.Source.MySQL.DataTypes.html)で確認できます。

# 本番

Stagingで問題なく動作することが確認できたので、本番DBで実施することにしました。本番DBのデータサイズは9TBを超えており、1インスタンスではストレージがパンクする恐れがあったため、c4.4xlargeのインスタンスを3台用意し、それぞれのインスタンスにタスクを分散させ、移行するテーブルの担当を複数に分けました。

稼働してしばらくすると、アプリケーションからの本番DBへの負荷により、DMSのタスクを動かしていると本番DBのCommit Latencyが増大する事象が確認できたため、日中はDMSタスクを止め、移行は夜間から早朝に掛けて実施することにしました。DMSにはそういったスケジューリングの機能はないので、AWS LambdaでDMSタスクを停止、再開するスクリプトを書きました。

DMSタスクを停止させると、全てのデータのロードが完了していないテーブルは再開時に再ロードとなり、つまり移行したデータを破棄して最初からやり直しとなってしまいます。このままではテーブルが巨大だとロードを完了させることは不可能なので、[ResumeEnabledオプション](http://docs.aws.amazon.com/ja_jp/dms/latest/userguide/CHAP_Tasks.CustomizingTasks.TaskSettings.FullLoad.html)をtrueに変更することで中断したポイントから再開できるようにしました。このオプションはマネジメントコンソールからは変更できないため、AWS CLIから `create-replication-task` でタスクを生成する必要があります。

タスクがデータの移行に失敗するとCloudWatch Logsにログを出力されますが、今回の移行時に下記のようなエラーを確認しました。

```
into table `db_name`.`table_name` CHARACTER SET UTF8 fields terminated by ',' enclosed by '"' lines terminated by '\n
```

エラー内容としては[こちら](http://docs.aws.amazon.com/ja_jp/dms/latest/userguide/CHAP_Troubleshooting.html#CHAP_Troubleshooting.Aurora)に酷似していましたが、SQL_MODEにANSI_QUOTESは設定しておらず、サポートに問い合わせても結局原因は掴めませんでした。データを確認したところエラーが発生したレコードは移行されておらず、エラーが発生したテーブルだけを別タスクに切り出し、再度移行を行ってもエラーは解消しなかったので、その箇所についてはmysqldumpにより手動で移行しました。

# カットオーバー

既存データは移行が完了し、DMSはCDCでリアルタイムに変更されるデータを移行している状態となったところで、新規DBに向き先を変更することにしました。向き先変更と同じタイミングでデータが挿入されidが衝突してしまわないように、新規DBの方はAUTO_INCREMENT値を増加させておきました。新規DBに向けたアプリケーションを別途用意し、動作に問題ないことを確認し、本番環境を新規DBに移行しました。

# さいごに

検証期間を含めると一ヶ月以上の作業期間となりましたが、晴れてintの枯渇問題から開放されました。なお移行が完了したしばらくした後に外道父さんが[DMSを使わずにリアルタイムの完全コピーを実現](http://blog.father.gedow.net/2017/01/24/aurora-cluster-copy/)しており、もう少し早ければ...!とも思いましたが無事完了できたことに変わりはないので良しとしました。あまり出くわさないケースかとは思いますが、誰かの参考になれば幸いです。


[^1]: [5.1からならBigintがデフォルトになったんだが...](https://github.com/rails/rails/pull/26266)

[^3]: 1コアCPU、メモリ1GB https://docs.aws.amazon.com/ja_jp/dms/latest/userguide/CHAP_Introduction.ReplicationInstance.html