-
Notifications
You must be signed in to change notification settings - Fork 9
Http caching ruby rails
素晴らしい事に、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キャッシュヘッダが使用されています。これは、より効率的な静的アセットの配信のためのアセットパイプラインや、自然なキャッシュメカニズムを提供するミドルウェアであるRack::CacheやRack::ETagなどと共に構成されています。
Rails 3.1系からthe Asset Pipelineの概念が登場しました。JSやCSSのアセットが圧縮され連結されたのに合わせて、Railsはリクエストを通して同一のアセットを再取得しないように、HTTPキャッシュヘッダを追加しました。アセットのためのリクエストは複数のヘッダとともにやってきます。それらはローカルでどのように保存をするべきかを定義しています :
-
Age
ヘッダは キャッシュから推察されるリソースの年齢を送信しています。 -
Cache-Control
はこのアセットがpublic
(中間プロキシで保存できる)であること、そして31,536,000秒(365日)というmax-age
の値を持っていることを示しています。 -
Etag
はRackのミドルウェアから計算され、レスポンスボディのダイジェストに基づいています。 -
Last-Modified
はファイルが持っている情報に基づき、最新の変更日時を表示しています。
ほとんどのアプリケーションにとってこれらの初期値は十分な物であり、アセットパイプラインに対して何かを変更する必要はありません。
Rails3ではRack::Cacheをネイティブのプロキシキャッシュとして搭載しました。プロダクションモードでは、全てのページのpublic
キャッシュヘッダが、仲介者プロキシとして動いている Rack::Cache に保存されます。結果として、キャッシュされたリソースのおかげでこれらのリクエストはRailsのスタックを迂回します。
デフォルトとして、Rack::Cacheはメモリ内の容量を使います。Herokuのような高度に分散された環境では、共有キャッシュ資源を利用するべきです。Herokuでは高パフォーマンスなHTTPヘッダベースのリソースキャッシュのために、[Rack::Cache を Memcachedアドオンと一緒に使いましょう](rack-cache-memcached-rails31)
Rack::ETagは自動でETag
ヘッダとCache-Control: private
を全てのレスポンスにつける事で、条件付きのリクエストをサポートします。
あなたのアプリケーションの詳細を知らなくても、ビューが描画されたあとにレスポンスの完全な文字列をハッシュ化することで、これが可能になります。
このアプローチがアプリケーションにとって可視なものである間は、アプリケーションにレスポンスボディをハッシュ化するために、リクエストを完全に処理すること求められています。節約できることといえば、304 Not Modified
のステータスコードと空のレスポンスが代わりに送られてから、クライアントのネットワークを通して戻される完全なレスポンスの送信コストです。
キャッシュヘッダを最大限のパフォーマンスがでるように設定する事は、未だにアプリケーションの開発者の責任なのです。
Railsは、リソースの時間ベースのキャッシュをするためにExpires
HTTP headerを経由した2つのコントローラーメソッドが用意されています。expires_in
とexpires_now
です。
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
はこのように適切に設定されているでしょう :
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
コントローラメソッドを使ってすることができます。これは、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`はコントローラのアクションによって叩かれるリクエストでのみ実行されます。ヘッダが前に`expires_in`経由で設定したリソースは、有効期間が過ぎるまで更新済みのリソースがすぐに求められる事はないでしょう。開発、デバッグ時にはこれを心に留めておいてください。
条件付きキャッシュヘッダ
条件付きGET
リクエストはブラウザにリクエストを始める事を求めるだけでなく、サーバがキャッシュされたレスポンスや処理を迂回して、完全に共有データ(ETag
ハッシュやLast-Modified
タイムスタンプ)を基本にした返答をすることを許可します。
Railsでは、適切な条件の振る舞いを明示するために、stale?
とfresh_when
メソッドを使います。
stale
コントローラメソッドは適切なEtag
とLast-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
に対する最初のリクエストは完全なリクエストスタックを実行します。(何のパフォーマンスも改善されません)
しかしながら、続いてくるリクエストはビューのレンダリングを飛ばして、リクエストのほとんどの高負荷な部分を回避しながら 304 Not modified
を返します。
304
のステータスコードはブラウザのロードの全体を早くするだけでなく、一度レスポンスの裏にあるコアなオブジェクトがキャッシュされたことを知れば、完全なリクエスト処理が迂回されるように、サーバ側をより効率化させます。
stale?
メソッドが真偽値を返している一方、リクエストの新鮮さに依存した事なるパスを実行することを許してくれるfresh_when
はETag
とLast-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
デフォルトでは、Railsは静的アセットのための基本的なHTTPキャッシュを提供しています。しかしながら、真に最適化されたHTTPキャッシュヘッダの知識というのは、にRailsが持つたくさんのリクエストキャッシュの機能の中から、あなたのアプリケーションへいくつか使う事で、定義されていくべきです。