1 キャッシュとは何か

キャッシュ(caching)とは、リクエスト・レスポンスサイクルの中で生成されたコンテンツを保存しておき、次回同じようなリクエストが発生したときのレスポンスでそのコンテンツを再利用することを指します。 キャッシュは、お気に入りのコーヒーカップをキッチンの戸棚ではなく机の上に置いておくのと似ています。必要なときにすぐに手に届くところに置いておけば、時間と労力を節約できます。

多くの場合、キャッシュはアプリケーションのパフォーマンスを効果的に増大するのに最適な方法です。キャッシュを導入することで、単一サーバーや単一データベースのWebサイトでも、数千ユーザーの同時接続による負荷に耐えられるようになります。

Railsには、すぐ利用できるキャッシュ機能がいくつも用意されており、データを単にキャッシュできるだけでなく、キャッシュの有効期限、キャッシュの依存関係、キャッシュの無効化などの課題にも対処できます。

本ガイドでは、フラグメントキャッシュからSQLキャッシュまで、Railsの包括的なキャッシュ戦略について解説します。これらの手法により、Railsアプリケーションのレスポンス時間を短縮して、サーバー料金がかさまないよう管理可能な範囲に抑えながら、数百万ビューを配信できるようになります。

2 キャッシュの種類

訳注: 「ページキャッシュ」と「アクションキャッシュ」の項目はRails 8.0.1で削除されました。

ここでは、キャッシュの手法をいくつか紹介します。

Action Controllerのキャッシュは、デフォルトではproduction環境でのみ有効になります。 rails dev:cacheコマンドを実行するか、config/environments/development.rbファイルでconfig.action_controller.perform_cachingtrueに設定することで、ローカルでキャッシュを試せるようになります。

config.action_controller.perform_caching値の変更は、Action Controllerコンポーネントで提供されるキャッシュでのみ有効です。つまり、後述する低レベルキャッシュの動作には影響しません。

2.1 フラグメントキャッシュ

動的なWebアプリケーションでは、基本的にさまざまなコンポーネントを用いてページをビルドしますが、キャッシュの特性はコンポーネントによって異なります。ページ内のさまざまなパーツごとにキャッシュや有効期限を設定したい場合は、フラグメントキャッシュ(fragment caching)を利用できます。

フラグメントキャッシュを使うと、ビューのロジックのフラグメントをキャッシュブロックでラップして、次回のリクエストでそれをキャッシュストアから取り出して配信できるようになります。

たとえば、ページ内で表示する製品(product)を製品ごとにキャッシュしたい場合は、次のように書けます。

<% @products.each do |product| %>
  <% cache product do %>
    <%= render product %>
  <% end %>
<% end %>

Railsアプリケーションがこのページへの最初のリクエストを受信すると、一意のキーを持つ新しいキャッシュエントリが保存されます。生成されるキーは次のようなものになります。

views/products/index:bea67108094918eeba42cd4a6e786901/products/1

キーの途中にある文字列は、テンプレートツリーのダイジェストです。これは、キャッシュするビューフラグメントのコンテンツを元に算出されたハッシュダイジェストです。ビューフラグメントが変更されると(HTMLが変更されるなど)、このダイジェストも変更されて既存のファイルが無効になります。

productレコードから派生したキャッシュバージョンは、キャッシュエントリに保存されます。 productが変更されるとキャッシュバージョンも変更され、古いバージョンを含むキャッシュフラグメントはすべて無視されます。

Memcachedなどのキャッシュストアは、古いキャッシュファイルを自動削除します。

条件を指定してフラグメントをキャッシュしたい場合は、cache_ifcache_unlessを利用できます。

<% cache_if admin?, product do %>
  <%= render product %>
<% end %>
2.1.1 コレクションキャッシュ

renderヘルパーは、コレクションでレンダリングされた個別のテンプレートもキャッシュできます。上のeachによるコード例のようにキャッシュテンプレートを個別に読み出す代わりに、すべてのキャッシュテンプレートを一括で読み出すことも可能です。

このコレクションキャッシュ(collection caching)機能を利用するには、コレクションをレンダリングするときに以下のようにcached: trueを指定します。

<%= render partial: 'products/product', collection: @products, cached: true %>

これにより、前回までにレンダリングされたすべてのキャッシュテンプレートが一括で読み出され、劇的に速度が向上します。しかも、それまでキャッシュされていなかったテンプレートもキャッシュに追加され、次回のレンダリングでまとめて読み出されるようになります。

キャッシュのキーはカスタマイズ可能です。 以下のコード例では、productページでローカライズ結果が別のローカライズで上書きされないようにするため、現在のロケールをキャッシュにプレフィックスしています。

