2007/02/12

mt-search.cgiを mod_cacheで超高速化する!!

かなり前からApache 2.2.xを使っているのですが、mod_cache/mod_disk_cacheなんていうモジュールが存在することに全然気がついていませんでした。このモジュールはサーバサイドでコンテンツキャッシングを実現するもので、CGIなどを使って生成される動的なコンテンツのレンダリング結果の再利用を可能にします(静的なコンテンツもキャッシュできますが、応答時間が問題になることはありませんし、一般的にはクライアントサイドでキャッシングされます)。キャッシュするコンテンツは時間制約の強くないものである必要があります。例えば、コメンティングシステムなどではユーザが行った操作をコンテンツに即座に反映する必要があるために適していませんが、検索システムやCMSのように対象となるデータセットが一定以上の間隔で更新されると期待され、レンダリング結果が変化しないような場合にはとても有効です。

Movable Type 3.3以降ではタグアーカイブの生成にmt-search.cgiが使われていてまたえらく遅いのですが、これを超高速化できるのではないかと思って試してみました。

httpd.confの記述

まず、何はなくともmod_cache, mod_disk_cacheをロードしましょう。

LoadModule cache_module libexec/apache22/mod_cache.so
LoadModule disk_cache_module libexec/apache22/mod_disk_cache.so

次にmod_disk_cache - Apache HTTP サーバmod_cache - Apache HTTP サーバを読んで必要な設定を加えます。最小限の設定は以下のようになります。

<IfModule mod_cache.c>
    <IfModule mod_disk_cache.c>
        CacheRoot /var/www/cache
        CacheEnable disk /MTDIR/mt-search.cgi
    </IfModule>
</IfModule>

httpd.confの編集が済んだらapachectl restartします。

MT::Bootstrapの修正

HTTP/1.1: Caching in HTTPあたりを読むと、クエリ文字列付きのHEAD/GETリクエストでは明示的に有効期限をサーバが返さない限り、キャッシュ機構はレスポンスをfreshなものとして取り扱ってはならない旨が書かれています。

一方、mod_cacheにはCacheIgnoreNoLastModという、上記のRFC2616の項をいい感じに無視してくれそうなディレクティブが用意されています。しかし、これを有効にした場合には、通常の「ETag、Last-Modfied、Expiresヘッダのいずれか一つを持ち、クエリ文字列のない」URLに加え、「ETag、Last-Modfied、Expiresヘッダのいずれもないが、クエリ文字列のない」URLのコンテンツもキャッシュされるようになりますが、「ETag、Last-Modfied、Expiresヘッダのいずれもなく、クエリ文字列がある」URLは依然としてキャッシュされません。

結局のところ、mod_cacheにキャッシュしてもらうには、mt-search.cgiが「ETag、Last-Modfied、Expiresヘッダのいずれか」を返答するように変更する必要があるということです。と言っても難しい変更ではなく、MT 3.34を例にとると以下の小変更で済みます。

--- lib/MT/Bootstrap.pm.bak	Wed Jan 10 12:11:30 2007
+++ lib/MT/Bootstrap.pm	Mon Feb 12 02:12:43 2007
@@ -63,11 +63,15 @@
                     local $SIG{__WARN__} = sub { $app->trace($_[0]) };
                     MT->set_instance($app);
                     $app->init_request(CGIObject => $cgi);
+                    $app->set_header('Expires' => '+1h')
+                        if $class eq 'MT::App::Search';
                     $app->run;
                 }
             } else {
                 $app = $class->new( %param ) or die $class->errstr;
                 local $SIG{__WARN__} = sub { $app->trace($_[0]) };
+                $app->set_header('Expires' => '+1h')
+                    if $class eq 'MT::App::Search';
                 $app->run;
             }
         };

この例ではキャッシュの有効期間を1時間に設定していますが、お好みで変更するとよいでしょう。

性能は?

試しに100回ほど問い合わせてみました。

キャッシュなしの場合:

$ ab -n 100 'http://example.org/MTDIR/mt-search.cgi?tag=x&blog_id=1'
(snipped)

Server Software:        Apache/2.2.4
Server Hostname:        example.org
Server Port:            80

Document Path:          /MTDIR/mt-search.cgi?tag=x&blog_id=1
Document Length:        3877 bytes

