実録!サービスを止めずに Amazon Aurora へ移行した話

Photo via Visual huntPhoto via Visual hunt

ヌーラボアカウントではつい先日、Amazon RDS for MySQL から Amazon Aurora へと移行しました。ここでは、その経緯と実際に実施した作業を簡単にご紹介させていただきます。

移行の経緯

ヌーラボアカウントは Backlog や CacooTypetalk といったヌーラボのサービスへの認証機能を提供しています。もし認証機能が使えないとすべてのサービスを利用できなくなってしまいます。そのため、ヌーラボアカウントには常に認証機能を提供し続けられるような、高いアベイラビリティが求められています。

ヌーラボアカウントではこれまで RDS for MySQL を利用していましたので、MySQL 互換を掲げる Amazon Aurora は、リリースされたときから移行の可能性を検討をしてきました。Aurora のメリットについてはいろいろなところで語られていますが、パフォーマンス面でのメリットよりも、そのアベイラビリティ(可用性)とスケーラビリティ(拡張性)に注目しました。

アベイラビリティ

FAQ の高い可用性とレプリケーションにあるように Amazon Aurora のディスク障害に対する高い対障害性と高度なレプリケーションは、大きなメリットです。

RDS for MySQL のマルチ AZ 構成においては、DNS ベースということもあり、障害発生からフェイルオーバーが完了するまでは数分かかっていました。Amazon Aurora では、単体でも 15 分以内に障害から回復しますし、複数台のノードでクラスターを構成しておけば 1 分以内にフェイルオーバーが完了するといわれています。

これは高い可用性につながります。

スケーラビリティ

RDS for MySQL ではディスクを増設できますが、システムを稼働させたまま増設しようとすると、どうしても手間がかかります。

FAQのハードウェアとスケーリングにあるように、Amazon Aurora のストレージはデータベースの使用量に応じて、データベースのパフォーマンスに影響を与えずに拡張されます。
ここも大きなポイントでした。

結論

RDS for MySQL に比べて選べるインスタンスタイプが少なかったり、料金が RDS for MySQL よりは少し割高だったり、というデメリットはありますが、スケーラビリティ、アベイラビリティの面では Aurora ではなく MySQL を選ぶ理由も特に見つかりません。他にも検討したことはありますが、大きくは以上のことからヌーラボアカウントは Amazon Aurora に移行することに決めました。

移行の手順

ヌーラボアカウントのアーキテクチャとアベイラビリティへの取り組み

ヌーラボアカウントは Java サーブレットとして実装され、それは EC2 インスタンス上で動作しています。データベースには RDS for MySQL を使っていました。

先にも述べたように、ヌーラボアカウントには認証機能を提供し続けることが求められます。そのため、データベースが読み取り専用になったときには一部の機能が動作しませんが、認証機能は問題なく動作するように設計されています。

移行手順の検討

データベースを移行するにあたって最も重要視したのは認証機能を提供し続けることでした。

ステージング環境は一足先に MySQL から Aurora に移行し、そこで試行錯誤しながら移行の手順を検討しました。そこで、アプリケーション自体はデータベース接続のために何かを変更する必要がないこともわかりました。

MySQL から Aurora へのデータは、ユーザーガイド Amazon Aurora DB クラスターへのデータの移行 に従い、レプリケーション機能を利用して移行します。Aurora のインスタンスを MySQL のスナップショットから作成します。そのあとから MySQL へ書き込まれたデータは、MySQL をマスター、 Aurora をスレーブとしたレプリケーションを構築して Aurora に反映します。

正しくレプリケーションが動作することが確認できたら、アプリケーションが接続するデータベースを切り替えます。そのとき、MySQL と Aurora の両方に書き込みが発生するとデータに不整合が発生してしまいます。アプリケーションを停止して作業すればこの問題は起こらないのですが、認証機能を止めるわけにはいきません。そこで、メンテナンス時間を設けて MySQL を一時的に読み取り専用にし、認証機能を含むある程度の機能が有効な状態で移行することにしました。

