«前の日記(2006-12-27 (水)) 最新 次の日記(2007-01-09 (火))»  

まちゅダイアリー


2007-01-07 (日)

ETag とキャッシュの関係(リベンジ→未解決)

昔の日記で試した tDiary で ETag を使う実験の続き。 まずは前回のおさらい。

  • 通常、「Last-Modified」と「If-Modified-Since」が同じ場合は、キャッシュが有効になる (サーバから 304 Not Modified が返る)
  • しかし、「Last-Modified」が同じでもコンテンツの内容が変わる場合がある。
    • tDiary の場合は、サイドバーのプラグインが出力する内容が変わる。
  • 「ETag」と「If-None-Match」を使うことで、上記の場合でもキャッシュの制御ができる(はず)。
    • tDiary の場合は、なぜか「ETag」が同じでも「304 Not Modified」ではなく「200 OK」が返る。
    • 200と304のどっちを返すかは、Webサーバ (Apache) が判断しているっぽい。

先に結論を

日記を書いたら長くなったので、先に結論を書いておく。 結果的にはリベンジ失敗。やっぱり原因は分からなかった。

ETag機能F5リロード時のステータスコード
無効304 Not Modified (キャッシュ有効)
有効200 OK (キャッシュ無効)

tDiary で ETag が無効の場合

まずは、 ETag が無効の場合。 Web ブラウザは Firefox を使い、 Live HTTP Header でリクエストとレスポンスを取得している。

GET /diary/ HTTP/1.1
Host: www.machu.jp
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; ja; rv:1.8.0.7) Gecko/20060909 Firefox/1.5.0.7
Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
Accept-Language: ja,en-us;q=0.7,en;q=0.3
Accept-Encoding: gzip,deflate
Accept-Charset: Shift_JIS,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Connection: keep-alive

HTTP/1.x 200 OK
Date: Sun, 07 Jan 2007 11:23:40 GMT
Server: Apache/1.3.37 (Unix)
Cache-Control: no-cache
Pragma: no-cache
Vary: User-Agent
Last-Modified: Fri, 29 Dec 2006 01:35:19 GMT
Content-Length: 33374
Keep-Alive: timeout=3, max=8
Connection: Keep-Alive
Content-Type: text/html; charset=EUC-JP

「Last-Modified」として「Fri, 29 Dec 2006 01:35:19 GMT」が返ってきている。 Webブラウザの更新ボタンを押すと、Firefoxは「If-Modified-Since」ヘッダにこの時間を付与する。

GET /diary/ HTTP/1.1
Host: www.machu.jp
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; ja; rv:1.8.0.7) Gecko/20060909 Firefox/1.5.0.7
Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
Accept-Language: ja,en-us;q=0.7,en;q=0.3
Accept-Encoding: gzip,deflate
Accept-Charset: Shift_JIS,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Connection: keep-alive
If-Modified-Since: Fri, 29 Dec 2006 01:35:19 GMT
Cache-Control: max-age=0

HTTP/1.x 304 Not Modified
Date: Sun, 07 Jan 2007 11:24:15 GMT
Server: Apache/1.3.37 (Unix)
Connection: Keep-Alive, Keep-Alive
Keep-Alive: timeout=3, max=7
Cache-Control: no-cache
Vary: User-Agent

最終更新日が変わっていないので、ステータスコード200ではなく、304 Not Modifiedが返ってくる。 304の場合は日記の本文 (HTML) は送られないので、ネットワークの帯域が節約できる。

tDiary で ETag を有効にした場合

次は ETag を有効にした場合。

GET /diary/ HTTP/1.1
Host: www.machu.jp
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; ja; rv:1.8.0.7) Gecko/20060909 Firefox/1.5.0.7
Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
Accept-Language: ja,en-us;q=0.7,en;q=0.3
Accept-Encoding: gzip,deflate
Accept-Charset: Shift_JIS,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Connection: keep-alive
Pragma: no-cache
Cache-Control: no-cache

HTTP/1.x 200 OK
Date: Sun, 07 Jan 2007 11:30:28 GMT
Server: Apache/1.3.37 (Unix)
Cache-Control: no-cache
Etag: "23d2c4aac21be1dde8bad3a274ec57e4"
Pragma: no-cache
Vary: User-Agent
Last-Modified: Fri, 29 Dec 2006 01:35:19 GMT
Content-Length: 33374
Keep-Alive: timeout=3, max=8
Connection: Keep-Alive
Content-Type: text/html; charset=EUC-JP

