バックエンドエンジニアの徳富(@yannKazu1)です。先日、メドピアのメインサービスであるmedpeer.jpで使われているデータベースエンジンを、MySQLからAurora MySQLへと移行しました。今回はその移行のプロセスについて詳しくお話しします。
移行したデータベースの簡単なインフラ構成
移行方針
今回移行するデータベースは複数のアプリケーションから参照されており、ダウンタイムによるユーザー影響が大きいため、移行方針の検討の段階で重視したのは、ダウンタイムの最小化でした。これを達成するために、DNSのCNAMEレコードと、Auroraのリードレプリカを活用し、移行させることにしました。
DNSのCNAMEレコードの使用
データベースエンドポイントをアプリケーションに直接記述する代わりに、DNSのCNAMEレコードを利用して間接的に参照するようにしました。これにより、データベースエンドポイントの変更が必要な場合でも、DNSのCNAMEレコードを更新するだけで対応できます。 今回移行したデータベースは複数のアプリケーションで参照されており、すべてのアプリケーションでデータベースホストを手で変更していくのはかなり時間がかかるため、この方法で行うことに決めました。
Auroraをリードレプリカとしての起動
Auroraはリードレプリカとして起動し、必要に応じてプライマリーに昇格させることが可能です。これにより、既存のデータベースと新しいAurora データベースとのデータ同期を保ちつつ、最小限のダウンタイムでデータベースをAuroraに切り替えることができました。
具体的な手順
以下に、具体的な手順を説明します。
①Auroraをリードレプリカとして起動
MySQLのリードレプリカとしてAurora MySQLを起動します。
②CNAMEレコードを作成
データベースエンドポイントを参照するCNAMEレコードを作成し、データベースエンドポイントを参照している全てのアプリケーションをDNS経由に切り替えます。これにより、Auroraに移行後にDNSのエンドポイントを変更するだけで全てのアプリケーションの参照先を切り替えることができ、ダウンタイムが大幅に削減できます。
③既存のRDSを読み取り専用に設定
既存のRDSにread_onlyパラメータを設定し、書き込みを停止します。 書き込みがなくなることで、Auroraレプリカのレプリケーションを追いつかせます。
④Auroraをプライマリーに昇格
リードレプリカとして起動していたAuroraのレプリケーション設定を削除し、フェールオーバーすることでプライマリーに昇格させます。
⑤DNSレコードのエンドポイントをAuroraに変更
DNSレコードの向き先をAuroraのデータベースエンドポイントに変更します。これにより、アプリケーション側で変更を加えることなく、エンドポイントが更新されます。
移行に際して検証したこと
本データベースは多くのアプリケーションが参照するメインデータベースであったため、慎重に検証を行った上で移行を実施しました。特に以下の2点を重視しました。
データベースエンジンの変更による予期せぬエラーの発生
まず、ステージング環境でAurora化を実施し、予期せぬエラーが発生しないかを検証しました。今回AuroraのエンジンはMySQLと互換性のあるバージョンに揃えたので、基本的には問題ないと予想されましたが、確認のためにステージングで検証を行いました。その結果、アプリケーション側の改修は必要ありませんでした。
Auroraのフェールオーバー時の接続切り替えの検証
AuroraとRailsを組み合わせて使用すると、Railsのコネクションプールの仕組み上、フェールオーバー時に新しいライターへのコネクション切り替えが自動的に行われず、リーダー(旧 ライター)にデータの書き込みを行ってしまう事象が知られています。これにより、予期せぬタイミングでデータ更新ができない状況が発生します。
今回、Auroraに移行したデータベースはRails、Go、PHPで書かれた複数のアプリケーションからアクセスを受けており、この問題への対策が必要でした。
対策方法としてはいくつかあることが知られていますが、今回書き込みエラーが発生した際に接続をリセットし、再接続を行うミドルウェアを開発しました。このミドルウェアは、RailsのActionDispatch::ContentSecurityPolicy::Middlewareより先に呼び出されるよう設定しました。
以下に、ミドルウェアの読み込みとそのコードを示します:
module RdsConnectionAdapters class Reconnect class RdsReadOnlyError < ActiveRecord::ActiveRecordError; end CONNECTION_ERROR = ['READ ONLY', '--read-only'].freeze CONNECTION_ERROR_REGULAR = /#{CONNECTION_ERROR.map { |w| Regexp.escape(w) }.join('|')}/ def initialize(app) @app = app end def call(env) @app.call(env) rescue ActiveRecord::StatementInvalid => e raise e unless should_clear_all_connections?(e) ActiveRecord::Base.clear_all_connections! raise RdsReadOnlyError, 'DBが読み込み専用になっていたため、再接続を行いました' end private def should_clear_all_connections?(e) if e.kind_of?(ActiveRecord::StatementInvalid) return CONNECTION_ERROR_REGULAR === e.message end false end end end
config/environments/production.rb(ミドルウェアの読み込み)
config.middleware.insert_before ActionDispatch::ContentSecurityPolicy::Middleware, RdsConnectionAdapters::Reconnect
移行の成果と今後の展望
CNAMEレコードの活用とAuroraレプリカの特性をうまく組み合わせることで、1時間程度のダウンタイムで抑えることができました。 DNSの工夫やAuroraの活用など、新しいアプローチに挑戦することで、予想よりもスムーズな移行が実現できたのは大きな成果です。
この経験から得た学びは、社内で共有し、会社全体の知見向上に努めたいと考えています。
是非読者になってください!
メドピアでは一緒に働く仲間を募集しています。
ご応募をお待ちしております!
■募集ポジションはこちら
■エンジニア紹介ページはこちら