メドピア開発者ブログ

集合知により医療を再発明しようと邁進しているヘルステックカンパニーのエンジニアブログです。読者に有用な情報発信ができるよう心がけたいので応援のほどよろしくお願いします。

大規模サービスのデータベースエンジンを MySQLからAurora MySQLへの移行 〜リードレプリカ, DNSを利用した最小ダウンタイム移行方法〜

バックエンドエンジニアの徳富(@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の活用など、新しいアプローチに挑戦することで、予想よりもスムーズな移行が実現できたのは大きな成果です。

この経験から得た学びは、社内で共有し、会社全体の知見向上に努めたいと考えています。


是非読者になってください!


メドピアでは一緒に働く仲間を募集しています。
ご応募をお待ちしております!

■募集ポジションはこちら

medpeer.co.jp

■エンジニア紹介ページはこちら

engineer.medpeer.co.jp