caching strategies - herokaijp/devcenter GitHub Wiki

Webアプリケーションは、ロードに予期せぬ時間を要するページを、少しばかり抱えてしまうことが良くあります。 Heroku上では、長時間のリクエストがdynoと結びつき、アプリケーションのパフォーマンスに深刻な影響を与えます。 どのページ、どのデータベースが低速なのかを判断するために、[New Relic]を使用して下さい。 (Webトランザクションとデータベースのタブを参照して下さい。) 一番時間を要するリクエストを試験してみて下さい。もし、原因が低速なデータベース、またはAPIのトランザクションにあるのであれば、 情報をキャッシュするために、低水準のキャッシュ(例えば、`Rails.cache.read/write/fetch`)を使用して下さい。

Herokuは、'automagic'のcache-moneyやcache_fuのようなライブラリを使わないことを推奨します。 我々は、容易なキャッシュ解決方法を模索中ですが、満足行くものを見つけ出せていません。 一般的にキャッシュは、アプリケーションに特化した試みです。以下は、Heroku上でRailsアプリを走らせる上で、キャッシュを使い パフォーマンス向上を計るためのロードマップとなります。

一度、memcachedを使うようアプリケーションを設定した場合、 Railsは、アクションキャッシュとフラグメントキャッシュの両方を自動的に使うようになります。

ページキャッシュ

Railsに組み込まれたページキャッシュ機能は、 ファイルシステムへの書き込み権限を必要とします。そのようなものは、Heroku上では動作しません。 しかしながら、我々が提供するVarnish HTTP Cacheは、同等のことを満たしてくれます。 (しかも、驚くほど早く動作します。) HTTPキャッシュのサポートは、全てのHerokuアプリケーションに組み込まれており、memcachedを必要としません。

Varnishのページキャッシュは、before_filterのロジックを必要としないページ(典型的なのは、認証のページ)に適切です。 そして、それは全てのユーザー(すなわち、カスタマイズされたコンテンツを持たない)にとって同様です。 アプリケーションの所有者として、どのようにページを最新式(または旧式)にするかを決める必要があります。 設定方法のインストラクションは、 http cacheのドキュメントを参照して下さい。

アクションキャッシュ

あなたが作成しているページが、認証や、その他before/afterフィルターが必要な場合、 Railsに組み込まれたアクションキャッシュ機能を 使うことで、コンテンツをキャッシュし続けることが可能です。アクションキャッシュは、HerokuのMemcacheアドオンを 使います。(必須となります。)

特定のアクションへのキャッシュを有効にするため、シンプルにコントローラーへcaches_action :<action_name>と追記して下さい。 もし、レイアウトに動的コンテンツ(ヘッダー内のユーザー名やemailアドレスのようなもの)を含む場合、 アクションのコンテンツをキャッシュし続けながら、動的にレイアウトをレンダーすることが可能です。

下記コントローラーのコードが、これらの概念を明確にしてくれます。:

:::ruby
# 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 Guides)

フラグメントキャッシュ

フラグメントキャッシュは、アプリ内のウィジェットやパーシャルをキャッシュするための素晴らしい仕組みです。 フラグメントキャッシュは、HerokuのMemcacheアドオンを使います。(必須となります。) 例えば、アプリがproductsをこのようにリスト化しているとします。:

:::ruby
# 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>

それであれば、フラグメントキャッシュを使うことで、product毎のパーシャルを簡単にキャッシュすることが出来るでしょう。 パーシャルにActiveRecordオブジェクトを渡せば、Railsは、自動的にキャッシュキーを生成します。:

:::ruby
# _product.html.erb
<% cache(product) do %> 
   <div><%= link_to product, product.name %>: <%= product.price%></div>
   <div><%= do_something_comlicated%></div>
<% end %>

別のフラグメントキャッシュ戦略として、新たなページロード毎に直接データストアからリフレッシュする必要の無い ウィジェットやその他ページの一部分をキャッシュするという方法があります。例えば、ウェブサイトのフロントページが、 売上高トップのproductsをリスト表示する場合、このフラグメントをキャッシュすることが出来るでしょう。 1時間毎に、情報をリフレッシュしたいと仮定します。:

:::ruby
# 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を全て返すクラスメソッドを持ったProductモデルと、競争相手のwebサイトから 価格でproductを検索するインスタンスメソッドです。これらのメソッドにより返されるデータには、低水準のキャッシュが 適切でしょう。

:::ruby
# 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属性をベースとしたcache-keyを生成していることに気付いて下さい。 これは一般的な規約となり、productが更新された際、常にキャッシュを無効とするのに役立ちます。 一般的に、インスタンスレベルの情報へ低水準のキャッシュを使用する場合、cache-keyを生成する必要があります。

これ以外の読み物

⚠️ **GitHub.com Fallback** ⚠️