2008-02-26

ETagについて

はてなブックマーク   livedoor clip

今回は ETag(Entity Tag) の話題です。サーバの構成をいろいろと考えていて気になり始めた技術の一つです。

ETag の詳細な説明は省きますが、HTTP/1.1 で定義されているヘッダで、キャッシュコントロールに使われます。

Apache の実装ではデフォルトで ETag はファイルサイズ、最終更新日時、inode 番号の3つから計算されるのですが、これが分散環境ではキャッシュの効かない原因になります。(同じ内容ファイルでも別のサーバ上にある場合には inode 番号が同じになることはほぼあり得ないため、負荷分散の都合で別サーバに割り当てられた場合、ETagも変わってしまう。)サーバの分散をするに当たってどんな問題があるかを調べていてみつけました。これを回避するためには、Apache 2.0 以降では FileETag ディレクティブの設定で ETag に inode 番号を含めず、ファイルサイズと最終更新日時だけから計算されるようにすればいいのですが、inode 番号を設定しない場合に問題になることはあるのでしょうか?

そもそも 最終更新時刻もファイルサイズも変わらずに inode 番号だけが変わることってあるのでしょうか?
inode 番号だけが変わるってことはファイルを移動したとかでしょうか? 普通にmvしても変わらないですよね。cp -a してコピー元を rm すれば変わりますね。でも移動しても内容が変わっていなければクライアント側には同じファイルと認識されてかまわないですよね。特に問題ないような気がしてきました。

もし「こんな問題がある」ってわかる人はぜひ教えてください。

ところで、Google 先生に聞いてみると「正しく LastModified が設定されているなら ETag なんてとってしまえ」という意見も散見されますが、それだと一秒以内の更新のときに対応できないですね。ファイルサイズと更新時刻からなる ETag をつけていても、一秒以内でしかもファイルサイズの変わらない更新(追加や削除のない文字の訂正とか)だと対応できないので、ETagをつければ問題がなくなるというわけではないですが。

これに関しても、 何か意見や情報がある人は教えてください。私はサーバ側ではいろいろなキャッシュコントロールのやり方をサポートして、クライアントに好きなやり方を選ばせるのがいいんじゃないかなと思います。まだサーバ側の負荷で困った経験もないので理想論に過ぎないかもしれませんが。

ここでちょっと話は変わって、Rails での ETag の話です。

上のようなことを気にしながら Rails 2.0.2 で開発中のアプリをこねくり回していて気づいたんですが、Rails では ETag を勝手につけてくれるらしいです。どうやって計算しているのでしょう? 以前も付いてたのでしょうか?

以下応答の抜粋です。

GET /search?q=xxx HTTP/1.1
Host: localhost:3000
If-None-Match: "3f79be44e27fc7dc248258b4f4fca3a9"
HTTP/1.x 304 Not Modified
Etag: "3f79be44e27fc7dc248258b4f4fca3a9"
Server: Mongrel 1.1.3

で、調べてみたところ、答えは response body 全体の MD5 ハッシュでした。
/usr/lib/ruby/gems/1.8/gems/actionpack-2.0.2/lib/action_controller/response.rbの中に以下の部分を見つけました。

def handle_conditional_get!
  if body.is_a?(String) && (headers['Status'] ? headers['Status'][0..2] == '200' : true)  && !body.empty?
    self.headers['ETag'] ||= %("#{Digest::MD5.hexdigest(body)}")
    self.headers['Cache-Control'] = 'private, max-age=0, must-revalidate' if headers['Cache-Control'] == DEFAULT_HEADERS['Cache-Control']
    if request.headers['HTTP_IF_NONE_MATCH'] == headers['ETag']
      self.headers['Status'] = '304 Not Modified'
      self.body = ''
    end
  end
end

ネットワークとCPUのどちらのリソースが先に逼迫してくるかで、とりあえずは「ネットワークが先」という判断になっているんですね。ちなみに actionpack-1.13.6 にはこれはなかったので、Rails 2.0 以降の機能のようです。