最近のRailsからのAuroraフェイルオーバー

2021.01.22

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

最近のRails

って言うと語弊がありますが、デフォルトがPumaになってる昨今のRails、という意味で。
Rails Aurora フェイルオーバー でググると色々ヒットしますが、少々偏った情報が多いため、整理して残したいと思った次第です。
偏ったというのは、Unicornでしか使えない(古い)情報が多いから。

さて

問題事象をザックリ言うと

フェイルオーバーによって、(Railsが)知らない間にREADERに降格してた元WRITERに対して更新クエリ投げちゃってエラーになる

です。Auroraの各インスタンスはフェイルオーバーによって、IPアドレスはそのままでREADER⇔WRITERにスイッチするのですが、Railsがそれに追従できずにそのままコネクションをプーリングし続けるために発生する問題。

まず

ググると行き着くのこちらでしょう
https://github.com/sonots/activerecord-refresh_connection

コネクションプーリングを無効化する というようなワードで各所で紹介されていますが、ザックリ言うとリクエストが終わる毎にプールされてるコネクションを全部切断する、という話。結果的にプーリングは無効化される、という事ですね。
このような(ある意味豪快な)技が採用できるのは、Unicornがマルチプロセスモデル(=リクエスト毎にプロセスが割り当てられる)だから。そのプロセス中の全コネクションを切断しても、他のリクエストには影響が及ばないという次第。見事ですね。
他方、最近のDefaultであるPumaはマルチスレッドモデルなので、この技は採用出来ません。このGemのドキュメントでもその旨は明記されてます。

では

Pumaではどうしたら?
という事でシンプルに対処してみたのがこちら。
ActiveRecord::ConnectionAdapters::Mysql2Adapteractive? メソッドをオーバーライドします。

  def active?
    if super # <- @connection.ping check only
      # check readOnly or not
      @connection.query('SHOW variables LIKE "innodb_read_only"').first.fetch(1) == 'OFF'
    end
  rescue Exception => e
    false
  end

以上。
プールから取り出されたコネクションは、最初に active? メソッドで評価されるので、この時にチェックを追加してやるだけ。これで READERと知らずに更新クエリ投げちゃう 問題はクリアです。

毎回チェッククエリが走る点が気に入らん、という意見もあるかと思います。総クエリ数に占める割合で考えれば十分許容範囲内と考えますが、 クエリエラー時のみに絞って上記のようなチェックをし、READERだったらコネクションを再生成する、といったアプローチもありでしょう。
後、意図的にReaderに接続したい、という要件が有る場合はよしなにやって下さいませ。

確認はしてないですが、諸々やってくれそうなGemもあります。が結局、要件に合わせていくらでもカスタマイズできるシンプルさが良いのだなーと思ってる次第です。

ところで

上記のコードは、どこかのWEBを参考にしたものなんですが、これが探しても探してもなかなかたどり着けず、、、で今もって見つからない次第。
他のどの手法と比べてもシンプルで明瞭な対処案だったので、これが共有されないのは惜しい! というのが今回このブログを起こした一番のモチベです。 心当たりのある方は是非ご一報下さい。