<%= render partial: 'products/product',
           collection: @products,
           cached: ->(product) { [I18n.locale, product] } %>

2.2 ロシアンドールキャッシュ

別のフラグメントキャッシュの内側にフラグメントをキャッシュしたいことがあります。このようにキャッシュをネストする手法を、マトリョーシカ人形のイメージになぞらえてロシアンドールキャッシュ(Russian doll caching)と呼びます。

ロシアンドールキャッシュのメリットは、たとえば内側のフラグメントで製品(product)が1件だけ更新された場合に、内側の他のフラグメントを捨てずに再利用し、外側のフラグメントは通常どおり再生成できることです。

前節で解説したように、キャッシュされたファイルは、そのファイルが直接依存しているレコードのupdated_atの値が変わると失効しますが、そのフラグメント内でネストしたキャッシュは失効しません。

次のビューを例に説明します。

<% cache product do %>
  <%= render product.games %>
<% end %>

上のビューをレンダリングした後、次のビューをレンダリングします。

<% cache game do %>
  <%= render game %>
<% end %>

gameのいずれかの属性で変更が発生すると、updated_at値が現在時刻で更新され、キャッシュが無効になります。しかし、productオブジェクトのupdated_atは変更されないので、productのキャッシュは無効にならず、アプリケーションは古いデータを配信します。

これを修正したい場合は、次のようにtouchメソッドでモデル同士を結びつけます。

class Product < ApplicationRecord
  has_many :games
end

class Game < ApplicationRecord
  belongs_to :product, touch: true
end

touchtrueに設定すると、あるgameレコードのupdated_atを更新するアクションを実行したときに、関連付けられているproductのupdated_atも同様に更新して、キャッシュを無効にします。

2.3 共有パーシャルキャッシュ

共有パーシャルキャッシュ(shared partial cacning)では、パーシャルのキャッシュや関連付けのキャッシュをMIMEタイプの異なる複数のファイルで共有できます。

たとえば、パーシャルキャッシュを共有すると、テンプレートのライターがHTMLとJavaScript間でパーシャルキャッシュを共有できるようになります。テンプレートリゾルバのファイルパスに複数のテンプレートがある場合は、テンプレート言語の拡張子のみが含まれ、MIMEタイプは含まれません。これによって、テンプレートを複数のMIMEタイプで利用できます。

HTMLリクエストとJavaScriptリクエストは、いずれも以下のコードにレスポンスを返します。

render(partial: "hotels/hotel", collection: @hotels, cached: true)

上のコードはhotels/hotel.erbという名前のファイルを読み込みます。

以下のように、レンダリングするパーシャルでformats属性を指定する方法も使えます。

render(partial: "hotels/hotel", collection: @hotels, formats: :html, cached: true)

上のコードは、ファイルのMIMEタイプにかかわらずhotels/hotel.html.erbという名前のファイルを読み込み、たとえばJavaScriptファイルでこのパーシャルをインクルードできるようになります。

2.4 Rails.cacheによる低レベルキャッシュ

ビューのフラグメントをキャッシュする代わりに、特定の値やクエリ結果をキャッシュする必要が生じる場合があります。Railsのキャッシュメカニズムは、シリアライズ可能な情報を保存するのに最も適しています。

低レベルキャッシュ(low-level caching)を実装する効率的な方法は、Rails.cache.fetchメソッドを使うことです。このメソッドは、キャッシュからの読み取りとキャッシュへの書き込みの両方を処理します。

  • 引数を1個だけ渡して呼び出すと、指定されたキーのキャッシュ値を取得して返します。
  • ブロックを渡して呼び出すと、ブロックはキャッシュミスの場合にのみ実行されます。
    • ブロックの戻り値は、メソッドから戻るときに指定したキャッシュキーの下のキャッシュに書き込まれます。
    • キャッシュにヒットした場合は、ブロックを実行せずにキャッシュされた値を直接返します。

以下の例を考えてみましょう。このアプリケーションには、ライバルWebサイトで製品価格を検索するインスタンスメソッドを持つProductモデルがあります。このメソッドによって返されるデータは、低レベルキャッシュに最適です。

class Product < ApplicationRecord
  def competing_price
    Rails.cache.fetch("#{cache_key_with_version}/competing_price", expires_in: 12.hours) do
      Competitor::API.find_price(id)
    end
  end
end

上の例ではcache_key_with_versionメソッドを使っているため、結果のキャッシュキーはproducts/233-20140225082222765838000/competing_priceのような形式になります。このcache_key_with_versionメソッドは、モデルのクラス名、idupdated_at属性に基づいてこの文字列を生成します。これは一般によく使われる生成手法であり、製品が更新されるたびにキャッシュが無効になるというメリットがあります。一般に、低レベルのキャッシュを使う場合はキャッシュキーを生成する必要があります。

