hideden.hatenablog.com

はてなぶろぐー。URLなげー。

mod_perlで親プロセスとのCopy on Writeな共有メモリを増やす方法。

大量にアクセスがあってMaxClientを大きく設定したい場合、Apacheの1プロセスあたりのメモリを少なくするのが重要。当然アプリ側で大きなライブラリを読み込まずに画像の変換はGearmandにタスクとして投げたりとかの工夫するのも有効だが、fork元になるApacheの親プロセスと子プロセス間でできる限りCopy on Writeな共有メモリを増やすのも有効。

その辺の詳しい仕組み等の話はnaoyaさんがd:id:naoya:20080212:1202830671でしてるのでそこを参考に。linuxカーネルまわりの仕組みって楽しいよね。会社の案件ではFreeBSDサーバーも結構あるんだけどFreeBSDではどうなるのかは知らない。

で、社内にXenなテストサーバーがあるのでまっさらな環境で色々実験してみた。・・・過程を全部書こうとしたら長くなったので省略。地味にpsしたり/proc/xxxxx/smaps眺めたりそんなのです。

親プロセス側のメモリにモジュールを持たせる方法

基本はとにかくstartup.plでuseして置くこと。apacheのconfigに

PerlRequire /path/to/startup.pl

等を記載しておき、その中で使用しているアプリケーションのモジュールを列挙する。業務ではSledgeを使用しているので、コントローラーやモデル等をひたすらuseする。

startup.pl

use strict;
use lib qw(/home/test/lib);

use TestApp::Pages ();
use TestApp::Pages::Root ();
use TestApp::Pages::Base ();
use TestApp::Data::Base ();
use TestApp::Data::Member ();
....(略

1;

こんな感じ。use Hoge ();と()を付け、関数のexportを抑止する。これだけでも結構共有メモリは増える。また、forkは親プロセスの複製になるので、こうやって事前にuseしてソースをコンパイル済みにしておくと子プロセス側でコンパイルするコストが節約でき、MaxRequestPerChildを小さめに設定してある&大量にアクセスがあり頻繁に子プロセスが再forkされるようなシステムだと負荷も軽くなるという効果もある。

・・・で、終わり。

だとつまらないので、これの問題点。

useはすべてのソースの実行前に読み込まれるからいいのだが、CGI.pmのように利用されるメソッドのみAUTOLOADで初回利用時にevalで生成している場合や、LWPのようにモジュールの使われ方によってrequireするサブモジュールを変えるもの、DBIのようにconnect時にドライバを読むようなものはこれでは初期化されないため、親プロセス側で共有されない。また、XSを利用しているモジュールはuseしただけでは.soが読み込まれないためこれまた共有されない。

いくつかのモジュールではmod_perlfastcgi用に初期化の方法を提供しているので素直にそれを使う。この辺の話はhttp://iandeth.dyndns.org/mt/ian/archives/000624.htmlがとても詳しいので参考にさせてもらう。

use DBI ();
DBI->install_driver("mysql");

use CGI;
CGI->compile(:cgi);

等。でも、

1. perldoc で文字列 'mod_perl' を検索してみる
2. モジュールのソースコードから、文字列 'MOD_PERL' を検索してみるれることがわかります。
3. それでも見つからないなら、startup.pl の中でメソッドを実行しておいちゃう

らしいのだが、1と2に該当するモジュールがほとんどない。。。でも全部実行するのも大変。。

動的にロードされるモジュールを調べる方法

全モジュールのソースを読む。だと半端無くきついので、Perlは一度読み込んだファイルを%INCに入れていくのを利用してみる。CPANを検索して見たところ、Module::Useとかもろその用途に使えそうなのがあるのだが、作成されたのがPerl5.6の頃らしく5.8では動かなかった。パッチを作ろうと思ったが常用するものでもない&数行で実現できるので自分で書いてみた。

INCdiff.pm

package INCdiff;
use strict;
use Apache::Constants qw(OK);
use vars qw(%init_module);

sub init { $init_module{$_}++ for keys %INC; }

sub handler {
    my @required = grep { !$init_module{$_} } keys %INC;
    warn "-------- module diff ---------";
    warn join "\n", @required;
    return OK;
}
1;

これだけ。まずこれをstartup.plの最下部で

use INCdiff;
INCdiff->init;

としておく。で、apacheのconfigに

MaxRequestsPerChild 100

PerlChildExitHandler INCdiff

と書いておく。PerlChildExitHandlerはMaxRequestsPerChild回アクセスを処理したapache子プロセスが死ぬときに呼ばれるHandlerなので、アクセスがさほど無いサーバーでMaxRequestsPerChildを大きく設定しているとなかなか表示されないかも。

後はtail -f でerror_logを見ながらユーザーがいっぱいアクセスしてくれるのを待つ。ab等のツールを使うときは全ページ・全機能がまんべんなく使われるようにしないとあまり意味がなくなる。テストサーバー等でやる場合、全モジュールが使用されるよう手動でアクセスしてからab等で残ったRequest数を消化すると楽。

しばらく待つと、startup.plの初期化が終わってからapacheの子プロセスが死ぬまでの間にロードされたモジュール(Compile済のTTテンプレなども含まれる)がずらずらと出るので適当に整形してstartup.plに追加していく。

某サービスだと、

Compress/Raw/Zlib.pm
Compress/Zlib.pm
Crypt/Blowfish.pm
DateTime/TimeZone/Asia/Tokyo.pm
DateTime/TimeZone/OlsonDB.pm
Encode/CJKConstants.pm
Encode/JP.pm
Encode/JP/JIS7.pm
File/GlobMapper.pm
HTML/HeadParser.pm
HTTP/Cookies.pm
HTTP/Cookies/Netscape.pm
HTTP/Headers/Util.pm
HTTP/Request/Common.pm
IO/Compress/Adapter/Deflate.pm
IO/Compress/Base.pm
IO/Compress/Base/Common.pm
IO/Compress/Gzip.pm
IO/Compress/Gzip/Constants.pm
IO/Compress/RawDeflate.pm
IO/Compress/Zlib/Extra.pm
IO/Select.pm
IO/Uncompress/Adapter/Inflate.pm
IO/Uncompress/Base.pm
IO/Uncompress/Gunzip.pm
IO/Uncompress/RawInflate.pm
LWP/Authen/Digest.pm
LWP/Protocol/http.pm
Net/HTTP.pm
Net/HTTP/Methods.pm
SOAP/Transport/HTTP.pm
Template/Plugin.pm
Template/Plugin/Date.pm
Template/Plugin/URL.pm
URI/_server.pm
URI/http.pm

等、結構な量が出力された。MaxClientsを多めに設定してあるサーバーでこの辺を全部useしておくと、全体でメモリ使用量が200MBほど少なくなった。XS系のモジュールの場合はこの方法ではわからないので、/proc/PROCESS_ID/smaps等を見ながら.soを見ていくと何がロードされているのかわかる。例えばApache::Requestの場合はstartup.plでnewするとエラーになってしまうため、

use Apache::Request ();
eval { Apache::Request->new(); };

等としてやればちゃんとi686-linux/auto/Apache/Request/Request.soがshared_dirtyになってる事が確認できた。これで大体70〜75%ほどが共有メモリになった。