カミナシ エンジニアブログ

株式会社カミナシのエンジニアが色々書くブログです

RDS Blue/Green Deployments を使ってシュッと utf8mb4 にマイグレーションした話

こんにちは。ソフトウェアエンジニアの坂井 (@manabusakai) です。

カミナシでは RDB に Amazon Aurora MySQL 2(MySQL 5.7 互換)を使っています(以下 Aurora MySQL と略します)。

ある日、社内の Slack で「𠮷」などの文字列が登録できないのではないかという話が出ました。これを聞いて「あー」と思った方も多いでしょう。

MySQL で有名な UTF-8 の 4 バイト文字問題で、歴史的な理由から MySQL 5.7 以前では utf8 の文字セットは utf8mb4 ではなく utf8mb3 を指しています。

dev.mysql.com

カミナシのアプリケーションは 4 バイトの文字列が入力された場合はシステムエラーを返す実装になっていますが、エラーの内容をユーザーにわかりやすく伝えることは難しいためユーザー体験としても良くない状況でした。

この問題で困っているユーザーは現時点では少数ですが、プロダクトが成長してデータ量が増えれば増えるほど対応するコストがかかるため、今のうちにすべてのテーブルを utf8mb4 にマイグレーションすることにしました。

マイグレーションするための方法を検討する

information_schema でテーブルの照合順序とカラムの文字セットを調べたところ、ほぼすべてのテーブルがマイグレーション対象であることがわかりました。

SELECT table_name, table_collation 
FROM information_schema.tables 
WHERE table_schema = 'xxxxxx';

SELECT table_name, column_name, character_set_name, collation_name 
FROM information_schema.columns 
WHERE table_schema = 'xxxxxx';

照合順序と文字セットを変更するには次のような ALTER TABLE を実行するだけですが、クエリの実行中はテーブルロックがかかるため、メンテナンスを実施してデータベースへの書き込みを止める必要があります。

ALTER TABLE xxxxxx 
CONVERT TO CHARACTER SET utf8mb4 
COLLATE utf8mb4_general_ci;

しかし、データ量の多いテーブルはすでに数十億レコードの規模になっており、通常のメンテナンスでマイグレーションを終わらせることは不可能でした(あとで詳しく書きますが、マイグレーションに丸 2 日ほどかかりました)。

このような場合、スイッチオーバーの手法がよく使われるかと思います。

  1. バイナリログを有効にする
  2. レプリケーションを設定して replica を用意する
  3. replica 側に先にマイグレーションを実施する
  4. source 側の書き込みを止める
  5. レプリケーション遅延がなくなったことを確認してスイッチオーバーする
  6. アプリケーションのデータベース接続先を新しい source に向ける

この手法は何度もやったことがありますが、神経を使う作業が多くなかなか大変です。この面倒な作業をマネージドで解決してくれるのが、タイトルにある Amazon RDS Blue/Green Deployments です(以下 Blue/Green Deployments と略します)。

aws.amazon.com

詳細は公式ブログに譲りますが、この機能を使うことで replica を準備したり、スイッチオーバーするときの手順を楽にしてくれます。今回はこの機能を使うことにしました。

マイグレーションの具体的な手順

Blue/Green Deployments の使い方はドキュメントに詳しくまとまっていますが、より具体的にイメージできるように自分が行った手順を詳しく解説していきます。

バイナリログを有効にする

Blue/Green Deployments を使うとその名の通り Blue と Green の環境が作られますが、これらは MySQL でいうところの source と replica の関係になります。そして、Blue から Green へのレプリケーションはバイナリログを使って実現されています。

Blue/Green Deployments を使うにはバイナリログを有効にする必要があるため、まずは Cluster Parameter Group で binlog_format のパラメータを変更します *1。バイナリログのフォーマットは MIXED が推奨されています。詳細は “Configuring Aurora MySQL binary logging” を参考にしてください。

なお、このパラメータは再起動しないと適用されないため、別の作業のために予定されていたメンテナンス時に合わせて対応しました。ここまでが事前準備です。

Green 側の環境を作成する

バイナリログを有効にしたら Green 側の環境を作成します。基本的にはドキュメントに沿って作業するだけですが、1 つだけ注意することがあります。