低レベルキャッシュの他の利用例も以下に示します。

# `write`で値をキャッシュに保存する
Rails.cache.write("greeting", "Hello, world!")

# `read`でキャッシュから値を取り出す
greeting = Rails.cache.read("greeting")
puts greeting # 出力: Hello, world!

# `fetch`は、キャッシュが存在しない場合はデフォルト値を設定するためにブロックで値を取得する
welcome_message = Rails.cache.fetch("welcome_message") { "Welcome to Rails!" }
puts welcome_message # 出力: Welcome to Rails!

# `delete`はキャッシュの値を削除する
Rails.cache.delete("greeting")
2.4.1 Active Recordオブジェクトのインスタンスのキャッシュは避けること

以下の例を考えてみましょう。このコードでは、スーパーユーザーを表すActive Recordオブジェクトのリストをキャッシュに保存しています。

# super_adminsを取り出すSQLクエリは重いので頻繁に実行しないこと
Rails.cache.fetch("super_admin_users", expires_in: 12.hours) do
  User.super_admins.to_a
end

このパターンは避けるべきです。理由は、インスタンスが変更される可能性があるためです。

production環境では、インスタンスの属性が異なっている可能性もあれば、レコードが削除されている可能性もあります。また、development環境でこのコードに変更を加えてコードが再読み込みされると、キャッシュストアが不安定になります。

インスタンスそのものを丸ごとキャッシュするのではなく、以下のようにid(またはその他のプリミティブデータ型)をキャッシュするようにしましょう。

# super_adminsを取り出すSQLクエリは重いので頻繁に実行しないこと
ids = Rails.cache.fetch("super_admin_user_ids", expires_in: 12.hours) do
  User.super_admins.pluck(:id)
end
User.where(id: ids).to_a

2.5 SQLキャッシュ

Railsのクエリキャッシュ(query caching)は、各クエリが返す結果セットをキャッシュする機能です。 リクエストによって以前と同じクエリが発生した場合は、データベースへのクエリを実行する代わりに、キャッシュされた結果セットを利用します。

以下に例を示します。

class ProductsController < ApplicationController
  def index
    # 検索クエリの実行
    @products = Product.all

    ...

    # 同じクエリの再実行
    @products = Product.all
  end
end

同じクエリをデータベースに対して再実行しても、実際にはデータベースにアクセスしません。クエリから最初に結果が返されたときは、結果をメモリ上のクエリキャッシュに保存し、2回目はメモリから結果を取得します。ただし取得のたびに、クエリされたオブジェクトの新しいインスタンスが作成されます。

クエリキャッシュはアクションの開始時に作成され、そのアクションの終了時に破棄されるため、アクションの継続時間中のみ保持されます。クエリ結果をより永続的な形で保存したい場合は、低レベルキャッシュを利用できます。

3 依存関係の管理

キャッシュを正しく無効にするには、キャッシュの依存関係を適切に定義する必要があります。

多くの場合、Railsでは依存関係が適切に処理されるので、特別な対応は不要です。ただし、カスタムヘルパーでキャッシュを扱うなどの場合は、明示的に依存関係を定義する必要があります。

3.1 暗黙の依存関係

テンプレートの依存関係は、ほとんどの場合テンプレート自身で呼び出されるrenderから導出されます。

デコード方法を取り扱うActionView::Digestorrenderを呼び出す方法の例を以下にいくつか示します。

render partial: "comments/comment", collection: commentable.comments
render "comments/comments"
render "comments/comments"
render("comments/comments")

render "header" # render("comments/header")に変換される

render(@topic)         # render("topics/topic")に変換される
render(topics)         # render("topics/topic")に変換される
render(message.topics) # render("topics/topic")に変換される

ただし、一部の呼び出しについては、キャッシュが適切に動作するための変更が必要です。たとえば、独自のコレクションを渡す場合は、次のように変更する必要があります。

render @project.documents.where(published: true)

上のコードを次のように変更します。

render partial: "documents/document", collection: @project.documents.where(published: true)

3.2 明示的な依存関係

テンプレートの依存関係をまったく導出できないことがあります。以下のようなヘルパー内でのレンダリングは、導出できない典型的な例です。

<%= render_sortable_todolists @project.todolists %>

このような呼び出しでは、以下のような特殊コメント形式で明示的に依存関係を示す必要があります。

<%# Template Dependency: todolists/todolist %>
<%= render_sortable_todolists @project.todolists %>

単一テーブル継承(STI)などでは、こうした明示的な依存関係を多数書かなければならなくなる可能性もあります。

テンプレートごとに依存関係を書く代わりに、以下のようにワイルドカードを用いてディレクトリ内の任意のテンプレートにマッチさせることも可能です。

