at posts/single.html

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」を出力しないようにしてみても、結果は同じだった。 先は長い。

関連する日記