本ガイドでは、キャッシュを導入してRailsアプリケーションを高速化する方法を解説します。
このガイドの内容:
キャッシュ(caching)とは、リクエスト・レスポンスサイクルの中で生成されたコンテンツを保存しておき、次回同じようなリクエストが発生したときのレスポンスでそのコンテンツを再利用することを指します。コストのかかる処理を何度も繰り返すのではなく、一度計算した結果を保存しておいて後で参照することで時間を節約するようなものです。
キャッシュはアプリケーションのパフォーマンスをきわめて効果的に向上させる方法です。キャッシュを導入することで、サーバー1台とデータベース1台のWebサイトのような小規模なインフラでも、数千ユーザーの同時接続を処理できるようになります。
Railsは、データのキャッシュだけでなく、キャッシュの有効期限、キャッシュの依存関係、キャッシュの無効化などにも対応できるキャッシュ機能をひと通り標準で提供しています。
Action Controllerのキャッシュは、デフォルトではproduction環境でのみ有効になります。
bin/rails dev:cacheコマンドを実行するか、config/environments/development.rbファイルでconfig.action_controller.perform_cachingをtrueに設定することで、ローカルでキャッシュを試せるようになります。
$ bin/rails dev:cache Development mode is now being cached. $ bin/rails dev:cache Development mode is no longer being cached.
config.action_controller.perform_caching値の変更は、Action Controllerコンポーネントで提供されるキャッシュでのみ有効です。つまり、後述する低レベルキャッシュの動作には影響しません。
新規Railsアプリケーションのdevelopment環境では、デフォルトで:memory_storeによるキャッシュが使われます。development環境でSolid Cacheを使いたい場合は、config/environments/development.rbファイルのcache_store設定を以下のように変更してください。
config.cache_store = :solid_cache_store
さらに、config/database.ymlのdevelopment/cacheにあるデータベースの設定、作成、マイグレーションを完了してください。
development:
primary:
<<: *default
database: storage/development.sqlite3
cache:
<<: *default
database: storage/development_cache.sqlite3
migrations_paths: db/cache_migrate
データベースの設定が完了したら、bin/rails db:prepareを実行してキャッシュ用のテーブルを作成します。
キャッシュそのものを無効にするには、cache_storeに:null_storeを設定します。
訳注: 「ページキャッシュ」と「アクションキャッシュ」の項目はRails 8.0.1で削除されました。
Railsは、多くのニーズやユースケースに対応できるさまざまなキャッシュ戦略を提供しています。メリットや有用なシナリオは、アプローチごとに異なります。
Rails.cacheによる低レベルキャッシュRails.cacheで利用できるRailsの低レベルキャッシュ(low-level caching)は、APIレスポンス、計算結果、高負荷クエリの結果などのシリアライズ可能な値を保存します。これにより、ビュー全体をキャッシュすることなく、個々のデータをキャッシュできるようになります。
Rails.cache.fetchメソッドは、キャッシュからの読み取りと書き込みの両方を処理します。
このメソッドが単一の引数で呼び出された場合、指定されたキーに対するキャッシュ済みの値を取得して返します。
このメソッドにブロックを渡すと、キャッシュミスのときだけブロックが実行されます。ブロックの戻り値は、指定されたキャッシュキーの下にキャッシュされ、返されます。キャッシュヒットの場合、ブロックを実行せずにキャッシュ済みの値が直接返されます。
利用例:
# `fetch`は、キャッシュが存在しない場合はデフォルト値を設定するためにブロックで値を取得する welcome_message = Rails.cache.fetch("welcome_message") { "Welcome to Rails!" } puts welcome_message # Output: Welcome to Rails!
「キャッシュがヒットする」とは、Railsがキャッシュ内に既存の値を見つけて再利用できたことを意味します。「キャッシュがミスする」とは、値がまだキャッシュ内に存在せず、Railsがそれを生成して保存する必要があったことを意味します。キャッシュミスは正常な動作であり、特にエントリの有効期限が切れた場合、キャッシュがクリアされた場合、またはキーが初めて使われた場合に発生します。
より高度なユースケースとして、Rails.cache.fetchにrace_condition_ttlのようなオプションも指定できます。これはキャッシュスタンピード(複数のプロセスが同時にキャッシュをリビルドしようとする状態)を防ぐのに有用なオプションで、1つのプロセスがエントリをリビルドしている間、期限切れ間もないエントリを短時間だけ再利用可能にします。オプションの完全なリストはAPIドキュメントActiveSupport::Cache::Storeで参照できます。
あるいは、キャッシュからの読み取りや書き込みをRails.cache.readやRails.cache.writeで指定することも可能です。キーを削除するには、Rails.cache.deleteを使います。
# `write`: 値をキャッシュに保存する Rails.cache.write("greeting", "Hello, world!") # `read`: キャッシュから値を取り出す greeting = Rails.cache.read("greeting") puts greeting # Output: Hello, world! # `fetch`: キャッシュが存在しない場合はデフォルト値を設定するためにブロックで値を取得する welcome_message = Rails.cache.fetch("welcome_message") { "Welcome to Rails!" } puts welcome_message # Output: Welcome to Rails! # `delete`: キャッシュの値を削除する Rails.cache.delete("greeting")
現在のキャッシュストアからすべてのデータを削除する必要がある場合は、Rails.cache.clearを呼び出します。
これは主にdevelopment環境や、明示的にキャッシュをリセットしたい場合に有用です。production環境でキャッシュをすべてクリアすると、膨大なキャッシュエントリがリビルドされて処理負荷が急増する可能性があります。
キャッシュのキーとして、値のハッシュや配列を指定できます。
# このキャッシュキーは有効 Rails.cache.read(site: "mysite", owners: [owner_1, owner_2])
キャッシュで使うキーには、cache_keyまたはto_paramに応答する任意のオブジェクトが使えます。カスタムキーが必要な場合は、独自のクラスでcache_keyメソッドを実装できます。Active Recordモデルは、モデル名とレコードIDに基づくキャッシュキーを最初から生成します。
以下の例を考えてみましょう。アプリケーションのProductモデルには、競合他社のWebサイトで製品の価格を調べるインスタンスメソッドがあります。このメソッドが返すデータは、低レベルキャッシュに適しています。
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メソッドは、モデルのクラス名、id、updated_at属性に基づいて文字列を<model class name>/<resource id>-<resource updated_at>の形式で生成します。
これは一般によく使われる生成手法であり、製品が更新されるたびにキャッシュが無効になるというメリットがあります。
Rails.cacheで使われるキーは、ストレージエンジンで実際に使われるキーとは異なります。後者のキーは名前空間によって修飾されたり、バックエンド技術の制約に合わせて変更されたりする可能性があるためです。つまり、Rails.cacheで保存した値を、dalli gemなどで取り出すことはできません。その代わり、memcachedのサイズ制限を超過したり、構文規則に違反したりすることを心配する必要もありません。
Railsのキャッシュに、Active Recordオブジェクトのリストを保存することは避けるべきです。
# super_adminsを取り出すSQLクエリは高負荷なので頻繁に実行したくないとする Rails.cache.fetch("super_admin_users", expires_in: 12.hours) do User.super_admins.to_a end
上の例では、super_adminsを表すUserのインスタンスは変更される可能性があり、属性も異なる場合があります。また、レコードが削除されることもあります。development環境では、コード変更時の再読み込みとの組み合わせによってキャッシュストアの動作が不安定になることもあります。
代わりに、以下のようにリソースIDなどのプリミティブなデータ型をキャッシュすべきです。
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
動的なWebアプリケーションでは、基本的にさまざまなコンポーネントを用いてページをビルドしますが、キャッシュの特性はコンポーネントによって異なります。たとえば、サイトのロゴのような静的なコンポーネントは、他のより動的なコンポーネントに比べてキャッシュ期間を長く取ります。
ページ内のパーツごとに個別のキャッシュや有効期限を設定したい場合は、フラグメントキャッシュ(fragment caching)を利用できます。
フラグメントキャッシュでは、ビューのロジックのフラグメントをキャッシュブロックでラップして、次回のリクエストでそれをキャッシュストアから取り出して配信できるようになります。
たとえば、ページ内で表示する製品(product)を製品ごとにキャッシュしたい場合は、以下のように書けます。
<% @products.each do |product| %> <% cache product do %> <%= render product %> <% end %> <% end %>
Railsアプリケーションがこのページへの最初のリクエストを受信すると、一意のキーを持つ新しいキャッシュエントリが保存されます。生成されるキーは以下のようなものになります。
views/products/index:bea67108094918eeba42cd4a6e786901/products/1
キーの途中にある文字列(bea67108094918eeba42cd4a6e786901)は、テンプレートツリーのダイジェストです。これは、キャッシュするビューフラグメントのコンテンツを元に算出されたハッシュダイジェストです。ビューフラグメントが変更されると(HTMLが変更されるなど)このダイジェストも変更され、別のキャッシュエントリとして扱われるようになります。
productレコードから導出されたキャッシュのバージョンもキャッシュエントリに保存されます。productレコードが更新されるとキャッシュバージョンも変更され、古いバージョンを含むキャッシュフラグメントは無効になります。
Railsではキャッシュキーとキャッシュバージョンが分離されていることで、キャッシュキーが再利用可能になります。つまり、productが更新されるたびにキャッシュエントリを新たに作成するのではなく、同じキャッシュキーに書き込まれるようになります。これにより、古いキャッシュエントリが新しいエントリで上書きされるため、キャッシュ容量全体が削減されます。
Memcachedなどのキャッシュストアは、領域の回収が必要になったときに、古いキャッシュエントリを自動削除します。
条件を指定してフラグメントをキャッシュしたい場合は、cache_ifやcache_unlessを利用できます。
<% cache_if admin?, product do %> <%= render product %> <% end %>
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] } %>
cachedは、以下のようにexpires_inキーとkeyキーを受け取るオプションハッシュで設定することも可能です。これにより、キャッシュキーと有効期限を明示的に制御できます。
<%= render partial: 'products/product', collection: @products, cached: { expires_in: 1.hour, key: ->(product) { [I18n.locale, product] } } %>
フラグメントキャッシュを利用する場合、Railsがキャッシュフラグメントを正しく無効化できるよう、テンプレートの依存関係を適切に定義する必要があります。
Railsは、一般的な多くのケースについてはテンプレートの依存関係を自動的に推論しますが、ヘルパー内でのレンダリングや、間接的なrender呼び出しが行われる場合には、テンプレートの依存関係を明示的に宣言する必要が生じることがあります。
Railsは、テンプレート内のrender呼び出しから、テンプレートの依存関係の多くを直接推論できます。たとえば、ActionView::Digestorは以下のような呼び出しを認識できます。
render partial: "comments/comment", collection: commentable.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呼び出しでは、Railsがテンプレートの依存関係を推論するために、より多くの情報を必要とします。たとえば、以下のようにカスタムコレクションを渡す場合です。
render @project.documents.where(published: true)
上のコードは、以下のようにパーシャル名とコレクションを明示的に指定する形に書き換える必要があります。
render partial: "documents/document", collection: @project.documents.where(published: true)
テンプレートの依存関係をまったく導出できないことがあります。典型的な例は、以下のようにrender呼び出しがヘルパーメソッド内で隠蔽されている場合です。
<%= render_sortable_todolists @project.todolists %>
このような呼び出しでは、以下のような特殊コメント形式で明示的に依存関係を示す必要があります。
<%# Template Dependency: todolists/todolist %> <%= render_sortable_todolists @project.todolists %>
単一テーブル継承(STI)などでは、ヘルパーが同じディレクトリ内のさまざまなパーシャルをレンダリングする可能性があります。
このような場合、テンプレートの特殊コメントですべての依存関係を網羅する代わりに、以下のようにワイルドカードを用いてディレクトリ内の任意のテンプレートにマッチさせることも可能です。
<%# Template Dependency: events/* %> <%= render_categorizable_events @person.events %>
キャッシュ呼び出しがヘルパー内で隠蔽されている場合は、コレクションキャッシュ用の特殊コメントも利用できます。
パーシャルテンプレートの冒頭が明示的なcache呼び出しでなければ、このコメントをテンプレート内の任意の場所に追加できます。
<%# Template Collection: notification %> <% my_helper_that_calls_cache(some_arg, notification) do %> <%= notification.name %> <% end %>
テンプレートファイルの外部での変更も、キャッシュされた出力に影響を与える可能性があります。たとえば、キャッシュされたブロック内でヘルパーメソッドを呼び出している場合、ヘルパーのコードを更新してもテンプレートのダイジェストは自動的に変更されません。
そのような場合は、テンプレートのダイジェストが変更されるようにテンプレートを更新します。シンプルな方法の1つは、以下のように更新日時をコメントとして追加・更新することです。
<%# Helper Dependency Updated: Jul 28, 2015 at 7pm %> <%= some_helper_method(person) %>
別のフラグメントキャッシュの内側にフラグメントをキャッシュしたいことがあります。このようにキャッシュをネストする手法を、マトリョーシカ人形のイメージになぞらえてロシアンドールキャッシュ(Russian doll caching)と呼びます。
ロシアンドールキャッシュのメリットは、たとえば内側のフラグメントで製品(product)が1件だけ更新された場合に、内側の他のフラグメントを捨てずに再利用し、外側のフラグメントは通常どおり再生成できることです。
前のセクションで解説したように、キャッシュされたフラグメントは、そのフラグメントが直接依存しているレコードのupdated_at値が変わると失効しますが、そのフラグメントを含む外側のフラグメントは自動的には失効しません。
以下のビューを例に説明します。
<% cache product do %> <%= render product.reviews %> <% end %>
上のビューは、さらに以下のビューをレンダリングします。
<% cache review do %> <%= render review %> <% end %>
内側のreviewが変更されると、そのupdated_at値も変わり、該当するフラグメントは失効します。しかし、外側のproductレコードのupdated_atは自動的には変わらないため、外側のフラグメントは古いデータを返し続けます。
これを解決するには、以下のようにtouchメソッドでモデル同士を連動させます。
class Product < ApplicationRecord has_many :reviews end class Review < ApplicationRecord belongs_to :product, touch: true end
touch: trueを設定すると、内側のreviewレコードのupdated_atが変更されるたびに、関連付けられているproductレコードのupdated_atも変更され、キャッシュが失効するようになります。
共有パーシャルキャッシュ(shared partial caching)は、パーシャルと、そのキャッシュ済み出力をMIMEタイプの異なる複数のテンプレートで共有できます。
たとえば、HTMLテンプレートとJavaScriptテンプレート間でパーシャルキャッシュを共有できます。render partial:が解決されるときに、明示的なフォーマットが指定されていないパーシャルを複数のレスポンスフォーマットで利用できます。
以下のコードは、HTMLリクエストでもJavaScriptリクエストでも利用できます。
render(partial: "hotels/hotel", collection: @hotels, cached: true)
上のコードはhotels/_hotel.html.erbパーシャルファイルを読み込みます。
以下のように、レンダリングするパーシャルで明示的にformatsオプションを指定する方法も使えます。
render(partial: "hotels/hotel", collection: @hotels, formats: :html, cached: true)
上のコードは、MIMEタイプの異なるテンプレート(JavaScriptテンプレートなど)でもhotels/_hotel.html.erbパーシャルファイルを読み込みます。
条件付きGETはHTTP仕様で定められた機能で、サーバーがブラウザに対して、前回のリクエスト以降にレスポンスが変更されていないことを通知し、ブラウザがキャッシュを再利用できるようにする仕組みです。
この仕組みは、ブラウザや中間キャッシュが既にレスポンスの直近のコピーを持っている場合に、サーバーがレスポンスbody全体の再送信を避けるのに有用です。
この仕組みは、If-None-MatchおよびIf-Modified-Sinceリクエストヘッダーと連携して動作します。
サーバーは、レスポンスが最新かどうかをETagや最終更新日時の情報で確認します。ブラウザ側のコピーがサーバーのバージョンと一致する場合、サーバーは304 Not Modifiedレスポンスをbodyなしで返せるようになります。
これらのヘッダーを評価して、完全なレスポンスを返すかどうかを判断するのはサーバー側の責任です。 Railsでは、この仕組みを手軽に実装できます。
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は、updated_atメソッドやcache_key_with_versionメソッドを用いてlast_modifiedやetagを設定します。
class ProductsController < ApplicationController def show @product = Product.find(params[:id]) if stale?(@product) respond_to do |wants| # ... 通常のレスポンス処理 end end end end
特殊なレスポンス処理を使わずにデフォルトのレンダリングメカニズムを利用する(つまりrespond_toも使わず独自のrender呼び出しも行わない)場合は、以下のように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
オプションハッシュの代わりに、単にモデルを渡すことも可能です。
Railsは、updated_atメソッドやcache_key_with_versionメソッドを用いてlast_modifiedやetagを設定します。
class ProductsController < ApplicationController def show @product = Product.find(params[:id]) fresh_when @product end end
last_modifiedとetagが両方とも設定されたときの振る舞いは、config.action_dispatch.strict_freshness設定に依存します。
trueの場合、RFC 7232のセクション6の規定に従い、etagのみが考慮されます。
falseの場合は、両方のヘッダーがチェックされ、両方とも一致した場合にのみ、レスポンスはフレッシュであるとみなされます。
ETagは、レスポンスbodyの特定のバージョンを一意に表すトークンで、多くの場合はハッシュです。サーバーがETagを送信すると、ブラウザは後でETagを返すことで「レスポンスはまだ同じか?」とサーバーに問い合わせられるようになり、完全なレスポンスを取得せずに済みます。
Railsは、デフォルトで「弱い」ETagを使います。 弱いETagは、レスポンスbodyがバイト単位で完全に一致しなくても、意味的に同等のレスポンスが同じETagを共有できるようにします。これは、意味の変わらない微細な表現の違い(無意味なホワイトスペースやフォーマットの変更など)が発生する場合に有用です。
弱いETagの冒頭には以下のようにW/が追加されるので、強いETagと区別できます。
W/"618bbc92e2d35ea1945008b42799b0e7" -> 弱いETag "618bbc92e2d35ea1945008b42799b0e7" -> 強いETag
強いETagは、弱いETagと異なり、レスポンスbodyがバイトレベルで完全一致しなければなりません。
強いETagは、巨大な動画やPDFファイル内でRangeリクエストを実行する場合に便利です(一部の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"
静的ページなどの変更が発生しないページでキャッシュを有効にしたいことがあります。
http_cache_foreverヘルパーを使うと、ブラウザやプロキシでキャッシュを極めて長い期間に設定できます。
キャッシュのレスポンスはデフォルトではprivateになっており、キャッシュはユーザーのWebブラウザでのみ行われます。プロキシでレスポンスをキャッシュ可能にするには、public: trueを設定してプロキシがキャッシュ済みレスポンスをすべてのユーザーに配信してよいことを示します。
このヘルパーメソッドを使うと、last_modifiedヘッダーがTime.new(2011, 1, 1).utcに設定され、Cache-Controlヘッダーが極めて長い期間に設定されます。
http_cache_foreverメソッドの利用には十分ご注意ください。ブラウザやプロキシは、別のURLでレスポンスが変更されるかキャッシュがクリアされるまで、キャッシュしたレスポンスをいつまでも再利用し続けます。
class HomeController < ApplicationController def index http_cache_forever(public: true) do render end end end
Active Recordのクエリキャッシュ(query caching)は、各クエリが返す結果セットをキャッシュする機能です。 同じリクエストまたは同じ実行コンテキスト内で同じクエリが再度実行された場合は、データベースへのクエリを実行する代わりに、キャッシュされた結果セットを利用します。
以下に例を示します。
class ProductsController < ApplicationController def index # 検索クエリの実行 @products = Product.all # ... # 同じクエリの再実行 @products = Product.all end end
同じクエリの2回目の実行では、データベースにアクセスしません。Active Recordは結果セットをメモリから読み出します。ただし取得のたびに、クエリされたオブジェクトの新しいインスタンスが引き続き作成されます。
クエリキャッシュはアクションの開始時に作成され、そのアクションの終了時に破棄されるため、リクエストが継続している間のみ保持されます。クエリ結果をより永続的な形で保存したい場合は、低レベルキャッシュをお使いください。
Solid Cacheは、データベースにバックエンドを持つActive Supportのキャッシュストアで、新規Railsアプリケーションのデフォルトのキャッシュストアとして採用されています。RedisやMemcachedなどのキャッシュサービスを別途実行せずに、従来よりも大容量・高耐久性のキャッシュが必要な場合に適しています。
Solid CacheはFIFO(First In, First Out)キャッシュ戦略を採用しており、キャッシュ容量の上限に達した場合、最初に追加されたアイテムが最初に削除されます。 このアプローチはシンプルですが、LRU(Least Recently Used)キャッシュと比較すると効率が落ちます。LRUは、直近で最もアクセスされていないアイテムを優先的に削除することで、利用頻度の高いデータを最適化できます。しかし、Solid CacheはFIFOの効率の低さを補うために、キャッシュの寿命を長くすることで無効化処理の発生頻度を減らしています。
Rails 8.0以降で生成された新しいRailsアプリケーションには、デフォルトでSolid Cacheが含まれています。Solid Cacheを使わない場合は、アプリケーションを生成するコマンドに--skip-solidフラグを追加してください。
$ bin/rails new app_name --skip-solid
--skip-solidフラグを使うと、Solid3兄弟(Solid Cache、Solid Queue、Solid Cable)のすべての機能がスキップされます。もし一部の機能だけを使いたい場合は、それぞれのインストールガイドに沿って個別にインストールしてください。たとえば、Solid Cacheを使わずにSolid QueueとSolid Cableだけを使いたい場合は、Solid QueueとSolid Cableのインストールガイドを参照してください。
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
databaseまたはdatabasesがキャッシュ設定で指定されていない場合、Solid CacheはActiveRecord::Baseのコネクションプールを利用します。つまり、キャッシュの読み書きは、それを囲んでいるデータベーストランザクションに参加します。
Solid Cacheをキャッシュストアとして利用するには、環境設定ファイルで以下のように設定します。
# config/environments/production.rb config.cache_store = :solid_cache_store
Rails.cacheを呼び出すことでキャッシュにアクセスできます。
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のオプションを調整して、キャッシュエントリの寿命とサイズをそれぞれ制御できます。
Solid Cacheは、キャッシュの書き込みをトラッキングするために、書き込みごとにカウンタをインクリメントします。カウンタがキャッシュ設定で指定したexpiry_batch_sizeの50%に達すると、キャッシュの有効期限を処理するバックグラウンドタスクがトリガーされます。
このアプローチにより、キャッシュ容量を縮小する必要が生じたときに、キャッシュレコードが書き込みを上回るペースで確実に失効するようになります。
バックグラウンドタスクは書き込みが発生した場合にのみ実行されるため、キャッシュが更新されない限りプロセスはアイドル状態のままです。キャッシュの失効処理をスレッドではなくバックグラウンドジョブで実行したい場合は、キャッシュ設定のexpiry_methodを:jobに設定してください。
キャッシュをさらに大規模化する必要が生じた場合のために、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]
Solid Cacheは、機密データを保護するための暗号化をサポートしています。
暗号化を有効にするには、キャッシュ設定ファイルでencrypt値を設定します。
# config/cache.yml production: encrypt: true
さらに、アプリケーションでActive Record暗号化をセットアップする必要もあります。
Railsは、キャッシュデータを保存するさまざまなストアを提供しています(SQLキャッシュを除く)。
別のキャッシュストアをセットアップするには、config.cache_storeオプションを使います。キャッシュストアのコンストラクタには、引数として他のパラメータも渡せます。
config.cache_store = :memory_store, { size: 64.megabytes }
または、設定ブロックの外部でActionController::Base.cache_storeを設定することも可能です。
キャッシュにアクセスするには、Rails.cacheを呼び出します。
:mem_cache_storeと:redis_cache_storeは、デフォルトではコネクションプールを利用します。つまり、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エラーが発生します。
ActiveSupport::Cache::StoreActiveSupport::Cache::Storeは、Railsでキャッシュとやりとりするための基盤を提供します。これは抽象クラスなので、単体では利用できません。代わりに、ストレージエンジンと結びついたこのクラスの具体的な実装が必要です。
Railsには、以下で説明するいくつかの実装が組み込まれています。
主要なAPIメソッドを以下に示します。
キャッシュストアのコンストラクタに渡されたオプションは、該当するAPIメソッドのデフォルトオプションとして扱われます。
ActiveSupport::Cache::MemoryStoreActiveSupport::Cache::MemoryStoreは、エントリを同じRubyプロセス内のメモリに保持します。
キャッシュストアのサイズを制限するには、キャッシュの初期化でmemory_storeを設定するときに: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の実装がデフォルトで使われます。
:memory_storeを使うとキャッシュデータがプロセス間で共有されないため、Railsコンソールでの変更は、そのコンソールプロセスにのみ影響し、実行中のサーバープロセスには影響しません。
ActiveSupport::Cache::FileStoreActiveSupport::Cache::FileStoreは、キャッシュエントリをファイルシステムに保存します。file_storeを使う場合は、キャッシュを初期化するときにファイル保存場所へのパスを指定する必要があります。
config.cache_store = :file_store, "/path/to/cache/directory"
このキャッシュストアを使うと、同一ホスト上にある複数のサーバープロセス間でキャッシュを共有できるようになります。
このキャッシュストアは、1〜2台のホストで運用される、トラフィックが小〜中規模のサイトに向いています。共有ファイルシステムを使えば、異なるホストで実行するサーバープロセス間のキャッシュを共有することも一応可能ですが、この設定は推奨されていません。
ファイルストアのキャッシュはディスクがいっぱいになるまで増加し続けるため、古いエントリを定期的に削除することをおすすめします。
ActiveSupport::Cache::MemCacheStoreActiveSupport::Cache::MemCacheStoreは、memcachedを用いてアプリケーションキャッシュの保存先を一元化します。デフォルトでは、本体にバンドルされているdalli gemが使われます。MemCacheStoreは、高性能かつ冗長性のある単一の共有キャッシュクラスタを提供できます。
キャッシュを初期化するときは、クラスタ内の全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固有の機能を利用する追加オプションを渡せます。
ActiveSupport::Cache::RedisCacheStoreActiveSupport::Cache::RedisCacheStoreは、メモリ使用量が最大に達したときにRedisの自動eviction(立ち退き)を利用して、Memcachedキャッシュサーバーと同様の機能を実現しています。
Redisのキーはデフォルトでは無期限なので、キャッシュ専用のRedisサーバーを別途使うようにし、永続化用のRedisサーバーには期限付きのキャッシュデータを保存しないようにしてください。詳しくはRedis cache server setup guide(英語)を参照してください。
「キャッシュのみ」のRedisサーバーでは、maxmemory-policyをallkeysのバリエーションのいずれかに設定します。最も利用頻度の低いキーを削除するallkeys-lfuは、デフォルトの選択肢として適しています。
キャッシュの読み書きのタイムアウトは、やや小さめに設定しましょう。多くの場合、キャッシュされた値を再生成する方が、1秒以上待って取得するよりも高速です。読み取りと書き込みのタイムアウトはデフォルトで1秒ですが、ネットワークのレイテンシが一貫して低い場合は、さらに短い値を設定できます。
キャッシュストアがリクエスト中にRedisへの接続に失敗した場合、デフォルトでは再接続を1回試みます。
キャッシュの読み書きでは決して例外が発生せず、単にnilを返してあたかも何もキャッシュされていないかのように振る舞います。
キャッシュで例外が生じているかどうかを計測するには、error_handlerを渡して例外収集サービスにレポートを送信してもよいでしょう。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 } } }
ActiveSupport::Cache::NullStoreActiveSupport::Cache::NullStoreは、リクエスト間でキャッシュされた値を永続化しません。development環境やtest環境での利用を想定しています。
null_storeキャッシュストアは、Rails.cacheと直接やりとりするコードを使っていて、キャッシュが原因でコード変更の結果が反映されなくなる場合に使うと非常に便利なことがあります。
config.cache_store = :null_store
キャッシュストアを独自に作成するには、ActiveSupport::Cache::Storeを拡張して適切なメソッドを実装します。これにより、Railsアプリケーションで任意のキャッシュ技術に差し替えられるようになります。
カスタムのキャッシュストアを利用するには、キャッシュストアに自作クラスの新しいインスタンスを設定します。
config.cache_store = MyCacheStore.new
キャッシュの利用は、コントローラのアクションに限定されません。バックグラウンドジョブ、Service Objectパターン、スクリプト、その他のアプリケーションコードでもRails.cacheを利用できます。
低レベルキャッシュは、リクエスト内でもリクエスト外でも同じように動作します。
class ReportJob < ApplicationJob def perform(account) Rails.cache.fetch([account, "daily-report"], expires_in: 1.hour) do account.generate_daily_report end end end
ただし、一部のキャッシュ動作は、Railsの実行コンテキスト内で動作することに依存しています。Active Recordのクエリキャッシュや、その他の実行ごとのステートは、Railsが管理する通常のリクエストやジョブに対して自動的にセットアップされます。
アプリケーションコードをカスタムスレッドや長時間実行されるスクリプトから独自に実行する場合は、Railsがステートを正しく管理できるようにするため、以下のようにRails.application.executor.wrapでラップしてください。
Rails.application.executor.wrap do Rails.cache.fetch("stats", expires_in: 5.minutes) { expensive_calculation } end
Executorや非リクエストコードの実行について詳しくは、Rails のスレッドとコード実行ガイドを参照してください。
一部のキャッシュストアは、「ローカルキャッシュ(local cache)」レイヤをサポートしています。これは、リクエストやブロックの実行中に最近読み取った値をメモリに保持することで、同じキーに対する読み取りの繰り返しを、背後のキャッシュストアを利用せずに提供できるようにします。
ローカルキャッシュは、特にRedisやMemcachedなどのリモートキャッシュストアで非常に有効です。ネットワーク経由の往復が繰り返されるのを避けることでパフォーマンスが向上します。
通常のRailsリクエストでは、ローカルキャッシュはミドルウェアによって管理されます。以下のようにブロックで囲むことで、ローカルキャッシュを手動で利用することも可能です。
Rails.cache.with_local_cache do Rails.cache.read("hot-key") Rails.cache.read("hot-key") end
ローカルキャッシュはあくまで一時的なものであり、現在の実行に限定されます。メインのキャッシュストアに取って代わるものではなく、ローカルキャッシュに書き込まれた値はリクエスト・ジョブ・プロセス間で共有されません。
Railsガイドは GitHub の yasslab/railsguides.jp で管理・公開されております。本ガイドを読んで気になる文章や間違ったコードを見かけたら、気軽に Pull Request を出して頂けると嬉しいです。Pull Request の送り方については GitHub の README をご参照ください。
原著における間違いを見つけたら『Rails のドキュメントに貢献する』を参考にしながらぜひ Rails コミュニティに貢献してみてください 🛠💨✨
本ガイドの品質向上に向けて、皆さまのご協力が得られれば嬉しいです。
Railsガイド運営チーム (@RailsGuidesJP)
Railsガイドは下記の協賛企業から継続的な支援を受けています。もしご興味あれば、協賛プランから気軽にお問い合わせいただけると嬉しいです。