<%# Template Dependency: events/* %>
<%= render_categorizable_events @person.events %>

コレクションのキャッシュで、パーシャルの冒頭で純粋なキャッシュ呼び出しが行われない場合は、以下の特殊コメント形式をテンプレートの任意の場所に追加することで、コレクションキャッシュを引き続き有効にできます。

<%# Template Collection: notification %>
<% my_helper_that_calls_cache(some_arg, notification) do %>
  <%= notification.name %>
<% end %>

3.3 外部の依存関係

たとえば、キャッシュされたブロック内でヘルパーメソッドを利用すると、このヘルパーを更新するときにキャッシュも更新しなければならなくなります。キャッシュをどんな方法で更新するかはさほど重要ではありませんが、テンプレートファイルのMD5を変更する必要があります。

推奨されている方法の1つは、以下のように特殊コメントで明示的に更新を示すことです。

<%# Helper Dependency Updated: Jul 28, 2015 at 7pm %>
<%= some_helper_method(person) %>

4 Solid Cache

Solid Cacheは、データベース上に構築されるActive Supportキャッシュストアです。 従来のハードディスクよりずっと高速な最新のSSD(ソリッドステートドライブ)を活用して、より大きなストレージ容量とシンプルなインフラストラクチャを備えたコストパフォーマンスの高いキャッシュを提供します。

SSDはRAMより若干遅いのですが、ほとんどのアプリケーションではその差はわずかであり、RAMよりも多くのデータを保存できるため、キャッシュを頻繁に無効化する必要がないことで補われています。その結果、平均キャッシュミスが少なくなり、応答時間も速くなります。

Solid CacheはFIFO(先入れ先出し)キャッシュ戦略を採用しています。

FIFO戦略では、キャッシュが上限に達したときに、キャッシュに最初に追加された項目が最初に削除されます。このアプローチは、最も最近アクセスされていない項目を最初に削除する LRU(least recently used: 最も最近使われていない)キャッシュ戦略と比較するとシンプルな代わりに効率は落ちます。これにより、利用頻度の高いデータが重点的に最適化されます。 ただしSolid Cacheは、キャッシュの持続期間を長くすることでFIFOの低効率を補い、キャッシュが無効化される頻度を減らします。

Solid Cacheは、Rails 8.0以降ではデフォルトで有効になっています。ただし、Solid Cacheが不要な場合は、以下のようにrails newコマンドでスキップできます。

rails new app_name --skip-solid

--skip-solidフラグを指定すると、Solid CacheとSolid Queueが両方ともスキップされます。Solid Queueを利用するがSolid Cacheは利用しない場合は、rails app:enable-solid-queueを実行することでSolid Queueを有効にできます。

4.1 データベースを設定する

Solid Cacheを利用するには、config/database.ymlファイルでデータベースコネクションを設定できます。 以下はSQLiteデータベースの設定例です。

production:
  primary:
    <<: *default
    database: storage/production.sqlite3
  cache:
    <<: *default
    database: storage/production_cache.sqlite3
    migrations_paths: db/cache_migrate

この設定では、キャッシュされたデータを保存するためにcacheで指定したデータベースが使われます。必要に応じて、MySQLやPostgreSQLなどの別のデータベースアダプタも指定できます。

production:
  primary: &primary_production
    <<: *default
    database: app_production
    username: app
    password: <%= ENV["APP_DATABASE_PASSWORD"] %>
  cache:
    <<: *primary_production
    database: app_production_cache
    migrations_paths: db/cache_migrate

キャッシュの設定でdatabasedatabasesが無指定の場合、Solid CacheはActiveRecord::Baseコネクションプールを使います。つまり、キャッシュの読み取りと書き込みは、それらをラップするデータベーストランザクションの一部になります。

production環境のキャッシュストアは、以下のようにデフォルトでSolid Cacheストアを利用するように設定されます。

  # config/environments/production.rb
  config.cache_store = :solid_cache_store

前述のRails.cacheによる低レベルキャッシュも参照してください。

4.2 キャッシュストアをカスタマイズする

Solid Cacheの設定は、config/cache.ymlファイルでカスタマイズできます。

default: &default
  store_options:
    # 保持ポリシーを満たすために最も古いキャッシュエントリの保存期間に上限を設定する
    max_age: <%= 60.days.to_i %>
    max_size: <%= 256.megabytes %>
    namespace: <%= Rails.env %>

store_optionsで利用できるキーの完全なリストについては、Solid Cache READMEのキャッシュ設定を参照してください。

ここではmax_ageオプションとmax_sizeオプションを調整して、キャッシュエントリの有効期間とサイズを制御できます。

