matsukaz's blog

Agile, node.js, ruby, AWS, cocos2d-xなどなどいろいろやってます

RailsとAmazon Aurora利用時のフェイルオーバー問題を解決

tl;dr

  • RailsのコネクションプールとAmazon Auroraのフェイルオーバーの仕組みは相性が悪く、フェイルオーバー時に致命的な問題が発生する
  • 解決方法の1つは、コネクションプールを使わないこと
    • ただし、都度接続だと接続コストがかかる
    • New Relicなどを使ってる場合は、自分の実装以外で使ってるコネクションまで都度接続になってしまう
    • 別スレッドでDB操作を行っている場合、処理中であってもそのスレッドのコネクションまで切断されてしまう(Railsのコネクション破棄がプロセス単位のため)
  • コネクションプールを活かしたままこの問題を解決できたので、その方法をご紹介します。ちなみにRails 4.2の話。

RailsAmazon Aurora利用時のフェイルオーバー問題とは

詳しくはこちら

qiita.com

問題が発生する状況をまとめると以下の通りです。

Amazon Auroraのフェイルオーバーの動作

  • ReaderはWriterに昇格し、WriterはReaderに降格する
    • 基本的には手動でフェイルオーバーさせた場合の話(パッチ適用などによるインスタンスの更新など)
    • 障害などでWriterが死んだ場合は、降格ではなくそもそもインスタンスにつながらなくなるはず
  • Writer/Readerの昇格/降格が発生しても、インスタンスIPアドレスは変わらない
    • WriterにつながるCluster Endpointと、Reader用のReader EndpointのDNSレコードのCNAMEが変わるだけ

Railsのコネクションプールの動作

  • フェイルオーバーすると、接続していたコネクション利用時に以下のエラーが発生
    • ActiveRecord::StatementInvalid (Mysql2::Error: MySQL server has gone away: xxx):
    • ActiveRecord::StatementInvalid (Mysql2::Error: Lost connection to MySQL server during query xxx):
  • 次にコネクションを利用しようとすると以下のエラーが発生
    • Mysql2::Error (Can't connect to MySQL server on 'aurora-test.cluster-xxxx.us-west-2.rds.amazonaws.com' (61)):
    • このエラーが発生したタイミングで、接続先(CNAMEかIPアドレスかは未確認)を内部でキャッシュしてしまう
  • Writerにつないでたコネクションは、フェイルオーバー後はWriterのつもりでReaderに降格したインスタンスにつながってしまう
  • 更新処理を行うたびに、Readerに対する更新処理となり、以下のエラーが発生
    • ActiveRecord::StatementInvalid (Mysql2::Error: The MySQL server is running with the --read-only option so it cannot execute this statement:

解決方法

解決方法その1. コネクションプールを使わない

上記のQiitaの記事にある方法がこれ。具体的には以下のgemを導入して都度接続にする方法。

github.com

これなら確かにReaderにつながることはなかったです。

解決方法その2. 接続できなくなった場合だけコネクションプールを破棄

今回考えたやり方がこちら。都度接続を避けたかった理由は冒頭でも書いたとおり、

  • 接続コストが気になる
  • New Relicを使ってるのでそっちまで都度接続されてしまう
  • 別スレッドでDB操作を行っている処理があり、突然コネクションが切断されてしまって使い物にならなかった

を回避したかったたからです。

導入手順

Railsの標準の ConnectionManagement を差し替えるだけです。

lib/active_record/connection_adapters/reconnect_on_error_management.rb ファイルを作成し、application.rbConnectionManagement を差し替えます(Amazon Auroraのための限定的な対応になるので、gemとか作ってないです。すみません(;´Д`))

gist.github.com

これだけ。

対応内容の説明

Railsのデフォルトの ConnectionManagement は、call で例外が発生すると clear_active_connections! を呼び出してコネクションをコネクションプールに戻します。 これだと接続先情報がそのまま残ってしまうため、何度試行してもReaderに接続にいってしまいます。

上記で紹介する ReconnectOnErrorManagement では、エラーが発生した場合のみ、clear_all_connections! を呼び出してコネクションをプールに戻さずに破棄しています。 この方法であれば、普段はコネクションプールを使い続けることができます。 ただし、ここでつかまえられる例外はrubyのsyntaxエラーやSQLのエラーなどなんでも飛んできますので、Amazon Auroraの接続エラーに限定してコネクションを破棄するため、エラーメッセージから対象を限定しています。

ちなみに都度接続を行う activerecord-refresh_connection は、例外だけじゃなく通常の処理が終了した場合も clear_all_connections! を呼び出して常にコネクションを破棄していました。

注意点

WriterとReaderの接続先情報が完全に置き換わるまでに少し時間がかかるようで(Amazon AuroraのCNAMEの切り替え?)、数秒〜20秒ぐらいはReaderにつながってしまう時間があります。 これは都度接続だったとしても同じ状況かと。 その間は更新処理はエラーになるのでご注意ください。

あとMySQLライブラリのエラーメッセージから接続回りのエラーを判断してますので、エラーメッセージが変わったりするとコネクションを破棄してくれなくなったりするかも。 mysql2 | RubyGems.org | your community gem host の0.3.21と0.4.3では問題なかったです。

利用実績

まだ本番では利用していませんが、数百回試した限りでは意図した通りの動作をしています。近々導入してしばらく様子を見るつもり。

やってることはシンプルなので大丈夫だと思いますが、何かあっても自己責任の上でご利用頂ければ m( )m

ご意見ご指摘ありましたらぜひお願いします。

2019/03/04 更新

いまは普通に運用で利用してますので、問題ないかと思います。

Amazon Web Services実践入門 (WEB+DB PRESS plus)

Amazon Web Services実践入門 (WEB+DB PRESS plus)

トーヨー オーロラテープ 3m巻 8色 113701

トーヨー オーロラテープ 3m巻 8色 113701