手順 1 MySQL のリードレプリカを作成してレプリケーションを止め、バイナリログが削除されるのを防ぐ。
手順 2 Aurora のインスタンスを立ち上げる
手順 3 MySQL をマスター、Aurora をスレーブとしたレプリケーションを構成する。
手順 4 MySQL を読み取り専用に変更する。これ以降は一部の機能が使えなくなる。
手順 5 アプリケーションの接続先を、順次 Aurora に変更していく。変更したサーバーから全ての機能が使えるようになっていく。
手順 6 MySQL の停止

以上のような手順で、アプリケーションを停止せずにデーターベースを移行することにしました。

一部の機能が使えなくなる手順 4 から手順 5 の作業を 1 時間のメンテナンス期間を設け、その中で実施することにしました。手順 4 で読み取り専用に変更した直後のスナップショットを取得しておくことで、万が一の場合に備えます。また、手順 5 からは Aurora への書き込みが始まります。もしこれ以降に大きな問題があって MySQL へ戻したくなった場合にはデータを一部捨てる必要があります。ですので、手順 5 以降は多少の問題があっても後戻りしないようにすることにしました。

サイトを通じてユーザー様へも通知しました。

ここから、それぞれの手順を詳しく見ていきます。移行前は下図の状態でした。

手順 1 MySQL のリードレプリカを作成してレプリケーションを止め、バイナリログが削除されるのを防ぐ

MySQL から Aurora へのデータ移行のためのレプリケーションは、mysql.rds_set_external_master コマンドを使い、手動でレプリケーションを設定します。これは RDS の管理下にないレプリケーションとなります。

また、RDS for MySQL は通常、バイナリログを定期的に S3 に退避して削除します。標準では 5 分程度で削除されます。RDS の管理下にあるレプリケーションならば、RDS はレプリケーションが追いついていることを確認してから削除するようになっているのですが、RDS の管理下にないレプリケーションのときには RDS がレプリケーションのことを気にせずバイナリログを削除してしまいます。

これではうまくレプリケーションできません。

この対処として、Amazon Aurora とのレプリケーションの「Amazon Aurora と外部 MySQL データベース間のレプリケーション」にあるように、もうひとつ RDS 管理下のリードレプリカを作成し、レプリケーションを止めておくとマスターがバイナリログを削除せずに保持し続けます。上記ドキュメントの手順に従い MySQL のリードレプリカを作成し、そこで mysql.rds_stop_replication コマンドを実行してレプリケーションを停止しておきました。

mysql> call mysql.rds_stop_replication;
+---------------------------+
| Message |
+---------------------------+
| Slave is down or disabled |
+---------------------------+
1 row in set (1.03 sec)

バイナリログが削除されないので、この設定をした後はストレージの空き容量に注意していましたが、このシステムでは大きなデータをデータベースに保存していなかったこともあり、特に大きな増加はありませんでした。

また、Aurora にレプリケーションを設定するときに指定するためのバイナリログのポジションを、このリードレプリカ上で確認しておきます。

mysql> show slave status \G
*************************** 1. row ***************************
 (中略)
 Master_Log_File: mysql-bin-changelog.208690
 Read_Master_Log_Pos: 110119
 (中略)
1 row in set (0.00 sec)

Master_Log_File と Read_Master_Log_Pos が重要なバイナリログのポジションです。

手順 2 Aurora のインスタンスを立ち上げる

マネジメントコンソールのマイグレーションメニューから、MySQL リードレプリカから Aurora のインスタンスを作成しました。これは最新のスナップショットをもとにインスタンスを立ち上げるため、直前に MySQL リードレプリカのスナップショットを取得しておきました。このときセキュリティグループやパラメータグループも設定しておきました。このインスタンスが Writer ノードとなります。また、マネージメントコンソールから Reader ノードを立ちあげてクラスターを構成しておきます。