4.3 キャッシュの有効期限を処理する

Solid Cacheは、書き込みごとにカウンタを増やすことでキャッシュ書き込みをトラッキングします。 カウンタがキャッシュ設定expiry_batch_sizeの50%に達すると、キャッシュの有効期限を処理するバックグラウンドタスクがトリガーされます。

このアプローチにより、キャッシュ容量を縮小する必要がある場合、キャッシュレコードの有効期限が書き込みよりも早いタイミングで確実に失効するようになります。

バックグラウンドタスクは書き込みがある場合にのみ実行されるため、キャッシュが更新されない限りプロセスはアイドル状態のままです。

有効期限プロセスをスレッドではなくバックグラウンドジョブで実行したい場合は、キャッシュ設定expiry_method:jobに設定します。

4.4 キャッシュをシャーディングする

キャッシュでさらなるスケーラビリティが必要な場合のために、Solid Cacheではシャーディング(sharding: キャッシュを複数のデータベースに分割する)をサポートしています。 これによりキャッシュの負荷が分散されてさらに強力になります。

シャーディングを有効にするには、まず以下のように複数のキャッシュデータベースをdatabase.ymlに追加します。

# config/database.yml
production:
  cache_shard1:
    database: cache1_production
    host: cache1-db
  cache_shard2:
    database: cache2_production
    host: cache2-db
  cache_shard3:
    database: cache3_production
    host: cache3-db

さらに、キャッシュの設定ファイルでシャードを指定する必要もあります。

# config/cache.yml
production:
  databases: [cache_shard1, cache_shard2, cache_shard3]

4.5 暗号化

Solid Cacheは、機密データを保護するための暗号化をサポートしています。

暗号化を有効にするには、キャッシュ設定ファイルでencrypt値を設定します。

# config/cache.yml
production:
  encrypt: true

さらに、アプリケーションでActive Record暗号化を利用する設定も必要です。

4.6 developmentモードでのキャッシュ

developmentモードでは、デフォルトで:memory_storeによるキャッシュが有効になります。これは、デフォルトで無効になっているAction Controllerキャッシュには適用されません。

Railsは、Action Controllerキャッシュの有効・無効を切り替えるbin/rails dev:cacheコマンドを提供しています。

$ bin/rails dev:cache
Development mode is now being cached.
$ bin/rails dev:cache
Development mode is no longer being cached.

development環境でSolid Cacheを使いたい場合は、config/environments/development.rbファイルでcache_store:solid_cache_storeを設定します。

config.cache_store = :solid_cache_store

さらに、cacheデータベースを作成してマイグレーションを実行しておく必要もあります。

development:
  <<: * default
  database: cache

キャッシュそのものを無効にするには、cache_store:null_storeを設定します。

5 その他のキャッシュストア

Railsは、キャッシュデータを保存するさまざまなストアが用意されています(SQLキャッシュを除く)。

5.1 設定

アプリケーションのデフォルトのキャッシュストアは、config.cache_storeオプションで設定できます。キャッシュストアのコンストラクタには、引数として他のパラメータも渡せます。

config.cache_store = :memory_store, { size: 64.megabytes }

または、設定ブロックの外部でActionController::Base.cache_storeを設定することも可能です。

キャッシュにアクセスするには、Rails.cacheを呼び出します。

5.1.1 コネクションプールのオプション

:mem_cache_store:redis_cache_storeは、デフォルトではプロセスごとに1つのコネクションを利用します。これは、Puma(または別のスレッド化サーバー)を使えば、複数のスレッドがキャッシュストアへのクエリを同時実行できるということです。

コネクションプールを無効にしたい場合は、キャッシュストアの設定時に:poolオプションをfalseに設定します。

config.cache_store = :mem_cache_store, "cache.example.com", { pool: false }

また、:poolオプションに個別のオプションを指定することで、デフォルトのプール設定をオーバーライドすることも可能です。

config.cache_store = :mem_cache_store, "cache.example.com", { pool: { size: 32, timeout: 1 } }
  • :size: プロセス1個あたりのコネクション数を指定します(デフォルトは5)

  • :timeout: コネクションごとの待ち時間を秒で指定します(デフォルトは5)。 タイムアウトまでにコネクションを利用できない場合は、Timeout::Errorエラーが発生します。

5.2 ActiveSupport::Cache::Store

ActiveSupport::Cache::Storeは、Railsでキャッシュとやりとりするための基盤を提供します。これは抽象クラスなので、単体では利用できません。代わりに、ストレージエンジンと結びついたこのクラスの具体的な実装が必要です。

Railsには、以下で説明するいくつかの実装が組み込まれています。

主要なAPIメソッドを以下に示します。

