blog

紫陽花

Rails ActionCacheを導入する

Webアプリケーションのパフォーマンス改善として、RailsのActionCacheを導入しました。

Railsのキャッシュに関する概要はRailsGuidesが一番わかりやすかったです。

Rails のキャッシュ: 概要

「キャッシュ(caching)」とは、リクエスト・レスポンスのサイクルの中で生成されたコンテンツを保存しておき、次回同じようなリクエストが発生したときのレスポンスでそのコンテンツを再利用することを指します。

キャッシュの手法は 3つ

  • ページキャッシュ (ページ単位のキャッシュ)
  • アクションキャッシュ (アクション単位のキャッシュ)
  • フラグメントキャッシュ (部品単位のキャッシュ)

今回紹介するのは「アクションキャッシュ」です。

アクションキャッシュの機能はRails3までは標準で機能として存在していましたが、Rails4からはGemとして切り出されて独立して存在しています。

actionpack-action_caching

Rails4以降で開発している場合はGemfileに記述しinstallします。

gem "actionpack-action_caching"
bundle install
class BooksController
  caches_action :index  

  def index
    @book = Book.find(1)
  end
end

これでキャッシュを導入できます。 初回アクセス時はindexメソッドが実行されますが、2回目以降は初回アクセス時にレンダリングされた結果が返ってきます。

キャッシュがなかった場合

  1. キャッシュの存在をチェックする
  2. indexアクションを実行
  3. キャッシュを生成

キャッシュがあった場合

  1. キャッシュの存在チェック
  2. キャッシュを表示

キャッシュはアクションよりも先に実行されるため、コールバックで値を取得しておく必要がある。

class BooksController
  before_action :set_book, only: :index
  caches_action :index  

  def index
  end
  
  private
    def set_book
     @book = Book.find(params[:id])
    end
end

表示される内容が変更される場合には少し工夫が必要です。

内容が更新されたにもかかわらず更新前に保存されたキャッシュが効いてしまい、最新の内容を表示することができない。という状況を想定します

その場合には、キャッシュする情報に更新情報を渡すことによって解決します。

class BooksController
  caches_action :index, cache_path: ->(c) { build_path(c, @book) }

  def index
  end
  
  private
    def build_path(controller, model: nil)
        cache_path = ActionController::Caching::Actions::ActionCachePath.new(controller).path
        cache_path = "#{base_path}:#{model.update_at.try(:strftime, "%Y%m%d-%H%M%S")}" if model.present?
        cache_path
    end
end

@bookの値が更新されupdated_atが変更されていればcache_pathは新しくなりキャッシュ自体も変更され最新のキャッシュが呼び出されるようになるため、内容が変更されていてもキャッシュも更新されるます。

開発環境でキャッシュを有効にし、機能しているか確認する場合は以下を config/environments/*.rb に記述

config.action_controller.perform_caching = true

キャッシュを有効にし、キャッシュを効かせたいメソッドを呼び出せばキャッシュが効いていることをログで確認することができる。

その際に生成されるキャッシュファイルは、tmp/caches以下に生成されていきます。

キャッシュをexpireするオプションがあります。

expires_in: 30.minutes

とすることで30ごとにexpireされます。

class BooksController
  before_action :set_book, only: :index
  caches_action :index, cache_path: ->(c) { build_path(c, @book) }, expires_in: 30.minutes

  def index
  end
  
  private
    def set_book
    end
    
    def build_path(controller, model: nil)
        cache_path = ActionController::Caching::Actions::ActionCachePath.new(controller).path
        cache_path = "#{base_path}:#{model.update_at.try(:strftime, "%Y%m%d-%H%M%S")}" if model.present?
        cache_path
    end
end

このままでも動作しますが、挙動としてファイルキャッシュになります。(tmp/cache以下にキャッシュ用のファイルが生成される)

複数サーバーでアプリケーションを運用する場合は現状のままだと各アプリケーションでキャッシュが行われてしまいAP1でキャッシュされているが、AP2ではキャッシュされていないのでファイルを生成する。など無駄な挙動が増えパフォーマンスに統一性が持てません。 この問題の解決としてキャッシュサーバとしてredisを用いて複数サーバー運用でも良い感じに挙動する仕組みに変更します。

Railsからredisを使うためredis-railsを導入します。

gem 'redis-rails'
bundle install
config.cache_store = :redis_store, "redis://localhost:3001/0/cache"
# redisの参照先はそれぞれの環境に合わせて変更する 上記は例

caches_action側のexpires_in: でexpireの期間を定義しているためapplication.rb側への定義は不要です。

なかなか早くなるので良いですね。