手順 3 MySQL をマスター、Aurora をスレーブとしたレプリケーションを構成する

Aurora 上で mysql.rds_set_external_master コマンドで MySQL をマスターに設定します。
このときのパラメータとして、先に調べたバイナリログのポジションを指定します。

mysql> call mysql.rds_set_external_master (
 -> "MySQL マスターのエンドポイント"
 -> , "3306"
 -> , "ユーザー"
 -> , "パスワード"
 -> , "mysql-bin-changelog.208690"
 -> , 110119
 -> , 0
 -> );
Query OK, 0 rows affected (0.03 sec)

次にレプリケーションを開始します。

mysql> call mysql.rds_start_replication;
+-------------------------+
| Message |
+-------------------------+
| Slave running normally. |
+-------------------------+
1 row in set (1.01 sec)
Query OK, 0 rows affected (1.01 sec)

しばらく経つと MySQL と Aurora が同じデータを保持するようになりました。

手順 4 MySQL を読み取り専用に変更する

メンテナンス期間に入ったら、まずはマネージメントコンソールからパラメータグループの read_only 変数を変更し、MySQL マスターを読み取り専用としました。これで MySQL にも Aurora にも書き込みが発生しない状態となり、アプリケーションの一部機能が使えない状態となりました。

手順 5 アプリケーションの接続先を、順次 Aurora に変更していく

アプリケーションが接続するデータベースを、順次 MySQL から Aurora に変更していきました。
接続先が変わったサーバーから、すべての機能が使えるようになっていきました。

ここでメンテナンス期間は終わりです。
アプリケーションはすべて Aurora にアクセスするようになりました。

ここで mysql.rds_reset_external_master コマンドによって MySQL から Aurora へのレプリケーションを解除しました。

mysql> call mysql.rds_reset_external_master;
+----------------------+
| message              |
+----------------------+
| Slave has been reset |
+----------------------+
1 row in set (0.01 sec)

Query OK, 0 rows affected (0.01 sec)

手順 6 MySQL の停止

最後に MySQL のインスタンスを停止しました。
ここは数日程度の余裕をもって停止しました。

データベースの移行はここで完了しました。
全体的には大きなトラブルもなく、スムーズに移行できたと感じています。

アプリケーションの変更

データベースが読み取り専用だったときの挙動の変更

アプリケーションはデータベースへの書き込んだときに、データベースが読み取り専用だったときはエラーページを表示していました。
今回は、明示的に読み取り専用に変更するため、エラーページではなくメンテナンス中である旨を表示するように、移行の前にアプリケーションを変更しておきました。

JDBC ドライバの変更

Java アプリケーションから Aurora に接続する場合には JDBC ドライバに MariaDB Connector/J を使うと高速なフェイルオーバーを実現できます。今回はデータベースの移行が終わったあとに JDBC ドライバを変更しました。

MariaDB Connector/J

従来の RDS のマルチ AZ はホットスタンバイの構成となっており、フェイルオーバーが発生したときにクラスターエンドポイントがマスターやスレーブの IP アドレスを返す仕組みでした。これは DNS ベースのため、TTL に影響されるので、アプリケーションがクラスターエンドポイントを参照している以上、どうしてもフェイルオーバーしたときにはアプリケーションがデータベースにアクセスできない時間が増えてしまいます。

MariaDB Connector/J は MariaDB と MySQL のための JDBC ドライバーです。フェイルオーバーのための仕組みが組み込まれており、Aurora のクラスターにも対応しています。
このドライバでは JDBC URL にクラスターエンドポイントではなく、以下のようにノードエンドポイントを列挙することで高速なフェイルオーバーを実現します。

jdbc:mysql:aurora://{ノードエンドポイント1}:3306,{ノードエンドポイント2}:3306,.../account-db

