HTTP のキャッシュ(その1)

時々、HTTPのキャッシュのメカニズムに関して悩んだり忘れたりすることがあったので整理してみました。

Expiration モデルとバリデーションモデル

HTTP のキャッシュ機構は、 Expiration モデル (主に Cache-Control や Expires を使用)と、バリデーションモデル(Last-Modified や ETag を使用)で成り立っています。


前者の Expiration モデルは、食べ物の消費期限に似ています。

  • 提供者が有効期限を指定する
  • 期限の解釈と判断は使用者がする


一方のバリデーションモデルは、スタバのお代わりに似てます。

  • 受け取った情報(レシート)を見せて、同じものを提供者に要求する
  • 提供者がOKと判断すれば低コストで提供する


バリデーションモデルはレスポンスの通信量とサーバーの負荷を軽くする事ができます。一方の Expiration モデルは、さらにリクエストそのものも省略できます。ただし、一度嘘を言ってしまうと取りやめが出来ないので扱いが難しいモデルでもあります。

個人的にはバリデーションモデルから始めて、力不足なら Expiration モデルも取り入れる、という事が多いかもです。

バリデーションモデル

今回はバリデーションモデルをスターバックスのお代わりモデルから整理します。


スターバックスのお代わりシステムは以下の流れになります。

  1. 客はコーヒーを購入時にレシートをもらい、取っておく
  2. 客はレシートを見せてお代わりを注文する
  3. 店員はレシートをチェックする
    • 購入日当日であれば同じものを100円で提供する
  4. 安い!


これをなぞってバリデーションモデルの流れを説明すると、以下のようになります。

  1. クライアントは最初のレスポンスのヘッダ(Last-ModifiedやETag)を中身のデータとともにキャッシュする
  2. クライアントはヘッダ(If-Modified-SinceやIf-None-Match)をつけてリクエストを送る
  3. サーバーはクライアントのヘッダを見る
    • 今回返すレスポンスの中身がキャッシュと同じと判断すれば、再度同じデータを返す代わりにステータス304(Not Modified)を返す
  4. (通信やサーバー負荷的に)安い!

肝心なのは、キャッシュが有効かどうかをサーバー側が毎回確認(バリデーション)する点です。これによって、有効期限等が分からない場合にも効率的にキャッシュが出来ますが、リクエストそのものを省略することはできません。

Last-Modified Date と Entity Tag

バリデーションを行う際に使用する情報は Last-Modified Date (最終更新日)と Entity Tag です。この二つの情報から、サーバーはデータを再送信するかどうかを判断します。

最終更新日によるバリデーションは想像しやすいです。レスポンスとキャッシュの最終更新日が一致すれば、同じデータを返すと判断出来ます。

実際に最終更新日を使用したバリデーションを行う簡単なCGI(懐かしい…)を作成して試してみました。


最初のリクエストでは、200のステータスとともに通常のデータが返ります。Last-Modified ヘッダが付いている事にご注意を。


再度リクエストを送る時には、 If-Modified-Since が付加されています。それを受け取ったCGIは、 304 のステータスを返します。

プログラムは以下の通りです。

#!/usr/local/bin/ruby -Ku
# -*- encoding: UTF-8 -*-
require 'time'

last_modified = Time.local(2011, 1, 9, 17, 30, 0)

# ヘッダからコンテンツが最新かどうかを判別
if_modified_since_header = ENV['HTTP_IF_MODIFIED_SINCE']
if if_modified_since_header && Time.httpdate(if_modified_since_header) == last_modified

  print \
    "Status: 304 Not Modified\n" +
    "Cache-Control: max-age=0, public, no-cache\n" +
    "\n"

else

  sleep(1)

  print \
    "Content-Type: text/plain; charset=UTF-8\n" +
    "Cache-Control: max-age=0, public, no-cache\n" +
    "Last-Modified: #{last_modified.httpdate}\n" +
    "\n" +
    "hello\n"
end


HTTPでは、最終更新日の他に Entity Tag によるバリデーションが用意されています。仕組みとしては最終更新日と基本同じですが、タグの計算方法はサーバー側に任せられています。例えばデータのハッシュ値を返しても良いですし、変更が全く無いファイルであれば、そのファイルのパスを返しても良いかもしれません。


感想など

最終更新日とEntity Tagが機能が重複しているので、ものぐさな自分としては「両方書くのは冗長だな〜」などと思ってしまいました。バリデーションモデルでは、誤って304を返しさえしなければクライアントは正しい結果が得られるはずなので、極端な話 ETag や Last-Modified は適当な値で構わないように思えます(って言ってしまうと誰かに怒られそうですが…)。

HTTP/1.1の仕様(RFC 2616) にはガイドラインも書かれてあります。それによると、サーバー側は「コストに見合えば ETag と Last-Modified の両方を提供する」事が推奨されていました。 Last-Modified が書かれていれば若干デバッグに役立つかもしれませんし、あれば書けば良いかな…。