Redis が好きで使いたい人、Redis が要件上必要な人(僕)向けに Rails で Redis とうまく付き合っていく上で抑えておくポイントを書いてみました。Redis サーバそのものをうまく扱う方法はこの記事では紹介しません。あくまで Rails で利用する部分に留めます。
Redis サーバが落ちた時・容量不足になった時、どうする?
Redis だけに限った話ではないですが、Redis サーバが落ちたり、容量が不足してエラーになった時に、アプリケーションをどのように動かすのか、または落とすのかを事前に設計する必要があります。
例えば、キャッシュで利用している場合は、Redis サーバに接続できなかったとしても、アプリケーションは落とさずにそのまま動作させる方が良いでしょう。多少パフォーマンスは落ちますが。ただ、パフォーマンスへの影響が深刻でキャッシュがないことで雪崩式に他のシステムへの悪影響を及ぼすのであれば潔くアプリケーションごと落とした方が良かったりもします。
また、セッションで利用している場合は、セッションを使った機能に関しては落とした方が良かったりしますが、落とし方にもいくつかのパターンがあると思います。例えば、セッションに全く関係のないページの閲覧は最低限できるようにするとか、ログインページなどセッションは必要だけど閲覧だけはできて、ログインしようとしても失敗してエラーメッセージを表示するだけ…など。ここら辺はアプリケーションごとに決めるところです。
キャッシュで使う
Rails では標準でキャッシュストアの Redis をサポートしています( redis
or hiredis
gem は必要です ) 。使い方も簡単です。
config.cache_store = :redis_cache_store, { url: ENV['REDIS_URL'] }
これだけで Rails.cache.fetch
や Rails.cache.write
Rails.cache.read
などのメソッドを通じで Redis にキャッシュしたデータを読み書きすることができます。
ActiveSupport::Cache::RedisCacheStore
は Redis サーバが落ちても問題ない
ActiveSupport::Cache::RedisCacheStore
は Redis サーバに接続した際にエラーが起きても無視するように作られています。つまりページ表示は遅くなるかもしれませんがエラーにはなりません。
def failsafe(method, returning: nil)
yield
rescue ::Redis::BaseError => e
handle_exception exception: e, method: method, returning: returning
returning
end
def handle_exception(exception:, method:, returning:)
if @error_handler
@error_handler.(method: method, exception: exception, returning: returning)
end
rescue => failsafe
warn "RedisCacheStore ignored exception in handle_exception: #{failsafe.class}: #{failsafe.message}\n #{failsafe.backtrace.join("\n ")}"
end
デフォルトではロガーが設定されていると、ロガーを通じて Redis サーバのエラーログを出力します。
ActiveSupport::Cache::Store.logger = Logger.new(STDOUT)
ただ、それだけだと検知できないこともあるので、エラーハンドラーを渡して Sentry などエラー収集サービスに送ることもできます。
cache_servers = %w(redis://cache-01:6379/0 redis://cache-02:6379/0)
config.cache_store = :redis_cache_store, { url: ENV['REDIS_URL'],
error_handler: -> (method:, returning:, exception:) {
# Report errors to Sentry as warnings
Raven.capture_exception exception, level: 'warning',
tags: { method: method, returning: returning }
}
}
話はそれますが、Rails.cache
を永続化前提のロジックを組んでいると、アプリケーションの動作自体が壊れる可能性は当然あります…。
セッションで使う
Redis をセッションストアとして利用する場合、おそらく2つの方法があります。
-
redis-rails
orredis-actionpack
gem を利用する -
redis-session-store
gem を利用する
redis-rails
or redis-actionpack
の挙動は、落ちる
redis-rails
や redis-actionpack
は Redis サーバが落ちると例外をはいて Rails アプリケーション自体が落ちます。これは、 redis-rails
がキャッシュやセッションなど用途が広く、落ちない挙動をデフォルトにするとややこしいことになるから、ということのようです ( 参考 issue ) 。ちなみにキャッシュ側で使う分にはデフォルトで例外ははきません。セッションを利用する場合のみです。
(2つの Gem を書いていますが、もしセッションでしか利用しないなら redis-actionpack
を直接使う方が良いようです)
redis-session-store
の挙動は、無視する
redis-session-store
は Redis サーバが落ちても例外をはかず Rails アプリケーションはそのまま動きます。
def get_session(env, sid)
sid && (session = load_session_from_redis(sid)) ? [sid, session] : session_default_values
rescue Errno::ECONNREFUSED, Redis::CannotConnectError => e
on_redis_down.call(e, env, sid) if on_redis_down
session_default_values
end
ActiveSupport::Cache::RedisCacheStore
と同様に on_redis_down
で例外処理を渡すことが可能です。
この場合、Rails アプリケーションは Redis サーバが反応なくても session
オブジェクトにアクセスできますし、何事もないように動きます。ただし、 session
は単なるハッシュオブジェクトでしかありません。
def session_default_values
[generate_sid, USE_INDIFFERENT_ACCESS ? {}.with_indifferent_access : {}]
end
そのため、セッションに依存したロジックを書いている場合は、そのままで問題がないのか、もしくは例外処理を入れてエラーページへ飛ばすのかを決める必要があります。
まとめ
個人的にはキャッシュは Rails デフォルトで、セッションは redis-session-store
を利用し、適宜例外処理を入れるのが良いかなと思っています。Redis サーバに限らずデータベースサーバなど、アプリケーションが依存しているミドルウェアが障害を起こしたときに、どのような振る舞いをすべきかはちゃんと考えて設計をしていきたいですね。