Aurora は 1 台の Writer と複数台の Reader からなるクラスターを構成できます。Writer か Reader かはグローバル変数 innodb_read_only で識別できます。クラスターが Writer ノードで障害を検知すると、即座に正常な Reader のうち 1 台の innodb_read_only を変更し、そのノードを Writer に変更します。

このドライバはクエリ発行時に障害を検出すると innodb_read_only によって Writer を確認し、クエリを発行するノードを切り替えて再度クエリを発行します。
フェイルオーバーが発生してもドライバが即座に接続先を切り替えるため、アプリケーションはフェイルオーバーが発生したことを気にせず処理を続けることができます。

詳細な挙動は mariadb.com のドキュメントでご確認ください。

注意すること

MariaDB Connector/J のバージョンには 1.2.3 を使いましたが、いくつか注意が必要なところがありました。

MariaDB のサイトには以下のような記載があります。

It’s originally based on the Drizzle JDBC code, and with a lot of additions and bug fixes.

この JDBC ドライバは Drizzle ( http://www.drizzle.org/ ) の JDBC ドライバがベースになっていて、MySQL Connector/J とはコードのベースが違います。そのため、パラメータの体系が違っていたり、細かい部分の挙動が違っていたりします。

現時点では JDBC 4.1 で追加された ResultSet インターフェースの T getObject(int i, Class<T> type) が常に null を返すような実装でした。ヌーラボアカウントでは Java の enum を MySQL の VARCHAR にマッピングしているところがあり、ResultSet から値を取得するときに前出のメソッドを使っていたので、そこに修正が必要でした。

また、アプリケーションの終了時にスレッドが残ってしまう問題があるようです。

https://mariadb.atlassian.net/browse/CONJ-61

これに対するワークアラウンドとして org.mariadb.jdbc.Driver クラスに unloadDriver というメソッドが用意されていて、ドライバーのアンロード後にこのメソッドを呼び出すことで正しく終了できるようになります。ヌーラボアカウントでは以下のようなコードを ServletContextListener の contextDestroyed メソッドから呼び出すようにしています。

try {
    final Class<?> clazz = ClassUtils.getClass("org.mariadb.jdbc.Driver");
    MethodUtils.invokeStaticMethod(clazz, "unloadDriver");
} catch (final ReflectiveOperationException e) {
}

ユニットテストで品質を担保する

本番環境が Aurora になっても、開発者は引き続き MySQL を利用します。また、Jenkins による CI 環境では、相変わらず MySQL を利用します。Aurora が MySQL 互換を掲げていても、どこまで互換性があるのかわからないところもあります。

そこで、Aurora に移行しているステージング環境を利用したユニットテストによって発行される SQL に問題がないことを担保したいと考えました。このサーバーでは本番環境と同じようにアプリケーションも稼働しているのですが、それに加えて、これを Jenkins のスレーブとして 1 日に 1 回、ビルドが実行されるようにしました。

これで MySQL と Aurora とで非互換な部分があっても、このテストで検知できます。

ちなみに今までこのテストがデータベースの互換性が原因で失敗したことはありません。MySQL との互換性については素晴らしい品質だと感じました。

まとめ

Aurora には Amazon の本気度が随所に伺えます。

移行後にデータベースの障害やフェイルオーバーが発生したことはありません。現在のところ本番環境では Aurora のメリットを体感できてはいませんが、ステージング環境ではフェイルオーバーが発生したときにアプリケーションには影響がないことを確認しています。

Aurora が備えている高いスケーラビリティ、アベイラビリティによって、システム全体のスケーラビリティ、アベイラビリティが高まりました。

MySQL との互換性も高いものがあり、インフラが変わっても、それを特に意識せず運用、開発をしていけるのは、とてもすごいことなのではないかと感じています。

今後 Amazon Aurora への移行を検討している皆様の一助になれば幸いです。


ヌーラボではデータベース大好き! というエンジニアを募集しています

 

より良いチームワークを生み出す

チームの創造力を高めるコラボレーションツール

製品をみる