「Last-Modified」だけでなく「ETag」の値も返ってきている。 Webブラウザの更新ボタンを押すと、Firefoxは「If-Modified-Since」ヘッダにこの時間を付与し、「If-None-Match」にETagの値を付与する。

GET /diary/ HTTP/1.1
Host: www.machu.jp
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; ja; rv:1.8.0.7) Gecko/20060909 Firefox/1.5.0.7
Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
Accept-Language: ja,en-us;q=0.7,en;q=0.3
Accept-Encoding: gzip,deflate
Accept-Charset: Shift_JIS,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Connection: keep-alive
If-Modified-Since: Fri, 29 Dec 2006 01:35:19 GMT
If-None-Match: "23d2c4aac21be1dde8bad3a274ec57e4"

HTTP/1.x 200 OK
Date: Sun, 07 Jan 2007 11:31:12 GMT
Server: Apache/1.3.37 (Unix)
Cache-Control: no-cache
Etag: "23d2c4aac21be1dde8bad3a274ec57e4"
Pragma: no-cache
Vary: User-Agent
Last-Modified: Fri, 29 Dec 2006 01:35:19 GMT
Content-Length: 33374
Keep-Alive: timeout=3, max=8
Connection: Keep-Alive
Content-Type: text/html; charset=EUC-JP

ETagの値もLast-Modifiedの値も変わっていないので、本来なら200ではなく304が返ってくるはず。 でも何故か、ETagヘッダを追加した場合は、200が返ってきている。

ちなみに、 Apache 1.3 ではなく Apache 2.0 で実験しても同じ結果だった。

静的 HTML の場合

そもそもETagの概念を勘違いしているかもと思い、単純なパターンとして普通の HTML を取得してみる。 静的な HTML の場合は、ETag の値は Web サーバ (Apache) が計算してくれる。

GET /index.html HTTP/1.1
Host: www.machu.jp
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; ja; rv:1.8.0.7) Gecko/20060909 Firefox/1.5.0.7
Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
Accept-Language: ja,en-us;q=0.7,en;q=0.3
Accept-Encoding: gzip,deflate
Accept-Charset: Shift_JIS,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Connection: keep-alive

HTTP/1.x 200 OK
Date: Sun, 07 Jan 2007 14:30:35 GMT
Server: Apache/1.3.37 (Unix)
Last-Modified: Sat, 28 Oct 2006 02:51:42 GMT
Etag: "52dcc9-971-4542c5be"
Accept-Ranges: bytes
Content-Length: 2417
Keep-Alive: timeout=3, max=8
Connection: Keep-Alive
Content-Type: text/html

ETagの値が返ってきたので、リロードボタンを押す。 「If-None-Match」に ETag の値が入る。

GET /index.html HTTP/1.1
Host: www.machu.jp
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; ja; rv:1.8.0.7) Gecko/20060909 Firefox/1.5.0.7
Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
Accept-Language: ja,en-us;q=0.7,en;q=0.3
Accept-Encoding: gzip,deflate
Accept-Charset: Shift_JIS,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Connection: keep-alive
If-Modified-Since: Sat, 28 Oct 2006 02:51:42 GMT
If-None-Match: "52dcc9-971-4542c5be"
Cache-Control: max-age=0

HTTP/1.x 304 Not Modified
Date: Sun, 07 Jan 2007 14:31:06 GMT
Server: Apache/1.3.37 (Unix)
Connection: Keep-Alive, Keep-Alive
Keep-Alive: timeout=3, max=7
Etag: "52dcc9-971-4542c5be"

ETag の内容が変わっていないので、 200 ではなく 304 が返ってきている。 これは期待通りの動作になってる。 なぜ CGI の場合だけ上手くいかないんだろう…。

Apache のソースを見てみる

200 を返すか 304 を返すかは Web サーバ (Apache) で判断しているっぽいので、 Apache のソースを読んでみる。 1.3 より読みやすそうな Apache 2.0 のソースとしばし格闘。それっぽいコードを見つけた。 ファイルは httpd-2.0.59/modules/http/http_protocol.c の328行目付近。

