TT2はCopy On Writeと相性が悪い

| | |
CoWでテンプレートキャッシュを共有!(C::V::H::T編) | Typemiss.netではHTML::Templateのテンプレートキャッシュを共有させてメモリを節約する話を書きましたが,同じことをCatalyst::View::TTでやろうとするとどうなるか,です。 TT2はH::Tに比べて内部が複雑で,普通に読み込ませておくだけではうまく共有することができませんでした。色々小細工をしてみたものの,copy on writeの発生を防ぐことができず,どうやっても子プロセスごとにメモリを用意する必要があるようです。 まずコントローラは同様に。 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 ) = @_;     my $no = $c->request->arguments->[0] || '0';     $c->stash->{template} = "$no.tt"; } sub pid : Local {     my ( $self, $c ) = @_;     $c->response->content_type('text/plain');     $c->response->body("$$\n"); } 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."\n" ); } 1;   この状態におけるメモリ消費状況の変化です。前回と同じく約10KBのテンプレート1000個を使っています。1000リクエストが終わり10MBのテンプレートが読み込まれると,private_dirtyが25MB増加しています。H::Tに比べてオーバーヘッドが大きいですね。 ビューでH::Tのときと同じようにテンプレートを読み込ませておきましょう。H::Tではパッケージ変数でキャッシュしているので,適当に読ませるだけで良かったのですが,TTではインスタンス変数にキャッシュが入っています。C::V::TTが使うインスタンスを取ってきて,そこに読み込ませる必要がありますのでnewをオーバーライドしました。$tmpl->processでテンプレートの処理まで行わず,$tmpl->context->templateで読み込みのみにとどめても良いのですが,とりあえず試しにprocessにて。 package TestApp::V::TT; use strict; use base 'Catalyst::View::TT'; use NEXT; sub new {   my $self = shift;   $self = $self->NEXT::new(@_);   my $tmpl = $self->template;   for(0..999){     $tmpl->process("$_.tt",{},\$out,{}) or die $tmpl->error();   }   return $self; } 1;   これでH::Tのときと同じくfork前に全部のテンプレートを読み込むはずです。この状態でメモリ変化がどうなるかを見ると下図のようになりました。キャッシュに使用して(できれば読み込みだけであって書き込みはしないで欲しいなと期待する)メモリ領域のうち半分くらいがprivateに,つまりページ内に書き込みアクセスがありcopy on writeが発生している様子です。 sub default : Private {     my ( $self, $c ) = @_;     my $no = $c->request->arguments->[0] || '0';     my $ctx = $c->component('V::TT')->template->context;     my ($d,$e);     $d = $ctx->template("$no.tt");     #$ctx->process($d, {});     $c->response->body("$no:".length($d)); }   これで各リクエスト時の処理が「テンプレートを読み込み,処理する」の2段階から「テンプレートを読み込む」だけになりました。このときの様子を見ると,ここではメモリコピーは起こっていません。コメントアウトされているcontext->processを呼ぶとメモリコピーが発生しました。 Template::Context::processから呼び出されるメソッドの中で,テンプレートの再帰読み込み防止のフラグとして$document->{_HOT}=1としていたり,テンプレート内でcomponent.callerによって呼び出し元を参照できるようにしているなど,キャッシュされていたTemplate::Documentオブジェクトへ操作を加えているからでしょうか。Copy On Writeはページを単位として管理されているため,4KBのページの中の1バイトでも書き換えられると4KBが丸ごとコピーされます。 で,上で見つけた代入処理などをとりあえず削ってみたTemplate::{Document,Context}などを作成してみたり,Template::ProviderでキャッシュしたTemplate::Documentオブジェクトをコピーしてから渡すようにしてみたりしたのですが・・・メモリコピーを無くすことはできませんでした。Template::ProviderのLRUキャッシュの操作なども削ったのですが(むしろ,これがあるのになぜtemplate()の呼び出しだけではコピーが発生しないのか謎),どこかにまだ書き込み処理が残っているのでしょう・・・もしかしたらテンポラリのオブジェクトがキャッシュされているテンプレートの前後に確保されて,その領域が書き換わっている? もうちょっと調べればどうにかできるのかも知れませんが良く分からないのでとりあえずここまで,とします。 ということで,大雑把なまとめです。
  • TT2を普通に使うとテンプレートサイズ×2.5倍(テンプレートにもよりけり?)が各プロセスごとに消費される。
  • fork前にキャッシュさせようとしても,テンプレート処理時にコピーが起こり,ある程度のサイズのメモリが各プロセス固有のものとなってしまう
  • 状況にもよるが,キャッシュを切って,COMPILE_DIRを指定してコンパイルしたテンプレートファイルを読み込ませるという方法も考えられる。(メモリに空きを作っておけばOS側のバッファに期待できるので)

Trackback URL for this post:

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