Rails でのキャッシング戦略
最終更新日 2024年04月25日(木)
Table of Contents
Web アプリケーションでは、ごく一部のページのロードに非常に時間がかかるのが一般的です。Heroku では、実行時間の長いリクエストによって dyno が拘束され、アプリケーションのパフォーマンスに深刻な影響が出る可能性があります。どのページやデータベースリクエストの実行速度が遅いかを確認するには、New Relic を使用します (Web の 「Transactions」 (トランザクション) および 「Database」 (データベース) タブを参照)。最も実行時間が長いリクエストを調べます。低速なデータベースまたは API トランザクションが原因の場合、(Rails.cache.read/write/fetch
などの) 低レベルキャッシングを使用して情報をキャッシュします。
cache-money や cache_fu などの ‘自動的で魔法のような’ キャッシングライブラリは使用しないことをお勧めします。Heroku でも時間をかけて簡易なキャッシングソリューションを調査しましたが、満足できるものは見つかりませんでした。一般的に、キャッシングはアプリケーション固有の取り組みです。Heroku で実行する Rails アプリでキャッシングによってパフォーマンスの向上を目指すロードマップについて、以下で説明します。
memcached を使用するようにアプリケーションを設定すると、Rails では、アクションとフラグメント両方のキャッシングに memcached を自動的に使用するようになります。
HTTP キャッシング
Rails での HTTP ヘッダーを使用したキャッシングは、コードをほとんど変更しなくてもアプリケーションに簡単に適用できる手法であり、こちらの別記事で説明しています。
ページキャッシング
Rails の page_caching gem は、ファイルシステム上にファイルを作成することによって機能します。Heroku には一時的なファイルストアがあるため、ページキャッシングは一見有効そうですが、意図したようには機能しません。代わりに、アクションキャッシングまたはフラグメントキャッシングを使用してください。あるいは、Rack::Cache をリバースプロキシとして使用して、アプリへのリクエストを全面的に回避してください。
アクションキャッシング
ページで認証またはその他の事前/事後フィルターが必要な場合でも、Rails の action_caching gem を使用してコンテンツをキャッシュできます。アクションキャッシングは Memcache アドオンを使用します (また、必要とします)。
特定のアクションでキャッシングを有効にするには、caches_action :<action_name>
をコントローラーに追加するだけです。レイアウトに動的な要素が含まれる (たとえば、ユーザー名やメールアドレスがヘッダーに含まれる) 場合、アクションの内容をキャッシュしたままレイアウトを動的にレンダリングできます。これを行うには、:layout => false
フラグを使用します。最後に、expire_action
コマンドを使用すると、新しいデータが書き込まれたときにキャッシュからアクションを削除できます。
次の Rails コントローラーのコードは、以上の概念を示しています。
# products_controller.rb
class ProductsController < ActionController
before_filter :authenticate
caches_action :index
caches_action :show, :layout => false
def index
@products = Product.all
end
def show
@product = Product.find(params[:id])
end
def create
expire_action :action => :index
end
end
(引用元: Rails ガイド)
フラグメントキャッシング
フラグメントキャッシングは、アプリケーションでウィジェットまたは部品をキャッシュするための優れたメカニズムです。フラグメントキャッシングは Memcache アドオンを使用します (また、必要とします)。たとえば、アプリで次のようにして商品を一覧表示するとします。
# index.html.erb
<%= render :partial => "product", :collection => @products %>
# _product.html.erb
<div><%= link_to product, product.name %>: <%= product.price%></div>
<div><%= do_something_comlicated%></div>
このとき、フラグメントキャッシングを使用すると、個別商品の部品を簡単にキャッシュできます。ActiveRecord オブジェクトを Rails に渡すと、Rails によってキャッシュキーが自動的に生成されます。
# _product.html.erb
<% cache(product) do %>
<div><%= link_to product, product.name %>: <%= product.price%></div>
<div><%= do_something_comlicated%></div>
<% end %>
もう 1 つのフラグメントキャッシング戦略は、新しいページロードのたびにページのウィジェットやその他の独立部品をライブデータストアから更新する必要がない場合に、それらの要素をキャッシュするというものです。たとえば、最も売れている商品のリストを Web サイトのフロントページに表示する場合に、この部品をキャッシュすることができます。1 時間おきに情報を更新する場合、次のようになります。
# index.html.erb
<% cache("top_products", :expires_in => 1.hour) do %>
<div id="topSellingProducts">
<% @recent_product = Product.order("units_sold DESC").limit(20) %>
<%= render :partial => "product", :collection => @recent_products %>
</div>
<% end %>
低レベルキャッシング
低レベルキャッシングでは、Rails.cache
オブジェクトを直接使用して任意の情報をキャッシュします。取得のコストが高く、多少古くなっても問題が少ないデータの保存に使用します。データベースクエリや API 呼び出しが一般的な用途です。
低レベルキャッシングは、Rails.cache.fetch
メソッドを使用すると最も効率的に実装できます。このメソッドは、キャッシュが利用可能であればキャッシュから値を読み取り、利用できない場合は、メソッドに渡されたブロックを実行して結果を返します。
>> Rails.cache.fetch('answer')
==> "nil"
>> Rails.cache.fetch('answer') {1 + 1}
==> 2
Rails.cache.fetch('answer')
==> 2
次の例を考えてみましょう。アプリケーションの Product モデルには、すべての在庫切れ商品を返すクラスメソッドと、競合 Web サイトでの商品価格を検索するインスタンスメソッドがあります。これらのメソッドによって返されるデータは、低レベルキャッシングに最も適しています。
# product.rb
def Product.out_of_stock
Rails.cache.fetch("out_of_stock_products", :expires_in => 5.minutes) do
Product.all.joins(:inventory).conditions.where("inventory.quantity = 0")
end
end
def competing_price
Rails.cache.fetch("/product/#{id}-#{updated_at}/comp_price", :expires_in => 12.hours) do
Competitor::API.find_price(id)
end
end
この最後の例では、モデルの id 属性と update_at 属性に基づいてキャッシュキーを生成しています。これは一般的な方法であり、商品が更新されるたびにキャッシュが無効化されるという利点があります。一般的には、インスタンスレベルの情報に低レベルキャッシングを使用するときは、キャッシュキーを生成する必要があります。
参考情報
- Rails でのキャッシング に関する RailsGuides の記事