Skip to content

Http caching ruby rails

shunter1112 edited this page Aug 3, 2013 · 1 revision

素晴らしい事に、Rails3は静的なページやアセット向けのシンプルなHTTPキャッシュを提供しています。あなたのアプリケーションはこの機能によって恩恵を受けることができますが、適切なキャッシュヘッダを規定しようとすると、たとえ動的なものであっても、あなたはアプリケーションを動かすためにリソースやユーザ体験、レスポンスタイムの大きな改善を求められるかもしれません。

この記事は、HTTPキャッシュヘッダを利用する事で、最小の変更でレスポンスタイムの改善ができるになるようないくつかの利用例を説明しています。

この記事が参照するためのアプリケーションのソースは[GitHubで利用可能です](https://github.com/vzmind/demo-http-caching/)。また実行中のものは[http://http-caching-rails.herokuapp.com](http://http-caching-rails.herokuapp.com)で確認する事が出来ます。

Rails3のデフォルトのHTTPキャッシュ

Rails3の初期設定では、最も基本的なシナリオを前提としてHTTPキャッシュヘッダが使用されています。これは、より効率的な静的アセットの配信のためのアセットパイプラインや、自然なキャッシュメカニズムを提供するミドルウェアであるRack::CacheRack::ETagなどと共に構成されています。

アセットパイプライン

Rails 3.1系からthe Asset Pipelineの概念が登場しました。JSやCSSのアセットが圧縮され連結されたのに合わせて、Railsはリクエストを通して同一のアセットを再取得しないように、HTTPキャッシュヘッダを追加しました。アセットのためのリクエストは複数のヘッダとともにやってきます。それらはローカルでどのように保存をするべきかを定義しています :

Asset pipeline response

  • Ageヘッダは キャッシュから推察されるリソースの年齢を送信しています。
  • Cache-Control はこのアセットがpublic(中間プロキシで保存できる)であること、そして31,536,000秒(365日)というmax-ageの値を持っていることを示しています。
  • Etag はRackのミドルウェアから計算され、レスポンスボディのダイジェストに基づいています。
  • Last-Modified はファイルが持っている情報に基づき、最新の変更日時を表示しています。

ほとんどのアプリケーションにとってこれらの初期値は十分な物であり、アセットパイプラインに対して何かを変更する必要はありません。

Rack::Cache

Rack::Cacheのようなリバースプロキシのキャッシュは、Webクライアント(ブラウザ)とあなたのアプリケーションの間に存在し、公にキャッシュしてもいいリソースを誰にも見える形でキャッシュします。

Rails3ではRack::Cacheをネイティブのプロキシキャッシュとして搭載しました。プロダクションモードでは、全てのページのpublicキャッシュヘッダが、仲介者プロキシとして動いている Rack::Cache に保存されます。結果として、キャッシュされたリソースのおかげでこれらのリクエストはRailsのスタックを迂回します。

デフォルトとして、Rack::Cacheはメモリ内の容量を使います。Herokuのような高度に分散された環境では、共有キャッシュ資源を利用するべきです。Herokuでは高パフォーマンスなHTTPヘッダベースのリソースキャッシュのために、[Rack::Cache を Memcachedアドオンと一緒に使いましょう](rack-cache-memcached-rails31)

Rack::ETag

`Cache-Control: private`ヘッダの副作用はこれらのリソースがリバースプロキシキャッシュ(Rack::Cacheでさえも)で保存されなくなることです。

Rack::ETagは自動でETagヘッダCache-Control: privateを全てのレスポンスにつける事で、条件付きのリクエストをサポートします。

Dynamic page headers

あなたのアプリケーションの詳細を知らなくても、ビューが描画されたあとにレスポンスの完全な文字列をハッシュ化することで、これが可能になります。

このアプローチがアプリケーションにとって可視なものである間は、アプリケーションにレスポンスボディをハッシュ化するために、リクエストを完全に処理すること求められています。節約できることといえば、304 Not Modifiedのステータスコードと空のレスポンスが代わりに送られてから、クライアントのネットワークを通して戻される完全なレスポンスの送信コストです。

キャッシュヘッダを最大限のパフォーマンスがでるように設定する事は、未だにアプリケーションの開発者の責任なのです。

時間ベースのキャッシュヘッダ

Railsは、リソースの時間ベースのキャッシュをするためにExpires HTTP headerを経由した2つのコントローラーメソッドが用意されています。expires_inexpires_nowです。

expires_in

Cache-Controlヘッダのmax-ageという値は、expires_inコントローラメソッドで設定されます。(サンプルアプリshowアクションの中で使われています。

:::ruby
def show
  @company = Company.find(params[:id])
  expires_in 3.minutes, :public => true
  # ...
end

Companyというリソースの為にリクエストが作られた時は、Cache-Controlはこのように適切に設定されているでしょう :

Expires_in logs

max-ageの値はリソースがクライアントから決められた間隔にリクエストされることを防ぎます。これはすこし粗めなキャッシュのアプローチを提供しますが、まれにしか変更しないコンテンツには役立ちますし、こういう場合はすぐに伝搬することが要求されていません。

Rack::Cacheのリクエストと併せてこれらを使用する場合、リソースは決められた期間内にコントローラを一度しか叩きません。

Started GET "/companies/2" for 127.0.0.1 at 2012-09-26 14:07:28 +0100
Processing by CompaniesController#show as HTML
  Parameters: {"id"=>"2"}
  Rendered companies/show.html.erb within layouts/application (9.0ms)
Completed 200 OK in 141ms (Views: 63.8ms | ActiveRecord: 14.4ms)

Started GET "/companies/2" for 127.0.0.1 at 2012-09-26 14:11:10 +0100
Processing by CompaniesController#show as HTML
  Parameters: {"id"=>"2"}
Completed 304 Not Modified in 2ms (ActiveRecord: 0.3ms)

2回目のリクエストがすぐに304 Not Modifiedを返すのに対して、最初のリクエストがビューのレンダリングのために完全な実行をすることに気をつけてください。

expires_now

あなたはリソースの強制的な破棄を、expires_nowコントローラメソッドを使ってすることができます。これは、Cache-Controlヘッダにno-cacheを設定し、ブラウザやあらゆる中間層のキャッシュ機構によってキャッシュされる事を防ぎます。

:::ruby
def show
  @person = Person.find(params[:id])

  # Set Cache-Control header no-cache for this one person
  # (just as an example)
  expires_now if params[:id] == '1'
end

Cache-Controlヘッダは削除され、強制的にリソースの破棄をしています。

expires_now_logs

`expires_now`はコントローラのアクションによって叩かれるリクエストでのみ実行されます。ヘッダが前に`expires_in`経由で設定したリソースは、有効期間が過ぎるまで更新済みのリソースがすぐに求められる事はないでしょう。開発、デバッグ時にはこれを心に留めておいてください。

Conditional cache headers

条件付きキャッシュヘッダ

条件付きGETリクエストはブラウザにリクエストを始める事を求めるだけでなく、サーバがキャッシュされたレスポンスや処理を迂回して、完全に共有データ(ETagハッシュやLast-Modifiedタイムスタンプ)を基本にした返答をすることを許可します。

Railsでは、適切な条件の振る舞いを明示するために、stale?fresh_whenメソッドを使います。

stale?

staleコントローラメソッドは適切なEtagLast-Modified-Sinceヘッダを設定し、現在のリクエストがstale (腐りかけた; 完全な処理をする必要がある) か、それともfresh (新鮮な; ウェブクライアントがキャッシュされたコンテンツをつかえる) かを決めます。

publicなリクエストには、リバースプロキシのキャッシュを追加するために、`:public => true`を明示します。

:::ruby
def show
  @company = Company.find(params[:id])
  # ...
  if stale?(etag: @company, last_modified: @company.updated_at)
    respond_to do |format|
      format.html # show.html.erb
      format.json { render json: @company }
    end
  end
end

`stale?`の中にあるネストされた`respond_to`のブロックは[ビューのレンダリングを確かな物にしており](https://github.com/vzmind/demo-http-caching/blob/master/app/controllers/companies_controller.rb#L13)、しばしばここは様々なリクエストの最も高負荷な場所になり得るので、必要な時だけ実行します。

ActiveRecornのオブジェクトと共にstale?を叩き、このupdated_atのタイムスタンプを最終変更日として使用するパターンは一般的です。Railsはこれを、オブジェクト自身の唯一の属性として許可しています。この例ではstale?(@company)として実装されます :

:::ruby
if stale?(@company)
  respond_to do |format|
    # ...
  end
end

この設定では、Companies#showに対する最初のリクエストは完全なリクエストスタックを実行します。(何のパフォーマンスも改善されません)

header-stale

しかしながら、続いてくるリクエストはビューのレンダリングを飛ばして、リクエストのほとんどの高負荷な部分を回避しながら 304 Not modified を返します。

header-not-stale

304のステータスコードはブラウザのロードの全体を早くするだけでなく、一度レスポンスの裏にあるコアなオブジェクトがキャッシュされたことを知れば、完全なリクエスト処理が迂回されるように、サーバ側をより効率化させます。

fresh_when

stale?メソッドが真偽値を返している一方、リクエストの新鮮さに依存した事なるパスを実行することを許してくれるfresh_whenETagLast-Modified-Sinceヘッダと、もしリクエストが新鮮であったら、304 Not Modifiedステータスコードもレスポンスにセットします。カスタマイズが必要ないコントローラのアクション、つまりはデフォルトの実装ならば、fresh_whenは使われるべきです。

:::ruby
def index
  @people = Person.scoped
  fresh_when last_modified: @people.maximum(:updated_at), public: true
end

簡単なリソースの読み込み

ここで説明してきたHTTPヘッダキャッシュのやり方は、リクエスト制御の一部であるビューのレンダリングを避ける事をしてきました。このように、ビューの処理をなるべく引き延ばす事は有益なことです。普通の実行処理では、Person.allに対するコントローラアクション呼び出しはすべてのPersonレコードをデータベースから取得しようとします。(モデルの構造によっては、すべての子要素も同様に呼び出されます。)

Started GET "/people" for 127.0.0.1 at 2012-09-26 15:08:15 +0100
Processing by PeopleController#index as HTML
  Person Load (0.2ms)  SELECT "people".* FROM "people" 
  Company Load (0.4ms)  SELECT "companies".* FROM "companies" WHERE "companies"."id" = 1 LIMIT 1
  Company Load (0.4ms)  SELECT "companies".* FROM "companies" WHERE "companies"."id" = 2 LIMIT 1
  Rendered people/index.html.erb within layouts/application (2023.8ms)
Completed 200 OK in 2030ms (Views: 2023.7ms | ActiveRecord: 5.2ms)

しかしながら、ActiveRelationスコープをコントローラ内で使用で、ビューが要求してくるまでにデータベースからオブジェクトの読み込みを遅らせることができます。

:::ruby
def index
  @people = Person.scoped
  fresh_when last_modified: @people.maximum(:updated_at)
end

ビューの処理がHTTPキャッシュによって避けられた場合、より少ないデータベースの呼び出しが求められ、更なる重要なパフォーマンスをもたらします。

Started GET "/people" for 127.0.0.1 at 2012-09-26 15:09:43 +0100
Processing by PeopleController#index as HTML
 (0.4ms)  SELECT MAX("people"."updated_at") AS max_id FROM "people" 
Completed 304 Not Modified in 1ms (ActiveRecord: 0.4ms)

もしコントローラアクションがまだ名前空間や[ActiveRecordのクエリメソッド](http://api.rubyonrails.org/classes/ActiveRecord/QueryMethods.html)の一つを使用していない場合、allメソッドのために同等のスコープを作る[無名スコープメソッドの`scoped`](http://api.rubyonrails.org/classes/ActiveRecord/Scoping/Named/ClassMethods.html#method-i-scoped)を使います。

パブリックリクエスト

パブリックなレスポンスは重要な情報を含んでおらず、すぐに中間プロキシのキャッシュに保存されます。publicなリソースを指定するために、public: trueをキャッシュしているメソッドのなかで使用します。

:::ruby
def show
  @company = Company.find(params[:id])
  expires_in(3.minutes, public: true)
  if stale?(@company, public: true)
    # …
  end
end

プライベートコンテンツ

デフォルトでは、Cache-Controlは全てのリクエストに対してプライベートとして設定されます。ですが、いくつかのキャッシュの設定は、明示的にプライベートなリソースだと示すためのより望ましい振る舞いへと、デフォルトを上書きする事が出来ます。

:::ruby
expires_in(1000.seconds, public: false)

キャッシュしないコンテンツ

コンテンツをキャッシュされないようにする一般的なやり方はbefore_filterを使う事です。この事をコントローラの継承ツリー内で定義してもいいですし、コントローラごとに明示的なプライベート設定を定義しても構いません :

:::ruby
before_filter :set_as_private

def set_as_private
  expires_now
end

Expires now

デフォルトでは、Railsは静的アセットのための基本的なHTTPキャッシュを提供しています。しかしながら、真に最適化されたHTTPキャッシュヘッダの知識というのは、にRailsが持つたくさんのリクエストキャッシュの機能の中から、あなたのアプリケーションへいくつか使う事で、定義されていくべきです。

Clone this wiki locally