CoWでテンプレートキャッシュを共有!(C::V::H::T編)

|
WebプログラマのためのCopy On Write解説:mod_perl/FastCGIでメモリを節約する方法 | Typemiss.netではCopy On Writeの概観を示しモジュールはfork前にロード!と書きました。今度はWebアプリケーションが使うデータ領域を共有し,更にメモリを節約することを考えてみましょう。何を共有するか? Webアプリケーションが共通して使うデータには何があるか? 一番先に思い浮かぶのがテンプレートのキャッシュです。 まず,CatalystでCatalyst::View::HTML::Templateを何も考えないで使ったときはどうなっているのか,見てみよう。とりあえずテスト用にアプリを作ります。 $ catalyst.pl -short TestApp created "TestApp" created "TestApp/script" created "TestApp/lib" created "TestApp/root" created "TestApp/root/static" created "TestApp/root/static/images" created "TestApp/t" created "TestApp/lib/TestApp" created "TestApp/lib/TestApp/M" created "TestApp/lib/TestApp/V" created "TestApp/lib/TestApp/C" created "TestApp/testapp.yml" created "TestApp/lib/TestApp.pm" created "TestApp/README" created "TestApp/Changes" created "TestApp/t/01app.t" created "TestApp/t/02pod.t" created "TestApp/t/03podcoverage.t" created "TestApp/root/static/images/catalyst_logo.png" created "TestApp/root/static/images/btn_120x50_built.png" created "TestApp/root/static/images/btn_120x50_built_shadow.png" created "TestApp/root/static/images/btn_120x50_powered.png" created "TestApp/root/static/images/btn_120x50_powered_shadow.png" created "TestApp/root/static/images/btn_88x31_built.png" created "TestApp/root/static/images/btn_88x31_built_shadow.png" created "TestApp/root/static/images/btn_88x31_powered.png" created "TestApp/root/static/images/btn_88x31_powered_shadow.png" created "TestApp/root/favicon.ico" created "TestApp/Makefile.PL" created "TestApp/script/testapp_cgi.pl" created "TestApp/script/testapp_fastcgi.pl" created "TestApp/script/testapp_server.pl" created "TestApp/script/testapp_test.pl" created "TestApp/script/testapp_create.pl" $ script/testapp_create.pl view HT HTML::Template  exists "/home/carpflag/TestApp/script/../lib/TestApp/V"  exists "/home/carpflag/TestApp/script/../t" created "/home/carpflag/TestApp/script/../lib/TestApp/V/HT.pm" created "/home/carpflag/TestApp/script/../t/v_HT.t"   ちょっとだけ修正して,/xxxでxxx.tmplを読むようにする。ついでにユーティリティメソッドを入れておこう。自分自身で使用しているメモリを出力させてみる。 package TestApp; use strict; use warnings; use Catalyst qw/ConfigLoader Static::Simple DefaultEnd/; use Linux::Smaps; our $VERSION = '0.01'; __PACKAGE__->setup; sub default : Private {     my ( $self, $c ) = @_;     $c->stash->{template} = ($c->request->arguments->[0] || '0').".tmpl"; } sub pid : Local {     my ( $self, $c ) = @_;     $c->response->content_type('text/plain');     $c->response->body($$); } sub inc : Local {     my ( $self, $c ) = @_;     $c->response->content_type('text/plain');     $c->response->body( "INC contains " . scalar(keys %INC) . "\n" .       join("\n",sort keys %INC). "\n" ); } sub smaps : Local {     my ($self, $c ) = @_;     $c->response->content_type('text/plain');     my $s = Linux::Smaps->new($$)->all;     my $str = sprintf(       "Pid: %8d\nSize:          %8d kB\nRss:           %5d kB\n Shared_Clean:  %5d kB\n Shared_Dirty:  %5d kB\n Private_Clean: %5d kB\n Private_Dirty: %5d kB\n",       $$, map{$s->$_} qw(size rss shared_clean shared_dirty private_clean private_dirty));     $c->response->body( $str ); } sub smaps_gp : Local {     my ($self, $c ) = @_;     $c->response->content_type('text/plain');     my $s = Linux::Smaps->new($$)->all;     my $str = join ' ', map{$s->$_} qw(size rss shared_clean shared_dirty private_clean private_dirty);     $c->response->body( $str ); } 1;   package TestApp::V::HT; use strict; use base 'Catalyst::View::HTML::Template'; __PACKAGE__->config(   die_on_bad_params => 0, ); 1;   テンプレートとしては0..999と1000個を用意します。内容は最初の行に一応番号を入れておき,続けて"x"100文字を100行作ります。一つのテンプレートで大体10kBになります。全部のテンプレートが読み込まれると大体10MBです。 $ cat generate.sh #!/bin/sh for x in `seq 0 999`; do   echo $x > root/$x.tmpl   for y in `seq 0 99`; do     echo 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' >> root/$x.tmpl   done done   この状態でmod_perlから起動してみましょう。10プロセスだけ StartServers 10 MaxClients 10 MaxRequestsPerChild 0 <VirtualHost *:80>   ServerName localhost:80   PerlModule mod_perl2   PerlModule TestApp   <Location />     SetHandler perl-script     PerlResponseHandler TestApp     Order allow,deny     Allow from all   </Location> </VirtualHost>   さて,アクセスしてみましょう。全部のプロセスにアクセスが回るよう,ab2 -k -c 10 -n 20で。(-n20なのは念のため)URLを変えながらアクセスし,合わせて/smaps_gpにアクセスしてデータを取りました。 見てのとおり,読み込まれるテンプレートが増えるにしたがってprivate_dirtyが増えていきます。privateなので子プロセス1つずつで使用しているメモリ領域です。(Catalyst::View::HTML::Templateではcache=>1がデフォルト,HTML::Templateではキャッシュサイズの制限が無いため増え続けるようになってます)テンプレートに動的な要素を含んでいないため,ほぼファイルサイズ=メモリ消費量になっていますね(約10MB)。これは1プロセスあたりなので,今回の設定であればトータルではその10倍,100MBのメモリが消費されています。 さて,この10MBを共有領域に置けないでしょうか。共有に置くには,forkする前にテンプレートをロードしてしまえば良い訳です。とりあえず手抜きでV::HTでいきなりテンプレートをロードしてしまうようにしてみましょう。 package TestApp::V::HT; use strict; use base 'Catalyst::View::HTML::Template'; use NEXT; __PACKAGE__->config(   die_on_bad_params => 0,   cache => 1, #  cache_debug => 1, ); for(0..999){   my $t = HTML::Template->new(     filename=>"$_.tmpl",     path => ['/home/kounoike/TestApp/root', '/home/kounoike/TestApp/root/base'],     %{__PACKAGE__->config} ); } 1;   注意することは,H::Tに与えるオプションでpathの値が違っていると,キャッシュキーが変わり(HTML::Template:: _cache_keyを読むと分かります),キャッシュヒットにならなくなってしまいます。(これでしばらくはまりました) さて,この状態で同じくリクエストを回してみましょう。 この通り,10MBのテンプレートキャッシュは最初から読み込まれています。しかし,すべてがshared_dirtyにいます。これならapacheの子プロセスがいくつあっても10MBしか使いません。 しかし,HTML::Templateのcache=>1では,テンプレートファイルの修正時刻を見て,キャッシュより新しいときは読み込みなおすようになっています。読み込みなおす=メモリを書き換える,です。 全部のテンプレートファイルをtouchしてもう一度リクエストを回してみましょう。 ご覧の通り,sharedだったテンプレートキャッシュがどんどんprivateに移っています。Copy On Writeが発生しているのです。こうなると同じテンプレートファイルなのに子プロセスごとにメモリを確保して,10MB×子プロセス数のメモリを消費してしまいます。 この状況では,apacheのMaxRequestsPerChildを設定すると,ある程度メモリのprivate化を抑えた「ように見せかける」ことになります。 一定数のアクセスがある度に子プロセスが再起動して(親プロセスのページテーブルをコピーし),共有領域に戻しています。しかし,共有されているキャッシュは親プロセスの持っているキャッシュで,このキャッシュは全く更新されていません。したがって,次にアクセスがあったときには「必ず」テンプレートファイルのリロード(とメモリのコピー)が発生します。対策としては,親プロセスを再起動するのが一番簡単でしょう。(mod_perlで親プロセスで実行されるハンドラがあるのなら,そこで無理やりリロードすれば良いのですが・・・そういうハンドラが見つかりませんでした)H::Tのオプションとしてblind_cache=>1を与えることで,修正時刻を読まないようにし,親プロセスが再起動するまで古いテンプレートを使い続けることもできます。 MaxRequestsPerChildはハンドラ内の処理で仕方なく書き換わってしまうデータのリフレッシュには役に立ちますが,状況によっては問題を不明瞭にしてしまうこともあるので注意が必要です。 おまけ。 TT2じゃなくHTML::Templateを使ったのはキャッシュ機構の違いが理由です。TT2に比べ,H::Tはキャッシュ機構が簡単で(%H::T::CACHEに入れてるだけ)ありながら,blind_cacheなどのキャッシュの制御機能があります。 はてなブックマーク - naoyaのブックマーク/ 2006年02月12日でMaxRequestsPerChildのネタを先に書かれてしまったので,逆にこの設定がうまくいかなくなる例を指摘してみました。もちろん,状況によっては子プロセスの再起動がベターな解になることもあるでしょう。Copy On Writeを意識することで,再起動するべきプロセスが親なのか,子なのか判断できるようになると思います。

Trackback URL for this post:

http://old.typemiss.net/trackback/65