廃止予定で新規作成できないバージョンを使っている場合、Green 側の環境は何時間待っても Provisioning のステータスから先に進みません。 AWS サポートに問い合わせたしたところ、そのような場合はまずインプレースアップグレードなどでサポートされているバージョンまで上げる必要があるとのことでした。

カミナシでもこの問題に当たってしまったので、上記のバイナリログの件と合わせてメンテナンス時にマイナーバージョンを上げました。

Parameter Group を変更して型変換を許容する

今回はカラムの文字セットを変更するため、VARCHAR 型のカラムなどはバイト数が変わります。たとえば、VARCHAR(64) は utf8 だと 192 バイトですが、utf8mb4 だと 256 バイトになります。

MySQL 5.7 以降からは行ベースレプリケーションがデフォルトになりました。バイナリログのフォーマットは MIXED にしているので、特定の条件のときに STATEMENT から ROW に自動的に切り替わります。

行ベースレプリケーションの場合、デフォルトの設定では source と replica でカラムのデータ型が完全に一致する必要があります。要するに、Green 側に utf8mb4 のマイグレーションを実行すると、次のようなエラーでレプリケーションが止まってしまいます。

Column 1 of table 'xxxxxx.xxxxxx' cannot be converted from type 'varchar(192(bytes))' to type 'varchar(256(bytes) utf8mb4)'

上位互換の文字セットへのマイグレーションなので、保存する文字列には互換性があります。 source と replica で異なるデータ型であってもレプリケーションさせるには、slave_type_conversions のパラメータを用いて型変換を許容させます。

MySQL のドキュメント “Replication of Columns Having Different Data Types” に詳しい解説がありますが、非不可逆変換(4 バイトの文字列を 3 バイトには戻せない)を許可する必要があるので ALL_NON_LOSSY に設定します。

Aurora MySQL の場合、このパラメータは Cluster Parameter Group でしか変更できないため、既存の Parameter Group をコピーして新たに slave_type_conversions を変更したものを用意します。

Green 側の Parameter Group を変更して再起動すると反映されます。

mysql> SELECT @@slave_type_conversions;

+--------------------------+
| @@slave_type_conversions |
+--------------------------+
| ALL_NON_LOSSY            |
+--------------------------+

Green 側にマイグレーションを実行する

準備が整ったので全テーブルに ALTER TABLE を実行していきます。

接続先をよく確認したうえで Green の Writer エンドポイントに接続します。接続先が合っていれば replica のステータスが見れるはずです *2

mysql> SHOW SLAVE STATUS\G
*************************** 1. row ***************************
               Slave_IO_State: Waiting for master to send event
                  Master_Host: 172.23.15.0
                  Master_User: rdsrepladmin
                  Master_Port: 3306
                Connect_Retry: 60
              Master_Log_File: mysql-bin-changelog.004138
          Read_Master_Log_Pos: 2130988
               Relay_Log_File: relaylog.000016
                Relay_Log_Pos: 6068186
        Relay_Master_Log_File: mysql-bin-changelog.004133
             Slave_IO_Running: Yes
            Slave_SQL_Running: Yes
(snip)

時間がかかるのは事前にわかっていたので、次のような SQL をファイルに書き出し、踏み台サーバからバックグラウンドで実行させました。

$ cat migration.sql
ALTER DATABASE xxxxxx CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;

-- テーブルの分だけ ALTER TABLE を列挙する
ALTER TABLE xxxxxx CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;

$ nohup mysql -h host_name -u user_name -p -D db_name < migration.sql &

マイグレーション中はテーブルロックがかかるため、レプリケーション遅延はどんどん増えていきます。下のグラフは実際にマイグレーションしたときの様子ですが、なんと 1 日と 21 時間(16 万秒の遅延!)ほどかかりました。

レプリケーション遅延の様子

Aurora MySQL のバイナリログの保存期間は 7 日間なので、大規模なデータベースでマイグレーションする際は保存期間を延ばす必要がある点に注意してください *3

書き込みを止めてスイッチオーバーする

