ブラウザキャッシュによる HTTP 高速化チューニング
かれこれ一年ほど前に実施した実サービスでの apache のチューニングネタを思い出したように書いています。
以前いた部署では少ないサーバ台数で大量のリクエストを如何に処理しきるかってことに燃えていたので、静的コンテンツなどをブラウザに支障のない範囲で最大限にキャッシュさせ、サーバとネットワークの負荷を最小化させていました。
当時参考にした情報源は以下の3つでした。
どのようなレスポンスヘッダを返しておけばブラウザキャッシュを最大化できるかのテクニックがまとめられています。
チューニングにおいて重要なのは自分自身での検証。というわけで自前で検証した結果と検証するために用意したプログラムを公開します。Plack による実装なので、ご自分の端末で簡単に環境構築&検証ができるかと思います。
ブラウザキャッシュで HTTP を高速化するためのレスポンスヘッダーの検証
検証環境ブラウザ
・ Google Chrome 4.1.249.1064 ・ FireFox 3.6.3 ・ ie8.0.7600.16385
レスポンスヘッダは下記の組み合わせ
・ Expires : なし 過去 現在 未来 ・ Last-Modified : なし 過去 ・ Cache-Control : なし private private,max-age=86400 no-cache,max-age=86400 ・ Pragma : なし no-cache
ブラウザキャッシュを最大化するテクニックの結論
ブラウザキャッシュを最大化するためのレスポンスヘッダの戦略は同様の結果となり、 最速設定なら 「Pragma ヘッダなし」 + 「Cache-Controlヘッダは private, max-age=??秒数」 最適設定なら 「Expires ヘッダあり」 + 「Last-Modified ヘッダあり」+ 「Cache-Controlヘッダは private」 とすれば良いことがわかりました。
検証した環境、プログラムが違うこともあり、「ブラウザキャッシュとレスポンスヘッダ - murankの日記」による結果とは異なる結果となっています。※作成したプログラムが変な場合には是非突っ込みをください。m(_ _)m
以下詳細です。表の見方は下記の通りです。
・ 200 が通常の GET リクエストが発生した
・ 304 が If-Modified-Since ヘッダ付きの条件付 GET リクエストが発生した
・ − がブラウザキャッシュを利用したためリクエストが発生しなかった
Google Chrome 4.1.249.1064 |
Last-Modified なし |
Last-Modified あり |
|||||||
Expires なし |
Expires 過去 |
Expires 現在 |
Expires 未来 |
Expires なし |
Expires 過去 |
Expires 現在 |
Expires 未来 |
||
Pragma なし |
Cache-Control なし |
200 | 200 | 200 | 200 | - | 304 | 304 | 304 |
Cache-Control private |
200 | 200 | 200 | 200 | - | 304 | 304 | 304 | |
Cache-Control private, max-age |
- | - | - | - | - | - | - | - | |
Cache-Control no-cache |
200 | 200 | 200 | 200 | 304 | 304 | 304 | 304 | |
Cache-Control no-cache, max-age |
200 | 200 | 200 | 200 | 304 | 304 | 304 | 304 | |
Pragma no-cache |
Cache-Control なし |
200 | 200 | 200 | 200 | 304 | 304 | 304 | 304 |
Cache-Control private |
200 | 200 | 200 | 200 | 304 | 304 | 304 | 304 | |
Cache-Control private, max-age |
200 | 200 | 200 | 200 | 304 | 304 | 304 | 304 | |
Cache-Control no-cache |
200 | 200 | 200 | 200 | 304 | 304 | 304 | 304 | |
Cache-Control no-cache, max-age |
200 | 200 | 200 | 200 | 304 | 304 | 304 | 304 |
FireFox 3.6.3 |
Last-Modified なし |
Last-Modified あり |
|||||||
Expires なし |
Expires 過去 |
Expires 現在 |
Expires 未来 |
Expires なし |
Expires 過去 |
Expires 現在 |
Expires 未来 |
||
Pragma なし |
Cache-Control なし |
200 | 200 | 200 | 200 | - | 304 | 304 | 304 |
Cache-Control private |
200 | 200 | 200 | 200 | - | 304 | 304 | 304 | |
Cache-Control private, max-age |
- | - | - | - | - | - | - | - | |
Cache-Control no-cache |
200 | 200 | 200 | 200 | 304 | 304 | 304 | 304 | |
Cache-Control no-cache, max-age |
200 | 200 | 200 | 200 | 304 | 304 | 304 | 304 | |
Pragma no-cache |
Cache-Control なし |
200 | 200 | 200 | 200 | 304 | 304 | 304 | 304 |
Cache-Control private |
200 | 200 | 200 | 200 | 304 | 304 | 304 | 304 | |
Cache-Control private, max-age |
200 | 200 | 200 | 200 | 304 | 304 | 304 | 304 | |
Cache-Control no-cache |
200 | 200 | 200 | 200 | 304 | 304 | 304 | 304 | |
Cache-Control no-cache, max-age |
200 | 200 | 200 | 200 | 304 | 304 | 304 | 304 |
ie8 8.0.7600.16385 |
Last-Modified なし |
Last-Modified あり |
|||||||
Expires なし |
Expires 過去 |
Expires 現在 |
Expires 未来 |
Expires なし |
Expires 過去 |
Expires 現在 |
Expires 未来 |
||
Pragma なし |
Cache-Control なし |
200 | 200 | 200 | 200 | - | 304 | 304 | 304 |
Cache-Control private |
200 | 200 | 200 | 200 | - | 304 | 304 | 304 | |
Cache-Control private, max-age |
- | - | - | - | - | - | - | - | |
Cache-Control no-cache |
200 | 200 | 200 | 200 | - | 304 | 304 | 304 | |
Cache-Control no-cache, max-age |
- | - | - | - | - | - | - | - | |
Pragma no-cache |
Cache-Control なし |
200 | 200 | 200 | 200 | - | 304 | 304 | 304 |
Cache-Control private |
200 | 200 | 200 | 200 | - | 304 | 304 | 304 | |
Cache-Control private, max-age |
- | - | - | - | - | - | - | - | |
Cache-Control no-cache |
200 | 200 | 200 | 200 | - | 304 | 304 | 304 | |
Cache-Control no-cache, max-age |
- | - | - | - | - | - | - | - |
本実験向けに用意したプログラムの説明
以下、検証用のプログラム一式です。ソースコードも非常に短いので plack のお勉強用にも使えます。w
・ server.psgi ※コンテンツ&レスポンスヘッダを返すための http サーバです。
・ analyze.pl ※アクセスログから 200, 304, - を分析するスクリプトです。
・ local.html ※様々なアクセスパターンが埋め込まれた index ページです。
・ c.gif ※表示する画像ファイルです。
プログラムの使い方です。
- 上記のファイルを全部同じディレクトリに保存します。
- まずは Plack が入っている環境を用意します。
- plackup server.pgi と入力して http サーバを起動します。
- firefox, ie なりブラウザを立ち上げて、http://localhost:5000/index.html とアクセスします。
※こんなようなページが起動するはずです。
- 画像が全部表示されたら、「このページにもう一度アクセス」リンクをクリックします。
- もう一度画像が全部表示されたら、CTRL + C で plack を停止します。
- perl analyze.pl access.log と入力して、上記表どおりの順序になるようにアクセスログを集計します。
- 表にまとめてお終い。次の分析を行う際には、log.txt と access.log を削除してからやってください。
server.pgi の解説
- Plack::Middleware::AccessLog により、アクセスログを取得しています。
- http://localhost:5000/img/(.+?)/(.+?)/(.+?)/(.+?)/c.gif というパスから、出力するレスポンスヘッダを決めています。
それぞれ順に、Expires, Last-Modified, Cache-Control, Pragma を制御します。
Expires : 0=null / 1=過去 / 2=現在 / 3=未来
Last-Modified : 0=null / 1=過去
Cache-Control : 0=null / 1=private / 2=private, max-age=86400 / 3=no-cache / 4=no-cache, max-age=86400
Pragma : 0=null / 1=no-cache
- HTTP_IF_MODIFIED_SINCE がリクエストヘッダに存在する場合、条件 GET リクエストに対して 304 ステータスコードを返し、ブラウザキャッシュを使うように通知します。
- /index.html で local.html を読み込んで index ページとして返します。
server.psgi のソースコード
use strict; use warnings; use Plack::Request; use Plack::Builder; use Data::Dumper; use File::MMagic; use DateTime; use DateTime::Format::HTTP; use Log::Dispatch; my $app = sub { my $env = shift; my $req = Plack::Request->new($env); my $age = 86400; open my $log, '>>', 'log.txt'; print $log Dumper $env; close $log; ## img 条件付きアクセス if ( $env->{HTTP_IF_MODIFIED_SINCE} ) { return [ 304, [], [], ]; } ## img 通常アクセス elsif ( $req->path =~ m!^/img/(.+?)/(.+?)/(.+?)/(.+?)/c\.gif! ) { my $expires = $1; my $lastmodified = $2; my $cachecontrol = $3; my $pragma = $4; my $file = 'c.gif'; my $type = File::MMagic->new->checktype_filename($file); open my $fh, '<', $file or die $!; binmode $fh; my $data = do { local $/; <$fh> }; close $fh; my $dt1 = DateTime->from_epoch( epoch => time - $age ); my $dt2 = DateTime->from_epoch( epoch => time + $age ); my $yesterday = DateTime::Format::HTTP->format_datetime($dt1); my $tomorrow = DateTime::Format::HTTP->format_datetime($dt2); my $now = DateTime::Format::HTTP->format_datetime(); my %header; $header{'Content-type'} = $type; $header{'Pragma'} = 'no-cache' if $pragma; $header{'Cache-Control'} = 'private' if $cachecontrol == 1; $header{'Cache-Control'} = "private, max-age=$age" if $cachecontrol == 2; $header{'Cache-Control'} = 'no-cache' if $cachecontrol == 3; $header{'Cache-Control'} = "no-cache, max-age=$age" if $cachecontrol == 4; $header{'Last-Modified'} = $yesterday if $lastmodified; $header{'Expires'} = $yesterday if $expires == 1; $header{'Expires'} = $now if $expires == 2; $header{'Expires'} = $yesterday if $expires == 3; return [ 200, [%header], [$data], ]; } ## index elsif ( $req->path =~ m!^/index\.html! ) { open my $fh, '<', 'local.html' or die $!; binmode $fh; my $data = do { local $/; <$fh> }; close $fh; my $res = $req->new_response(200); $res->content_type('text/html'); $res->body($data); $res->finalize; } ## ??? else { my $res = $req->new_response(200); $res->content_type('text/html'); $res->body( $req->path ); $res->finalize; } }; builder { my $logger = Log::Dispatch->new( outputs => [ [ 'File', min_level => 'debug', filename => 'access.log' ], ], ); enable "Plack::Middleware::AccessLog", logger => sub { $logger->log( level => 'debug', message => @_ ) }; $app; };
analyze.pl の解説
- access.log 内の1回目のアクセスログを読み飛ばし、2回目のアクセスログを読み込む。
- 上記表どおりの順序になるようにアクセスログを集計して標準出力に結果を表示。
my %log; my $count = 0; open my $fh, '<', $ARGV[0] or die $!; while (<$fh>) { chomp; $count++ if $_ =~ m!"GET\s/index.html!; next if $count < 2; my @data = split /\s/, $_; my $path = $data[6]; my $status = $data[8]; $log{$path} = $status; } close $fh; for my $pragma ( 0 .. 1 ) { for my $cachecontrol ( 0 .. 4 ) { for my $lastmodified ( 0 .. 1 ) { for my $expires ( 0 .. 3 ) { $key = "/img/$expires/$lastmodified/$cachecontrol/$pragma/c.gif"; print $log{$key} || ' - '; print ","; } } print "\n"; } }
静的コンテンツをブラウザキャッシュで高速化するための apache の設定
というわけで、apache - httpd.conf の設定はこんな感じにしています。コンテンツ圧縮も使って更にネットワーク負荷を軽減しています。
apache 2 系の httpd.conf
## mod_headers FileETag none Header onsuccess append Cache-Control private, max-age=86400 ## mod_expires ExpiresActive On ExpiresByType image/jpeg "access plus 1 days" ExpiresByType image/png "access plus 1 days" ExpiresByType image/gif "access plus 1 days" ExpiresByType text/css "access plus 1 days" ExpiresByType text/javascript "access plus 1 days" ExpiresByType application/x-javascript "access plus 1 days" ExpiresByType application/javascript "access plus 1 days" ## mod_deflate DeflateCompressionLevel 5 AddOutputFilterByType DEFLATE text/html AddOutputFilterByType DEFLATE text/plain AddOutputFilterByType DEFLATE text/css AddOutputFilterByType DEFLATE text/xml AddOutputFilterByType DEFLATE application/x-javascript AddOutputFilterByType DEFLATE application/xml AddOutputFilterByType DEFLATE application/rdf+xml
apache 1.3 系の httpd.conf
## mod_headers Header append Cache-Control "private, max-age=86400" ## mod_expires ExpiresActive On ExpiresByType image/jpeg A86400 ExpiresByType image/png A86400 ExpiresByType image/gif A86400 ExpiresByType text/css A86400 ExpiresByType text/javascript A86400 ExpiresByType application/x-javascript A86400 ExpiresByType application/javascript A86400 ## mod_gzip LogFormat "%h %l %u %t \"%r\" %>s %b mod_gzip: %{mod_gzip_compression_ratio}npct." common_with_mod_gzip_info LogFormat "%h %l %u %t \"%r\" %>s %b mod_gzip: %{mod_gzip_result}n In:%{mod_gzip_input_size}n Out:%{mod_gzip_output_size}n:%{mod_gzip_compression_ratio}npct." common_with_mod_gzip_info2 CustomLog /usr/local/lib/apache/logs/gzip_log common_with_mod_gzip_info2 mod_gzip_send_vary No mod_gzip_on Yes mod_gzip_static_suffix .gz AddEncoding gzip .gz mod_gzip_update_static No mod_gzip_dechunk yes mod_gzip_keep_workfiles No mod_gzip_minimum_file_size 1000 mod_gzip_maximum_file_size 0 mod_gzip_maximum_inmem_size 60000 mod_gzip_keep_workfiles No mod_gzip_temp_dir /tmp mod_gzip_handle_methods GET POST mod_gzip_item_include mime ^application/x-httpd-cgi mod_gzip_item_include mime ^application/x-httpd-php mod_gzip_item_include handler ^perl-script$ mod_gzip_item_include handler ^server-status$ mod_gzip_item_include handler ^server-info$ mod_gzip_item_include mime ^text/.* mod_gzip_item_include mime ^httpd/unix-directory$ mod_gzip_item_include file \.shtml$ mod_gzip_item_include file \.htm$ mod_gzip_item_include file \.html$ mod_gzip_item_include file \.txt$ mod_gzip_item_include file \.php$ mod_gzip_item_include file \.pl$ mod_gzip_item_include file \.cgi$ mod_gzip_item_exclude mime ^image/.* mod_gzip_item_exclude file \.css$ mod_gzip_item_exclude file \.js$ mod_gzip_min_http 1001
最後ですが、上記のような設定を行った場合のリスクについて理解しておく必要があります。
上記設定を適用すると、一度表示した画像など静的コンテンツは、丸一日の間ブラウザキャッシュのみが利用されます。何らかの理由でコンテンツを更新した場合でも、条件付き GET リクエストすら発生しないため、最新のファイルが反映されないこととなります。
したがって、コンテンツ更新の頻度が高いファイルに対しては、上記設定を行うことにより不具合が発生します。逆にほぼ静的のままの画像等のコンテンツは上記設定が、高速化にかなり効いてきます。
僕的な使い方は、サイトのリニューアル前には、数日にわたり設定を一時的に OFF にして、リニューアルして落ち着いた時点で、再度 ON にする運用をしていました。
その他にファイル名にバージョンを加えることで名前をユニークにするテクニックもあります。
※前述しましたが、実験用に書いたプログラムが正しいのか若干不安です。ミスなどありましたらご指摘くださいませ。
コメントやシェアをお願いします!