キャッシュストアのコンストラクタに渡されるオプションは、該当するAPIメソッドのデフォルトオプションとして扱われます。

5.3 ActiveSupport::Cache::MemoryStore

ActiveSupport::Cache::MemoryStoreは、エントリーを同じRubyプロセス内のメモリに保持します。

キャッシュストアのサイズを制限するには、イニシャライザで:sizeオプションを指定します(デフォルトは32MB)。キャッシュがこのサイズを超えるとクリーンアップが開始され、直近の利用が最も少ない(LRU: Least Recently Used)エントリから削除されます。

config.cache_store = :memory_store, { size: 64.megabytes }

Ruby on Railsサーバーのプロセスを複数実行している場合(Phusion PassengerやPumaをクラスタモードで利用している場合)は、Railsサーバーのキャッシュデータをプロセスのインスタンス間で共有できなくなります。

このキャッシュストアは、アプリケーションを大規模にデプロイするには向いていません。ただし、小規模でトラフィックの少ないサイトでサーバープロセスを数個動かす程度であれば問題なく動作します。もちろん、development環境やtest環境でも動作します。

新規Railsプロジェクトのdevelopment環境では、この実装をデフォルトで使うよう設定されます。

:memory_storeを使うとキャッシュデータがプロセス間で共有されないため、Railsコンソールから手動でキャッシュを読み書きすることも無効にすることもできません。

5.4 ActiveSupport::Cache::FileStore

ActiveSupport::Cache::FileStoreは、エントリをファイルシステムに保存します。キャッシュを初期化するときに、ファイル保存場所へのパスを指定する必要があります。

config.cache_store = :file_store, "/path/to/cache/directory"

このキャッシュストアを使うと、同一ホスト上にある複数のサーバープロセス間でキャッシュを共有できるようになります。

このキャッシュストアは、トラフィックが中規模程度のサイトを1、2個程度ホストする場合に向いています。異なるホストで実行するサーバープロセス間のキャッシュを共有することも一応可能ですが、おすすめできません。

ファイルストアのキャッシュはディスクがいっぱいになるまで増加するため、古いエントリを定期的に削除することをおすすめします。

5.5 ActiveSupport::Cache::MemCacheStore

ActiveSupport::Cache::MemCacheStoreは、アプリケーションキャッシュの保存先をDangaのmemcachedサーバーに一元化します。

Railsでは、本体にバンドルされているdalli gemがデフォルトで使われます。dalliは、現時点で最も広くproduction Webサイトで利用されているキャッシュストアです。高性能かつ高冗長性を備えており、単一のキャッシュストアも共有キャッシュクラスタも提供できます。

キャッシュを初期化するときは、クラスタ内の全memcachedサーバーのアドレスを指定するか、MEMCACHE_SERVERS環境変数を適切に設定しておく必要があります。

config.cache_store = :mem_cache_store, "cache-1.example.com", "cache-2.example.com"

どちらも指定されていない場合は、memcachedがlocalhostのデフォルトポート(127.0.0.1:11211)で実行されていると仮定しますが、これは大規模サイトのセットアップには向いていません。

config.cache_store = :mem_cache_store # $MEMCACHE_SERVERSにフォールバックし、次に127.0.0.1:11211になる

サポートされているアドレスの種類について詳しくはDalli::Clientのドキュメントを参照してください。

このキャッシュのwriteメソッド(およびfetchメソッド)には、memcached固有の機能を利用する追加オプションを渡せます。

5.6 ActiveSupport::Cache::RedisCacheStore

ActiveSupport::Cache::RedisCacheStoreは、メモリ使用量が最大に達したときにRedisの自動eviction(立ち退き)を利用して、Memcachedキャッシュサーバーと同様の機能を実現しています。

デプロイに関するメモ: Redisのキーはデフォルトでは無期限なので、専用のRedisキャッシュサーバーを使うときはご注意ください。永続化用のRedisサーバーに期限付きのキャッシュデータを保存してはいけません。詳しくはRedis cache server setup guide(英語)を参照してください。

「キャッシュのみ」のRedisサーバーでは、maxmemory-policyに以下のいずれかのallkeysを設定してください。

Redis 4以降ではallkeys-lfuによるLFU(Least Frequently Used: 利用頻度が最も低いキャッシュを削除する)evictionアルゴリズムがサポートされており、これはデフォルトの選択肢として優れています。

Redis 3以前では、allkeys-lruを用いてLRU(Least Recently Used: 直近の利用が最も少ないキャッシュを削除する)アルゴリズムにすべきです。

キャッシュの読み書きのタイムアウトは、やや低めに設定しましょう。キャッシュの取り出しで1秒以上待つよりも、キャッシュ値を再生成する方が高速になることもよくあります。読み書きのデフォルトタイムアウト値は1秒ですが、ネットワークのレイテンシが常に低い場合は値を小さくするとよい結果が得られることがあります。