Blue/Green Deployments のドキュメントを読むとスイッチオーバー時のダウンタイムは 1 分未満とありますが、今回は万全を期すために深夜メンテナンスを実施してすべての書き込みを止めました。

スイッチオーバーする前に Aurora MySQL 側でもガードレールチェックというテストが行われますが、自分の目でも確認しておくと良いでしょう。

replica 側で SHOW SLAVE STATUS を実行しレプリケーションの状態を確認します。

mysql> SHOW SLAVE STATUS\G
*************************** 1. row ***************************
               Slave_IO_State: Waiting for master to send event
                  Master_Host: 172.23.15.0
                  Master_User: rdsrepladmin
                  Master_Port: 3306
                Connect_Retry: 60
              Master_Log_File: mysql-bin-changelog.004278
          Read_Master_Log_Pos: 40733291
               Relay_Log_File: relaylog.000613
                Relay_Log_Pos: 40733524
        Relay_Master_Log_File: mysql-bin-changelog.004278
             Slave_IO_Running: Yes
            Slave_SQL_Running: Yes
(snip)
        Seconds_Behind_Master: 0

Slave_SQL_Running / Slave_IO_Running はレプリケーションが正常に動作しているかで期待値はどちらも Yes です。 Seconds_Behind_Master はレプリケーション遅延している秒数で期待値は 0 です。

あとは source 側で SHOW PROCESSLIST を実行し、実行中のクエリがないか確認しておきましょう。

ここまで準備ができれば、Blue/Green Deployments でスイッチオーバーするだけです。ドキュメントによると、内部では次のようなステップが実行されているとのことです。

  1. ガードレールチェックを実行
  2. 両方の環境で新しい書き込みを停止
  3. 両方の環境でデータベースへの接続を切断し、新しい接続をブロック
  4. レプリケーション遅延が解消されるまで待機
  5. 両方の環境の DB クラスターと DB インスタンスの名前を変更
  6. ​​両方の環境でデータベースへの接続を許可
  7. 新しい DB クラスターへの書き込みを許可

ドキュメントには記載がありませんが、おそらく 4 番と 5 番の間に MySQL のフェイルオーバーが行われているはずです。

5 番で Green 側の DB クラスター名と DB インスタンス名がもともとの名前に置き換わってくれるので、アプリケーションが見ている Writer / Reader のエンドポイントはそのままで大丈夫です。

エンドポイントの DNS キャッシュは TTL が 5 秒に設定されているので、スイッチオーバー後はすぐに新しい DB クラスターに向くはずですが、念のため CloudWatch で DatabaseConnections のメトリクスを確認すると良いでしょう。

Blue 側の環境を削除する

スイッチオーバーしたあと Blue 側の環境はそのまま残ります。スイッチオーバーが完了するともう Blue 側にロールバックすることはできないため、不要であれば削除してしまいましょう。

今回は念のため最終スナップショットを取ってから削除しました。

おわりに

ご覧いただいたように Blue/Green Deployments が神経を使う作業をほぼやってくれるので、手軽にスイッチオーバーを実施することができます。データベースの作業は失敗すると大事故につながるので、ステップが減って手順がシンプルになるのは良いですね。

多くの企業では手順書を用意したり、スクリプト化していると思いますが、それらを置き換えることもできるでしょう。これまでスイッチオーバーで苦労してきた方は、一度検討してみてはいかがでしょうか?

今回は文字セットのマイグレーションでしたが、Aurora MySQL のメジャーアップグレードでも使える方法です。 Aurora MySQL 3 への移行も早めにやっていこうと思います。


最後に宣伝です 📣

カミナシでは絶賛採用中です! 一緒に強いチームを作っていく仲間を募集しています!

*1:Aurora MySQL 2.10 以降はバイナリログのパフォーマンスへの影響は気にしなくても良さそうです。詳しくは、個人ブログに書いた「Aurora MySQL のバイナリログのパフォーマンス影響について改めて調べてみた」をご覧ください。

*2:コマンドには master / slave という名前が残っていますが、正しい呼び方は source / replica に変わっています

*3:Aurora MySQL 互換 DB クラスターのバイナリログの保持期間を長くするにはどうすればよいですか?