cuzic です。
Ruby on Rails Advent Calendar 24日目の記事です。
今日はクリスマスイブです。
実は生まれて初めての Advent Calendar への投稿です。
さらに Qiita で書いたのも初めてです。
ちょっとドキドキです。
今日は、Ruby on Rails における Conditional Get
に
ついて書きます。
ConditionalGet とは
ここで書こうとしている ConditionalGet
とは、
HTTP1.1
で規定されている取得済みのコンテンツであれば
ブラウザ内のローカルキャッシュを使わせることで、
トラフィック量を削減する機能のことです。
一般に、ブラウザは動作を高速化するため、トラフィックを
軽減するため、コンテンツを一旦キャッシュとして保管します。
とはいえ、キャッシュを使う限り、そのキャッシュは今でも
有効なのか、全文を再取得すべきなのかを判断する必要があります。
このキャッシュの有効性の判断はコンピュータ科学の2つの難問の1つです。
There are only two hard things in Computer Science:
cache invalidation and naming things.
-- Phil Karlton
この難問に対する HTTP
の回答が Last-Modified
ヘッダと、
ETag
ヘッダです。
サーバは応答するときにコンテンツの Last-Modified
と
ETag
を付与します。ブラウザはその値を覚えておきます。
同じコンテンツにアクセスするとき、 ブラウザは ETag
の値を
If-None-Match
ヘッダとしてサーバに送信します。
サーバでは前回の ETag
と同一かどうか確認し、
同一であれば 304 Not Modified
という応答を返します。
このとき応答するのはヘッダのみでよいため、ネットワークトラフィックの
節約になります。
当然、この ETag
は、本文のハッシュ値など異なるコンテンツであれば
異なる値になるようにする必要があります。
Rack での ConditionalGet
ConditionalGet が使われているかの確認方法
Ruby on Rails
では ConditionalGet
を下記の Rack
ミドルウェアが
自動的に透過的に実行してくれます。
Rack::ETag
Rack::ConditionalGet
なお、自分の環境で、どのような Rack ミドルウェアが使われいるかは、
次のコマンドで確認できます。
rake middleware
上記のコマンドを実行すると、 Rack::ConditionalGet
と Rack::ETag
は
かなり下の方にあるはずです。
これらの Rack ミドルウェアが下の方に位置しているのには理由があります。
その理由は、後ほど述べます。
Rack::ETag
と Rack::ConditionalGet
の動作
Rack::ETag
は、WEBアプリケーションサーバからの
レスポンスに対して、 Etag
ヘッダを付与する Rack
ミドルウェアです。
Rack::ETag
は、下記の3つの条件がすべて満たされるとき、
response.body
の MD5
の値を ETag
を付与します。
- レスポンスのステータスコードが 200 か 201 であること
-
to_path
メソッドに応答しないこと (※) -
Cache-Control
ヘッダがno-cache
でないこと -
ETag
ヘッダやLast-Modified
ヘッダがすでに含まれていないこと
(※) to_path メソッドに応答するときは、 Apache
や NginX
がレスポンスを処理するので、
ここで ETag
を付与する必要がない。くわしくは、 X-Sendfile
などで Google
すること。
そして、 ETag
がすでに含まれていることを前提として、
Rack::ConditionalGet
が下記の動作を行います。
1. ブラウザのキャッシュ内のコンテンツが fresh
(最新と一致している)かどうかを判断する。
2. fresh であれば次の処理を行う。
応答ステータスを 200 から 304 に変更する
Body 、Content-Type、Content-Length を削除する。
fresh
かどうかの判定は、下記のように行います。
- リクエストに
If-Modified-Since
ヘッダがあれば、それがレスポンスヘッダのLast-Modified
よりも新しいこと。 - リクエストに
If-None-Match
ヘッダがあれば、それがレスポンスヘッダのETag
と一致していること。
まとめると、開発者が、特になにもしていなかったとしても、
ETag を Rack::ETag が付与してくれています。
そして、 Rack::ConditionalGet が適切に 304 の応答を返します。
Rack ミドルウェアの順序
Rack ミドルウェアは rake middleware
で表示される順序に
従って処理が行われます。
リクエストを処理するときは上から順に処理され、
レスポンスを返すときは下から順に処理されます。
つまり、レスポンスを返すとき、 Rack::ETag
がまず処理して、
ETag
ヘッダを付与し、それに従って、 Rack::ConditionalGet
が
適切に 304 not modified
の応答を返します。
Rack::ConditionalGet
で 304 を返す場合、他の Rack ミドルウェア
での処理を行う必要もありません。
早い段階で ConditionalGet
で応答することで処理を効率よく
することができます。
Ruby on Rails での ConditionalGet
プログラマがたとえ特に何も指定しなくても、
Rack::ConditionalGet
によって、適切に 304 Not Modified
を
応答する処理をしてくれます。
これは Rails アプリケーションによるレスポンスの生成処理が
すべて終わってから、Rack ミドルウェアが行うものです。
ネットワークトラフィックの節約にはつながります。
しかし、WEBアプリケーションサーバの CPU 負荷の低減にはなりません。
304 Not Modified を返すかどうかの判断をレスポンスの生成処理を行う
より早い段階で実行できれば、WEBアプリケーションサーバの
CPU 負荷の低減できます。
この目的のために ActionController::ConditionalGet
モジュールの
fresh_when
や stale?
があります。
紙幅が限られているため、 fresh_when
を使う例を簡単にだけ紹介します。
下記の例を見てみましょう。
def show
@article = Article.find(params[:id])
fresh_when(@article)
end
この簡単な例では、 Last-Modified
に @article.updated_at
メソッドの値を
ETag
に @article.cache_key
メソッドの値が使われます。
この簡単なコードによって、クライアントのキャッシュが fresh
かどうかの
判定が行われ、 fresh
であれば View のレンダリングをスキップし、
304 Not Modified
を応答させることができます。
cache_key
メソッドは知らない方もいるかもしれません。
そのレコードを一意に特定するための文字列を生成するメソッドで、
他にもフラグメントキャッシュを使うときなどにもこのメソッドは有用です。
まとめ
今回紹介した ConditionalGet
のテクニックは
Ruby on Rails
の優れた特長を表しています。
第1の特長として、プログラマが特に何も意識することなく、
自動的に透過的にフレームワークが適切に処理してくれる点です。
ネットワーク負荷の低減のための 304 Not Modified
を応答する
ためのコードを一切書く必要がないのです。
第2の特長は、しかもそれをモジュール化された適用範囲の高い
技術によって実現している点です。この場合は、Rack ミドルウェア
という Rails に限らず適用可能な優れたモジュール化技術で実現されています。
Rack ミドルウェアで実現されているため、より高度な部品に交換することも
容易に可能となっています。
第3の特長は、デフォルトの動作に加えてさらに高度な処理をすることも
非常に簡単な点です。たとえば、プログラマがほんの少しのコードを
書き足すだけで、 fresh
であれば View のレンダリング処理を
スキップさせることができます。