※ 1.3系の場合は apache_1.3.37/src/main/http_protocol.c の579行目付近に同様のコードがある。

    if_nonematch = apr_table_get(r->headers_in, "If-None-Match");
    if (if_nonematch != NULL) {
        if (r->method_number == M_GET) {
            if (if_nonematch[0] == '*') {
                return HTTP_NOT_MODIFIED;
            }
            if (etag != NULL) {
                if (apr_table_get(r->headers_in, "Range")) {
                    if (etag[0] != 'W'
                        && ap_find_list_item(r->pool, if_nonematch, etag)) {
                        return HTTP_NOT_MODIFIED;
                    }
                }
                else if (ap_strstr_c(if_nonematch, etag)) {
                    return HTTP_NOT_MODIFIED;
                }
            }
        }
        else if (if_nonematch[0] == '*'
                 || (etag != NULL
                     && ap_find_list_item(r->pool, if_nonematch, etag))) {
            return HTTP_PRECONDITION_FAILED;
        }
    }
    else if ((r->method_number == M_GET)
             && ((if_modified_since =
                  apr_table_get(r->headers_in,
                                "If-Modified-Since")) != NULL)) {
    <以下、「If-Modified-Since」と「Last-Modified」の比較のコード>

リクエストヘッダに「If-None-Match」が含まれていて、 GET メソッドの場合に if の中が実行される。 「If-None-Match」の1文字目が「*」以外で、ETagの1文字目が「W」以外の時に注目。

                else if (ap_strstr_c(if_nonematch, etag)) {
                    return HTTP_NOT_MODIFIED;
                }

ここで、「If-None-Match」と「ETag」の内容を比較して、一致していれば HTTP_NOT_MODIFIED (304) を返すようにしている。 このコードを見る限りは、単純に ETag の値が同じなら動きそうな気がするのに、なぜだ…。

「If-None-Match: *」で実験

もしかしたらさっきの if 文まで到達していないかも…と思った。 そこで、その少し前のコードが実行されるかどうかを実験した。

            if (if_nonematch[0] == '*') {
                return HTTP_NOT_MODIFIED;
            }

リクエストヘッダに「If-None-Match: *」が含まれていたら、強制的に HTTP_NOT_MODIFIED が返るようになっている。 そこで、コマンドラインからこのリクエストを投げてみた。

$ telnet machu.jp 80
Trying 202.181.97.46...
Connected to machu.jp.
Escape character is '^]'.
GET /diary/ HTTP/1.1
Host: www.machu.jp
If-None-Match: *

HTTP/1.1 304 Not Modified
Date: Sun, 07 Jan 2007 14:49:01 GMT
Server: Apache/1.3.37 (Unix)
Cache-Control: no-cache
Vary: User-Agent

今度は見事に 304 が返ってきた。 ということは、この箇所までは実行されているはず。 うーん。なんでその先が上手くいかないんだろう。

tDiary のソースを修正して、「Pragma: no-cache」や「Cache-Control: no-cache」を出力しないようにしてみても、結果は同じだった。 先は長い。

Tags: tDiary plugin

ETag とステータスコード 200 or 304

ETag を入れると 304 が返らなくなるのは、 tDiary というより Web サーバ (Apache) の問題っぽい。 なので、簡単な CGI スクリプトでもう一回実験してみた。

#!/usr/local/bin/ruby
require 'cgi'

cgi = CGI.new
head = {}
head['Last-Modified'] = 'Fri, 29 Dec 2006 01:35:19 GMT'
head['ETag'] = %Q("1234567890")
print cgi.header(head)
puts 'hello!'

今度もコマンドライン (telnet) からアクセスする。

$ telnet machu.jp 80
Trying 202.181.97.46...
Connected to machu.jp.
Escape character is '^]'.
GET /etag.cgi HTTP/1.0
Host: www.machu.jp
If-None-Match: "1234567890"

HTTP/1.1 200 OK
Date: Sun, 07 Jan 2007 16:09:32 GMT
Server: Apache/1.3.37 (Unix)
ETag: "1234567890"
Connection: close
Content-Type: text/html

hello!

やっぱり CGI の時だけうまく動いていないのか…。

追記 (2007-01-10)

「ETag を入れると 304 が返らなくなるのは、 tDiary というより Web サーバ (Apache) の問題っぽい」という日記の内容に対して、どさにっきからツッコミを頂戴した。

問題なんてものは存在しておらず、あるとすれば Apache がやりもしないことをやってくれるという思い違いが存在していただけ。

あぁ、確かに「問題」という言い方は良くなかった。 CGI ではどっちがやると規定していないので、必要があるのなら CGI スクリプト側でちゃんと制御すべきと。 (たださんも、「CGIスクリプトが自分で判断して304を返せばいいだけ」と書いている)

Apache の挙動については中途半端なままだけど、少しは勉強になりました。

Tags: tDiary