Concurrency Level:      1
Time taken for tests:   41.432825 seconds
Complete requests:      100
Failed requests:        0
Write errors:           0
Total transferred:      423600 bytes
HTML transferred:       387700 bytes
Requests per second:    2.41 [#/sec] (mean)
Time per request:       414.328 [ms] (mean)
Time per request:       414.328 [ms] (mean, across all concurrent requests)
Transfer rate:          9.97 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.0      0       0
Processing:   404  413  22.5    410     598
Waiting:      399  408  22.5    404     592
Total:        404  413  22.5    410     598

Percentage of the requests served within a certain time (ms)
  50%    410
  66%    410
  75%    410
  80%    410
  90%    414
  95%    415
  98%    516
  99%    598
 100%    598 (longest request)

キャッシュありの場合:

$ ab -n 100 'http://example.org/MTDIR/mt-search.cgi?tag=x&blog_id=1'
(snipped)

Server Software:        Apache/2.2.4
Server Hostname:        example.org
Server Port:            80

Document Path:          /MTDIR/mt-search.cgi?tag=x&blog_id=1
Document Length:        3877 bytes

Concurrency Level:      1
Time taken for tests:   0.487676 seconds
Complete requests:      100
Failed requests:        0
Write errors:           0
Total transferred:      426592 bytes
HTML transferred:       387700 bytes
Requests per second:    205.05 [#/sec] (mean)
Time per request:       4.877 [ms] (mean)
Time per request:       4.877 [ms] (mean, across all concurrent requests)
Transfer rate:          853.03 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.0      0       0
Processing:     0    4  41.1      0     411
Waiting:        0    4  41.0      0     410
Total:          0    4  41.1      0     411

Percentage of the requests served within a certain time (ms)
  50%      0
  66%      0
  75%      0
  80%      0
  90%      0
  95%      0
  98%      0
  99%    411
 100%    411 (longest request)

41.43秒から0.49秒に高速化!!!

もう少しちゃんと見ると、キャッシュありの方は最初の一回目のリクエストに411msecかかっていて、残りの99回分のリクエストには77msecしか要していません。したがって、キャッシュヒット時には500倍以上速くなっているということです。条件によって結果はいろいろ変わってきますけどね。

ついでなので、FastCGI (mod_fcgid)でキャッシュあり・なしのデータも追加しておきます。

FastCGI (mod_fcgid) + キャッシュなしの場合:

$ ab -n 100 'http://example.org/MTDIR/mt-search.fcgi?tag=x&blog_id=1'
(snipped)

Server Software:        Apache/2.2.4
Server Hostname:        example.org
Server Port:            80

Document Path:          /MTDIR/mt-search.fcgi?tag=x&blog_id=1
Document Length:        3877 bytes

Concurrency Level:      1
Time taken for tests:   2.945091 seconds
Complete requests:      100
Failed requests:        0
Write errors:           0
Total transferred:      419600 bytes
HTML transferred:       387700 bytes
Requests per second:    33.95 [#/sec] (mean)
Time per request:       29.451 [ms] (mean)
Time per request:       29.451 [ms] (mean, across all concurrent requests)
Transfer rate:          138.88 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.0      0       0
Processing:    29   29   1.3     29      42
Waiting:       28   28   1.4     28      42
Total:         29   29   1.3     29      42

Percentage of the requests served within a certain time (ms)
  50%     29
  66%     29
  75%     29
  80%     29
  90%     29
  95%     29
  98%     29
  99%     42
 100%     42 (longest request)

FastCGI (mod_fcgid) + キャッシュありの場合:

$ ab -n 100 'http://example.org/MTDIR/mt-search.fcgi?tag=x&blog_id=1'
(snipped)

Server Software:        Apache/2.2.4
Server Hostname:        example.org
Server Port:            80

Document Path:          /MTDIR/mt-search.fcgi?tag=x&blog_id=1
Document Length:        3877 bytes

Concurrency Level:      1
Time taken for tests:   0.107752 seconds
Complete requests:      100
Failed requests:        0
Write errors:           0
Total transferred:      426570 bytes
HTML transferred:       387700 bytes
Requests per second:    928.06 [#/sec] (mean)
Time per request:       1.078 [ms] (mean)
Time per request:       1.078 [ms] (mean, across all concurrent requests)
Transfer rate:          3860.72 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.0      0       0
Processing:     0    0   3.0      0      30
Waiting:        0    0   2.9      0      29
Total:          0    0   3.0      0      30

Percentage of the requests served within a certain time (ms)
  50%      0
  66%      0
  75%      0
  80%      0
  90%      0
  95%      0
  98%      0
  99%     30
 100%     30 (longest request)

まとめると私の環境でのmt-search.cgiの応答時間は、キャッシュヒット時には約0.8msec、キャッシュミス時にはCGI版で約400msec、FastCGI版で約30msec、ということになります。念のため、キャッシュミス時の応答時間はアプリケーションにも依存しますし、キャッシュヒット時の応答時間はキャッシュしているデータのサイズに依存します。

また、このエントリーで書いたmod_cacheを使った高速化手法は、mt-search.cgi以外の任意のアプリケーションに適用可能です。冒頭でも触れましたが、比較的ルーズなコンシステンシを実現すればいいようなコンテンツ配信の高速化には絶大な効果がありますね。

0 コメント: