worker MPM における mod_perl のグローバル変数

イマイチ worker MPM で mod_perl が安全に使える自信がなかったので確かめてみました。題して「worker MPM でのグローバル変数は Thread Local Storage か Process Global Storage か?」

まず,prefork MPM での httpd.conf から書くと,

ServerLimit 2
MaxClients 2
StartServers 2
MinSpareServers 1
MaxSpareServers 0
MaxRequestsPerChild 0

こんな感じで,これと等価かな?と思う worker MPM の conf は

ServerLimit 1
ThreadLimit 2
MaxClients 2
StartServers 1
MinSpareThreads 1
MaxSpareThreads 2
ThreadsPerChild 2
MaxRequestsPerChild 0

としました(間違いがあったら教えてください)。つまり,前者は2プロセスをずっと使い回す設定,後者は1プロセス内2スレッドをずっと使い回す設定となります。

で,RequestHandler として

package Sandbox::App::Handler;

... (use Apache2::〜 あたりは略) ...

use threads;
use Sandbox::App::Storage;

sub handler : method {
  my ($class, $r) = @_;
  $r->content_type('text/plain');
  $r->print(
    sprintf('%s(%d:%d): %5d',
      'sandbox',
      $$,
      threads->tid(),
      Sandbox::App::Storage::counter(),
    )
  );
  return Apache2::Const::OK;
}

1;

と,単純に text/plain でカウンタを出力するハンドラとしました。カウンタ部分のモジュールは,

package Sandbox::App::Storage;

my $counter = 0;
sub counter {
  return ++ $counter;
}

1;

こんな感じ。わざわざ分けるまでもないんですがちょっと思うところあってわけました。

実行すると,

sandbox(24609:0):     1

みたく,プロセス ID とスレッド ID,そしてカウンタが出力されます。

まずは prefork MPM で

% ab -n 1000 -c 2 http://localhost/sandbox/app1

と,同時接続数 2 で合計 1000 回のアタック(嘘)をかけてやります。その後普通にブラウザでアクセスすると,

sandbox(24609:0):     501

のようになります。これは理想的に 2 つのプロセスで 1000 リクエストを分け合った場合の話で,運が悪いと 300:700 くらいにわかれたりもします(でも 500:500 にきちんとわかれることが多いですよ)。

ではもう一方のプロセスのカウンタを確認してみましょう…ということで何回かリロードすると,プロセス番号が違う出力が得られます。

sandbox(24610:0):     501

足して約 1000 になるので(余り 2 回分はブラウザからアクセスした結果)うまくいったことがわかります。

さて,次は worker MPM の番です。Fedora の場合,まず service httpd stop してから /etc/sysconfig/httpd の内容を書き換えて httpd.worker バージョンにしてやります。あとは同じ手順で実験を繰り返します。

最後にブラウザで確認すると,

sandbox(24718:0):     501

となりました。ところがここで困ったことが。別スレッドの結果を見ようと思ってリロードを繰り返しても同じスレッドの結果が表示され続けるんですね。仕方がないので iframe を使って 2 枚以上同じページにつめこんでみても…うまくいかない。じゃあということで IE6 でこの iframe バージョンをみてみたら…うまく両スレッドの結果をみることができました(今まで Firefox 1.5 使っていたんです)。なぜブラウザによって変わるのかは謎ですが面白い現象ですね。

ともかく結論として,

  • worker MPM では変数は Thread Local Storage に格納される
  • つまり prefork とほぼ同じ感覚

って実は perldoc の threads::shared を読むとそもそも「By default, variables are private to each thread」って書いてあるじゃん。というお話でした。

あとで読む

2 つめのドキュメントを読むとわかりますが,threaded mod_perl だと,Apache のように,perl インタプリタプールを用意してそこから持ってくるという形になっています。けっこう要注意ポイント。

ころんでもただでは起きたくない

さて,そもそも threaded Perl の変数が TLS に格納されるわけですが,逆に同一プロセスの異スレッド間で変数を共有するにはどうすればいいのか。上記3番目のリンクにも書いてありますが,threads::shared を使えばよろしい。

ということで,先ほどの Sandbox::App::Storage を書き換えてやります。

package Sandbox::App::Storage;

use threads;
use threads::shared;

my $counter :shared = 0;
sub counter {
  return ++ $counter;
}

1;

この実行結果は,

sandbox(24837:0):     514
sandbox(24837:1):     488

見事共有されませんでした(涙)。

さて,なぜうまくいかなかったのでしょう?答えは明日以降*1

*1:綺麗な説明が思いつかないだけだったりします