November 27, 2008
堀愚霊瑠の指摘で気付いた、はてなスターの静的ファイルとか想像以上にアレな件
id:HolyGrail (堀愚霊瑠氏) の「はてなブックマークが重い件について、Page Detailerというツールを使って調べてみる - id:HolyGrailとid:HoryGrailの区別がつかない日記」とか見てて、色々問題点が指摘されてて、うん、まぁそうだねーとか色々と思いつつ、YSlow は、有用なツールである反面、減点基準が必ずしも全てのサイトに適合しないというか、ハッキリ言ってしまえば Yahoo! Inc. 基準すぎるので、鵜呑みにし過ぎるのもどうかなーとか思ってた。
で、気になったのは
ETag 自体は「正しく利用していれば」有用だし、ヘッダサイズ云々って言うほどデカくもないし、アレだなーとか思いつつ、なんで「正しく利用できていないのであれば」という前提のものに引っかかったのかなというのが非常に気になった。
ちなみに、ETag ヘッダは、デフォルトではそのファイルの inode 番号、ファイルサイズ、更新時刻 (epoch 秒) の値を 16 進数表記して、ハイフンで繋いだ文字列をダブルクォーテーションで括ったもの。
Perl で同じような文字列を出力するなら、
-- 以下、HTTP とかにさほど詳しくない人向けの説明。
これが、HTTP レスポンスに含まれていたら、次に HTTP リクエストする際に、ブラウザ側でキャッシュした時の ETag の値を If-None-Match ヘッダに渡します。サーバは、今現在サーバ上にあるファイルの ETag の値が一致するかを確認して、一致した場合は 304 (Not Modified) の HTTP ステータスを返し、ブラウザは「以前キャッシュした時から更新されていない」と解釈して、キャッシュされているファイルを再利用します。
ETag の値を返さない場合、Last-Modified ヘッダが HTTP レスポンスに含まれていたら、次に HTTP リクエストする際に、ブラウザ側でキャッシュした時の Last-Modified の値を If-Modified-Since ヘッダに渡します。サーバは、今現在サーバ上にあるファイルの Last-Modified の値がそれ以前であるかを確認して、それ以前だった場合は 304 (Not Modified) の HTTP ステータスを返し、ブラウザは「以前キャッシュした時から更新されていない」と解釈して、キャッシュされているファイルを再利用します。
-- 以上、HTTP とかにさほど詳しくない人向けの説明。
で、思ったのは「はてなスター (だけ?) の静的ファイルが ETag を正しく使えてないから引っ掛かったのかな」と。
よくよく考えたらはてなスターはこのブログにも貼ってあるし、はてなの色々なコンテンツに大量に利用されている事実を考えたら、もし ETag を正しく使えてないのなら、無駄なトラフィックを大量に生み出しているわけで、決して侮れない。
ナニゲに HatenaStar.js はデカいし。
考えられるありがちな落し穴を考えると、「基本的には同じファイル」なんだけど、配置されているサーバが複数台数に分散されているような場合、ほぼ間違いなくそれぞれのサーバで inode 番号が変わるので、ETag に含まれる値の inode の部分 (先頭部分) はリクエストを処理するサーバによって異なってくる。
そういう場合は、ETag を吐かないようにさえすれば、Last-Modified と If-Modified-Since でいいあんばいにキャッシュ比較とかをしてくれる。
Apache の場合は FileETag ディレクティブを利用して、以下のように設定すれば ETag を吐かなくなる。
あるいは、ファイルサイズ、更新時刻 (epoch 秒) は一緒なのであれば、ETag を吐く時に不要になるのは inode 番号だけ。では、ETag の値から inode 番号を抑制すれば済む問題なので、同様に FileETag ディレクティブを利用して、inode 番号だけ利用しないように、以下のように設定するのがセオリー。
じゃあ、はてなスターは、このありがちな落し穴をおかしているのではないだろうか?とアタリをつけて、HTTP HEAD リクエストを送出してみて確認してみた。
案の定、ETag は三つの値がハイフンで繋がれています。
恐らく先頭の b88995 は inode 番号なので、複数台のサーバで運用されている場合は、邪魔になる恐れがあります。
続いて、それを検証するために、この ETag の値と、Last-Modified の値を、それぞれ If-None-Match と If-Modified-Since に含んでみたら、ちゃんと 304 (Not Modified) ステータスを返してくれるか検証してみました。
ETag を見るとやはり先頭が f9665 で先程とは違うので、inode 番号が違います。
でも Content-Length も一緒だし、MD5 の checksum を見ても同じなので、同じ JS ファイルであることは間違いありません。
でも「あなたのとこにキャッシュされているファイルとは、比較するかぎりどうも別のファイルだから、このデッカイ JS ファイルをあらためて読み込んでキャッシュしなおしてよ」とブラウザに訴えてきているのです。
堀愚霊瑠氏の指摘するとおり、正しく使えてない、無駄な ETag を吐いてます。
何度か繰り返してみると、ETag によって返される inode 番号は 2 種類であることから、「はてなスターは 2 台のサーバから静的なファイルを返している」ということがわかります。
結論: はてなスターは ETag を吐かないようにするか、inode 番号を利用しないように正して、きちんとブラウザのキャッシュを有効活用させるようにして欲しいです!
って、気持ちよくしめようとしたら、現実はもっとひどいことに気付いた。
目を凝らしてよく見れば ETag だけの問題ではない。
Last-Modified が 2 台それぞれのサーバによって違う。
5 分も違う。
当然、ETag も inode 番号の部分だけじゃなく、更新時刻の部分も違う。
ETag を抑止しても、Last-Modified がバラバラだから、古いほうを先に GET してしまうと結局新しいほうを GET しに行った時に 304 ステータスは返らない。
はてなスターは、あれだけのパーツ画像とデッカイ JS をバラまいておいて、ブラウザにキャッシュさせる気はないのだろうか。。
ファイルの属性は転送しないようなよっぽど変なデプロイツールを使っているのか、あるいはインターンで来た学生に JS を精密に写経させていたのだろうか。
ちなみに、上記であがっていた 4 つのファイルのそれぞれが、ETag と Last-Modified 共にサーバごとに合致しない。
GIF 画像ファイルまでもがそういう状況なのだから、インターンで来た学生にバイナリエディタで GIF を精密に写経させていたのだろうか。
ちなみに、今日の午前中の時点での各ファイルの ETag、Last-Modified の組み合わせを列挙してみた。
■ http://s.hatena.ne.jp/js/HatenaStar.js
■ http://s.hatena.ne.jp/images/comment.gif
■ http://s.hatena.ne.jp/images/add.gif
■ http://s.hatena.ne.jp/images/star.gif
これらを直して欲しいと切に願うと同時に、それぞれのファイルのヘッダがいつ正しく修正されるかを、今後継続して生暖かく見つめていきたいと思いました。
で、気になったのは
という箇所。13. Configure ETags
ETagsっていうのはサーバ上のファイルとブラウザのキャッシュが一致しているかどうかを検証するためのものなのですが、正しく利用できていないのであれば、ETagsは無駄なだけなので取り除いてやりましょう、という項目です。
http://s.hatena.ne.jp/js/HatenaStar.js
http://s.hatena.ne.jp/images/comment.gif
の4つのファイルに対してETagsヘッダが出ているようなので、必要なければ取り除いてヘッダサイズを減らしましょう。
ETag 自体は「正しく利用していれば」有用だし、ヘッダサイズ云々って言うほどデカくもないし、アレだなーとか思いつつ、なんで「正しく利用できていないのであれば」という前提のものに引っかかったのかなというのが非常に気になった。
ちなみに、ETag ヘッダは、デフォルトではそのファイルの inode 番号、ファイルサイズ、更新時刻 (epoch 秒) の値を 16 進数表記して、ハイフンで繋いだ文字列をダブルクォーテーションで括ったもの。
Perl で同じような文字列を出力するなら、
printf qq/ETag: "%x-%x-%x"\n/, (stat $filename)[1, 7, 9];こんな感じ。
-- 以下、HTTP とかにさほど詳しくない人向けの説明。
これが、HTTP レスポンスに含まれていたら、次に HTTP リクエストする際に、ブラウザ側でキャッシュした時の ETag の値を If-None-Match ヘッダに渡します。サーバは、今現在サーバ上にあるファイルの ETag の値が一致するかを確認して、一致した場合は 304 (Not Modified) の HTTP ステータスを返し、ブラウザは「以前キャッシュした時から更新されていない」と解釈して、キャッシュされているファイルを再利用します。
ETag の値を返さない場合、Last-Modified ヘッダが HTTP レスポンスに含まれていたら、次に HTTP リクエストする際に、ブラウザ側でキャッシュした時の Last-Modified の値を If-Modified-Since ヘッダに渡します。サーバは、今現在サーバ上にあるファイルの Last-Modified の値がそれ以前であるかを確認して、それ以前だった場合は 304 (Not Modified) の HTTP ステータスを返し、ブラウザは「以前キャッシュした時から更新されていない」と解釈して、キャッシュされているファイルを再利用します。
-- 以上、HTTP とかにさほど詳しくない人向けの説明。
で、思ったのは「はてなスター (だけ?) の静的ファイルが ETag を正しく使えてないから引っ掛かったのかな」と。
よくよく考えたらはてなスターはこのブログにも貼ってあるし、はてなの色々なコンテンツに大量に利用されている事実を考えたら、もし ETag を正しく使えてないのなら、無駄なトラフィックを大量に生み出しているわけで、決して侮れない。
ナニゲに HatenaStar.js はデカいし。
考えられるありがちな落し穴を考えると、「基本的には同じファイル」なんだけど、配置されているサーバが複数台数に分散されているような場合、ほぼ間違いなくそれぞれのサーバで inode 番号が変わるので、ETag に含まれる値の inode の部分 (先頭部分) はリクエストを処理するサーバによって異なってくる。
そういう場合は、ETag を吐かないようにさえすれば、Last-Modified と If-Modified-Since でいいあんばいにキャッシュ比較とかをしてくれる。
Apache の場合は FileETag ディレクティブを利用して、以下のように設定すれば ETag を吐かなくなる。
FileETag None
あるいは、ファイルサイズ、更新時刻 (epoch 秒) は一緒なのであれば、ETag を吐く時に不要になるのは inode 番号だけ。では、ETag の値から inode 番号を抑制すれば済む問題なので、同様に FileETag ディレクティブを利用して、inode 番号だけ利用しないように、以下のように設定するのがセオリー。
FileETag Size MTimeあるいは
FileETag -INodeこうすることで、複数台数であっても、ファイルサイズと更新時刻によって生成された値によって、ETag と If-None-Match でいいあんばいにキャッシュ比較とかをしてくれる。
じゃあ、はてなスターは、このありがちな落し穴をおかしているのではないだろうか?とアタリをつけて、HTTP HEAD リクエストを送出してみて確認してみた。
% telnet s.hatena.ne.jp 80 Trying 59.106.108.97... Connected to s.hatena.ne.jp. Escape character is '^]'. HEAD /js/HatenaStar.js HTTP/1.1 Host: s.hatena.ne.jp Cookie: b=hoge Connection: close HTTP/1.1 200 OK Date: Thu, 27 Nov 2008 00:53:51 GMT Server: Apache Last-Modified: Tue, 04 Nov 2008 09:25:37 GMT ETag: "b88995-1697f-45ad9a572a640" Accept-Ranges: bytes Content-Length: 92543 Vary: Accept-Encoding Connection: close Content-Type: application/x-javascript Connection closed by foreign host.* ちなみに、b=hoge とかいう Cookie を送っているのは、session cookie をリクエストの都度吐いてくるので、迷惑にならないように「既に session cookie 持ってるよ」と欺いてます
案の定、ETag は三つの値がハイフンで繋がれています。
恐らく先頭の b88995 は inode 番号なので、複数台のサーバで運用されている場合は、邪魔になる恐れがあります。
続いて、それを検証するために、この ETag の値と、Last-Modified の値を、それぞれ If-None-Match と If-Modified-Since に含んでみたら、ちゃんと 304 (Not Modified) ステータスを返してくれるか検証してみました。
% telnet s.hatena.ne.jp 80 Trying 59.106.108.97... Connected to s.hatena.ne.jp. Escape character is '^]'. HEAD /js/HatenaStar.js HTTP/1.1 Host: s.hatena.ne.jp Connection: close Cookie: b=hoge If-Modified-Since: Tue, 04 Nov 2008 09:25:37 GMT If-None-Match: "b88995-1697f-45ad9a572a640" HTTP/1.1 200 OK Date: Thu, 27 Nov 2008 00:54:48 GMT Server: Apache Last-Modified: Tue, 04 Nov 2008 09:20:37 GMT ETag: "f9665-1697f-45ad993910340" Accept-Ranges: bytes Content-Length: 92543 Vary: Accept-Encoding Connection: close Content-Type: application/x-javascript Connection closed by foreign host.304 (Not Modified) を期待していたら、200 (OK) ステータスが返ってきました。
ETag を見るとやはり先頭が f9665 で先程とは違うので、inode 番号が違います。
でも Content-Length も一緒だし、MD5 の checksum を見ても同じなので、同じ JS ファイルであることは間違いありません。
でも「あなたのとこにキャッシュされているファイルとは、比較するかぎりどうも別のファイルだから、このデッカイ JS ファイルをあらためて読み込んでキャッシュしなおしてよ」とブラウザに訴えてきているのです。
堀愚霊瑠氏の指摘するとおり、正しく使えてない、無駄な ETag を吐いてます。
何度か繰り返してみると、ETag によって返される inode 番号は 2 種類であることから、「はてなスターは 2 台のサーバから静的なファイルを返している」ということがわかります。
結論: はてなスターは ETag を吐かないようにするか、inode 番号を利用しないように正して、きちんとブラウザのキャッシュを有効活用させるようにして欲しいです!
って、気持ちよくしめようとしたら、現実はもっとひどいことに気付いた。
目を凝らしてよく見れば ETag だけの問題ではない。
Last-Modified が 2 台それぞれのサーバによって違う。
5 分も違う。
当然、ETag も inode 番号の部分だけじゃなく、更新時刻の部分も違う。
ETag を抑止しても、Last-Modified がバラバラだから、古いほうを先に GET してしまうと結局新しいほうを GET しに行った時に 304 ステータスは返らない。
はてなスターは、あれだけのパーツ画像とデッカイ JS をバラまいておいて、ブラウザにキャッシュさせる気はないのだろうか。。
ファイルの属性は転送しないようなよっぽど変なデプロイツールを使っているのか、あるいはインターンで来た学生に JS を精密に写経させていたのだろうか。
ちなみに、上記であがっていた 4 つのファイルのそれぞれが、ETag と Last-Modified 共にサーバごとに合致しない。
GIF 画像ファイルまでもがそういう状況なのだから、インターンで来た学生にバイナリエディタで GIF を精密に写経させていたのだろうか。
ちなみに、今日の午前中の時点での各ファイルの ETag、Last-Modified の組み合わせを列挙してみた。
■ http://s.hatena.ne.jp/js/HatenaStar.js
Last-Modified: Tue, 04 Nov 2008 09:25:37 GMT ETag: "b88995-1697f-45ad9a572a640"
Last-Modified: Tue, 04 Nov 2008 09:20:37 GMT ETag: "f9665-1697f-45ad993910340"
■ http://s.hatena.ne.jp/images/comment.gif
Last-Modified: Tue, 13 May 2008 13:04:52 GMT ETag: "f8ff2-362-44d1c4f516500"
Last-Modified: Tue, 13 May 2008 13:04:53 GMT ETag: "b88361-362-44d1c4f60a740"
■ http://s.hatena.ne.jp/images/add.gif
Last-Modified: Tue, 13 May 2008 13:04:52 GMT ETag: "f8fe8-51-44d1c4f516500"
Last-Modified: Tue, 13 May 2008 13:04:53 GMT ETag: "b88357-51-44d1c4f60a740"
■ http://s.hatena.ne.jp/images/star.gif
Last-Modified: Tue, 13 May 2008 13:04:52 GMT ETag: "f9044-b2-44d1c4f516500"
Last-Modified: Tue, 13 May 2008 13:04:53 GMT ETag: "b883b3-b2-44d1c4f60a740"
これらを直して欲しいと切に願うと同時に、それぞれのファイルのヘッダがいつ正しく修正されるかを、今後継続して生暖かく見つめていきたいと思いました。