ブラウザキャッシュによる HTTP 高速化チューニング

かれこれ一年ほど前に実施した実サービスでの apache のチューニングネタを思い出したように書いています。

以前いた部署では少ないサーバ台数で大量のリクエストを如何に処理しきるかってことに燃えていたので、静的コンテンツなどをブラウザに支障のない範囲で最大限にキャッシュさせ、サーバとネットワークの負荷を最小化させていました。

当時参考にした情報源は以下の3つでした。
どのようなレスポンスヘッダを返しておけばブラウザキャッシュを最大化できるかのテクニックがまとめられています。

チューニングにおいて重要なのは自分自身での検証。というわけで自前で検証した結果と検証するために用意したプログラムを公開します。Plack による実装なので、ご自分の端末で簡単に環境構築&検証ができるかと思います。

- スポンサーリンク -

ブラウザキャッシュで HTTP を高速化するためのレスポンスヘッダーの検証

検証環境ブラウザ

・ Google Chrome 4.1.249.1064 ・ FireFox 3.6.3 ・ ie8.0.7600.16385

レスポンスヘッダは下記の組み合わせ

・ Expires: なし過去現在未来
・ Last-Modified: なし過去  
・ Cache-Control: なしprivateprivate,max-age=86400no-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 ※表示する画像ファイルです。

プログラムの使い方です。

  1. 上記のファイルを全部同じディレクトリに保存します。
  2. まずは Plack が入っている環境を用意します。
  3. plackup server.pgi と入力して http サーバを起動します。
  4. firefox, ie なりブラウザを立ち上げて、http://localhost:5000/index.html とアクセスします。
    ※こんなようなページが起動するはずです。
    img001.png
  5. 画像が全部表示されたら、「このページにもう一度アクセス」リンクをクリックします。
  6. もう一度画像が全部表示されたら、CTRL + C で plack を停止します。
  7. perl analyze.pl access.log と入力して、上記表どおりの順序になるようにアクセスログを集計します。
  8. 表にまとめてお終い。次の分析を行う際には、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 にする運用をしていました。
その他にファイル名にバージョンを加えることで名前をユニークにするテクニックもあります。

※前述しましたが、実験用に書いたプログラムが正しいのか若干不安です。ミスなどありましたらご指摘くださいませ。

- スポンサーリンク -