キャッシュストアがリクエスト中に接続に失敗した場合、デフォルトではRedisへの再接続を1回試みます。

キャッシュの読み書きでは決して例外が発生せず、単にnilを返してあたかも何もキャッシュされていないかのように振る舞います。

キャッシュで例外が生じているかどうかを測定するには、error_handlerを渡して例外収集サービスにレポートを送信してもよいでしょう。例外収集サービスは以下の3つのキーワード引数を受け取れる必要があります。

  • method: 最初に呼び出されたキャッシュストアメソッド名
  • returning: ユーザーに返した値(通常はnil
  • exception: rescueされた例外

Redisを利用するには、まずGemfileにredis gemを追加します。

gem "redis"

最後に、関連するconfig/environments/*.rbファイルに以下の設定を追加します。

config.cache_store = :redis_cache_store, { url: ENV["REDIS_URL"] }

より複雑なproduction向けRedisキャッシュストアは、以下のような感じになります。

cache_servers = %w(redis://cache-01:6379/0 redis://cache-02:6379/0)
config.cache_store = :redis_cache_store, { url: cache_servers,

  connect_timeout:    30,  # デフォルトは1(秒)
  read_timeout:       0.2, # デフォルトは1(秒)
  write_timeout:      0.2, # デフォルトは1(秒)
  reconnect_attempts: 2,   # デフォルトは1

  error_handler: -> (method:, returning:, exception:) {
    # エラーをwarningとしてSentryに送信する
    Sentry.capture_exception exception, level: "warning",
      tags: { method: method, returning: returning }
  }
}

5.7 ActiveSupport::Cache::NullStore

ActiveSupport::Cache::NullStoreは個別のWebリクエストを対象とし、リクエストが終了すると保存された値をクリアするので、development環境やtest環境での利用のみを想定しています。

Rails.cacheと直接やりとりするコードを使っていて、キャッシュが原因でコード変更の結果が反映されなくなる場合にこのキャッシュストアを使うと、非常に便利です。

config.cache_store = :null_store
5.7.1 カスタムのキャッシュストア

キャッシュストアを独自に作成するには、ActiveSupport::Cache::Storeを拡張して適切なメソッドを実装します。これにより、Railsアプリケーションでさまざまなキャッシュ技術に差し替えられるようになります。

カスタムのキャッシュストアを利用するには、自作クラスの新しいインスタンスにキャッシュストアを設定します。

config.cache_store = MyCacheStore.new

6 キャッシュのキー

キャッシュで使うキーには、cache_keyto_paramに応答する任意のオブジェクトが使えます。自分のクラスでcache_keyメソッドを実装すると、カスタムキーを生成できるようになります。Active Recordは、このクラス名とレコードidに基づいてキーを生成します。

キャッシュのキーとして、値のハッシュと配列を指定できます。

# このキャッシュキーは有効
Rails.cache.read(site: "mysite", owners: [owner_1, owner_2])

Rails.cacheで使うキーは、ストレージエンジンで実際に使われるキーと同じになりません。実際のキーは、バックエンドの技術的制約に合わせて名前空間化または変更される可能性もあります。そのため、たとえばRails.cacheで値を保存してからdalli gemで値を取り出すようなことはできません。その代わり、memcachedのサイズ制限超過や構文規則違反を気にする必要もありません。

7 条件付きGETのサポート

条件付きGETは、HTTP仕様で定められた機能です。「GETリクエストへのレスポンスが前回リクエストのレスポンスから変更されていなければ、ブラウザ内キャッシュを安全に利用できる」ことを、Webサーバーからブラウザに通知します。

この機能は、HTTP_IF_NONE_MATCHヘッダとHTTP_IF_MODIFIED_SINCEヘッダを使って、一意のコンテンツidや最終更新タイムスタンプをやり取りします。 コンテンツid(ETag)または最終更新タイムスタンプがサーバー側のバージョンと一致する場合は、「変更なし」ステータスのみを持つ空レスポンスをサーバーが返すだけで済みます。

最終更新タイムスタンプやif-none-matchヘッダの有無を確認して、完全なレスポンスを返す必要があるかどうかを決定するのは、サーバー側(つまり開発者)の責任です。

Railsでは、次のように条件付きGETを比較的簡単に利用できます。

class ProductsController < ApplicationController
  def show
    @product = Product.find(params[:id])

    # 指定のタイムスタンプやETag値によって、リクエストが古いことがわかった場合
    # (再処理が必要な場合)、このブロックを実行する
    if stale?(last_modified: @product.updated_at.utc, etag: @product.cache_key_with_version)
      respond_to do |wants|
        # ... 通常のレスポンス処理
      end
    end

    # リクエストがフレッシュな(つまり前回から変更されていない)場合は処理不要。
    # デフォルトのレンダリングでは、前回の`stale?`呼び出しの結果に基いて
    # 処理が必要かどうかを判断して :not_modifiedを送信するだけでよい。
  end
end

オプションハッシュの代わりに、単にモデルを渡すことも可能です。Railsのlast_modifiedetagの設定では、updated_atメソッドやcache_key_with_versionメソッドが使われます。

class ProductsController < ApplicationController
  def show
    @product = Product.find(params[:id])

    if stale?(@product)
      respond_to do |wants|
        # ... 通常のレスポンス処理
      end
    end
  end
end

特殊なレスポンス処理を使わずにデフォルトのレンダリングメカニズムを利用する(つまりrespond_toも使わず独自レンダリングもしない)場合は、fresh_whenヘルパーで簡単に処理できます。

class ProductsController < ApplicationController
  # リクエストがフレッシュな自動的に:not_modifiedを返す
  # 古い場合はデフォルトのテンプレート(product.*)を返す

  def show
    @product = Product.find(params[:id])
    fresh_when last_modified: @product.published_at.utc, etag: @product
  end
end

last_modifiedetagが両方設定されている場合の振る舞いは、config-action-dispatch-strict-freshness設定の値によって異なります。

  • trueに設定されている場合: RFC 7232セクション6で指定されているようにetagのみが考慮されます。
  • falseに設定されている場合: 両方の条件が満たされていれば、キャッシュは最新のものと見なされます。 これは、従来のRailsの振る舞いと同じです。

静的ページなどの有効期限のないページでキャッシュを有効にしたいことがあります。http_cache_foreverヘルパーを使うと、ブラウザやプロキシでキャッシュを無期限にできます。

キャッシュのレスポンスはデフォルトではprivateになっており、キャッシュはユーザーのWebブラウザでのみ行われます。プロキシでレスポンスをキャッシュ可能にするには、public: trueを設定してすべてのユーザーへのレスポンスがキャッシュされるようにします。

このヘルパーメソッドを使うと、last_modifiedヘッダーがTime.new(2011, 1, 1).utcに設定され、expiresヘッダーが100年に設定されます。

このメソッドの利用には十分ご注意ください。ブラウザやプロキシにキャッシュされたレスポンスは、ユーザーがブラウザやプロキシでキャッシュを強制的にクリアしない限り無効にできません。

class HomeController < ApplicationController
  def index
    http_cache_forever(public: true) do
      render
    end
  end
end

7.1 強いETagと弱いETag

Railsは、デフォルトで「弱い」ETagを使います。

弱いETagでは、レスポンスのbodyが微妙に異なる場合にも同じETagを与えることで、事実上同じレスポンスとして扱えるようになります。レスポンスbodyのごく一部が変更されたときにページを再生成したくない場合に便利です。

弱いETagの冒頭にはW/が追加されるので、強いETagと区別できます。

  W/"618bbc92e2d35ea1945008b42799b0e7" → 弱いETag
  "618bbc92e2d35ea1945008b42799b0e7"   → 強いETag

強いETagは、弱いETagと異なり、レスポンスがバイトレベルで完全一致しなければなりません。

強いETagは巨大な動画やPDFファイル内でRangeリクエストを実行する場合に便利です。Akamaiなど一部のCDNでは、強いETagのみをサポートしています。

強いETagの生成がどうしても必要な場合は、次のようにできます。

class ProductsController < ApplicationController
  def show
    @product = Product.find(params[:id])
    fresh_when last_modified: @product.published_at.utc, strong_etag: @product
  end
end

以下のように、レスポンスに強いETagを直接設定することも可能です。

response.strong_etag = response.body # => "618bbc92e2d35ea1945008b42799b0e7"

フィードバックについて

Railsガイドは GitHub の yasslab/railsguides.jp で管理・公開されております。本ガイドを読んで気になる文章や間違ったコードを見かけたら、気軽に Pull Request を出して頂けると嬉しいです。Pull Request の送り方については GitHub の README をご参照ください。

原著における間違いを見つけたら『Rails のドキュメントに貢献する』を参考にしながらぜひ Rails コミュニティに貢献してみてください 🛠💨✨

本ガイドの品質向上に向けて、皆さまのご協力が得られれば嬉しいです。

Railsガイド運営チーム (@RailsGuidesJP)

支援・協賛

Railsガイドは下記の協賛企業から継続的な支援を受けています。支援・協賛にご興味あれば協賛プランからお問い合わせいただけると嬉しいです。

  1. Star
  2. このエントリーをはてなブックマークに追加