WebプログラマのためのCopy On Write解説:mod_perl/FastCGIでメモリを節約する方法

| | | |
Perl:forkしたプロセス間でのメモリ領域の共有 (Link: 遅レス。 - Apache + mod_perl - MaxClients の値に注意) | Typemiss.netの後,LinuxのCopy On Writeについて調べてみました。 このエントリではLinuxのCopy On Writeの挙動を簡単に説明し,mod_perlやFastCGI環境の場合に,どういうことがおこっているのか,どうすればそのような環境でメモリを節約できるのかについて説明してみます。・・・あまり分かりやすくなっていないかもしれませんが。 そもそもの始まりは遅レス。 - Apache + mod_perl - MaxClients の値に注意から。mod_perlにおけるC10K problemでは子プロセス1つ辺りのメモリ消費量を,「Max_Process_Size - Shared_RAM_per_Child」として計算するということです。しかし,この「Shared_RAM_per_Child」って何でしょう? という話です。 (ちなみにこのスライドの更に元ネタはPractical mod_perl Chapter 10. Improving Performance with Shared Memory and Proper Forkingみたいですね。同じ式です。) 「共有されたメモリ」とは何か,その答えは「fork()におけるCopy On Writeの効果」です。詳しい解説はJF - The Linux Kernel 4. メモリ管理5. プロセスにあります。 が,ここではそこまで難しいことは考えず,「大体どんなものなのか」「どうすれば良いのか」に絞って説明します。 Perl(に限らずどんな言語でも)における変数やプログラムはすべてメモリに記憶されています。メモリには番地(アドレス)が付けられていて,CPUからはこの番地を使って読み書きを制御します。 しかし,近年のCPU/OSでは,仮想メモリという機構が使われています。通常のユーザプロセスの側では,実際の物理メモリの番地を使って読み書きをするのでは無く,プロセスごとの仮想メモリ空間を持ち,仮想アドレスから物理アドレスへの変換テーブルを経由してメモリにアクセスします。正確にはさらにスワップをはじめとして,その他色々な機能があるのですがここでは省略します。 仮想アドレスから物理アドレスへの変換テーブル(ページテーブル)の様子を簡単に図示するとこのようになっています。 perlのインタプリタ(水色)が読み込まれ,何かのモジュール(黄色)が読み込まれ,モジュールやスクリプトが使う変数(緑色)がメモリに確保されています。Linuxカーネルは物理メモリ上に実際にメモリを確保するとともに,このプロセスが使うアドレス変換表であるページテーブルを作成します。 Linuxは,"Copy On Write"により効率的なメモリ管理と高速なforkを提供しています。 これはどういうことかというと,「forkを行うときにメモリをコピーするのではなく,書き込みを行うときにコピーを行う」ということです。まず,forkしたときに何が起こるのかを説明します。先ほどの状態からforkが行われると,次の図のようになります。プロセス1がforkして,プロセス2が作成されました。 物理メモリ上の使用している領域をコピーするのではなく,親プロセスのページテーブルをコピーして子プロセスのページテーブルとしているのです。これにより,二つのプロセスで同じメモリ領域を参照しています。forkしたことによるメモリの消費は新しく作られたページテーブル(とその他のプロセス管理に必要な情報)だけなのです。これが「共有されたメモリ」の正体です。むしろ「forkではすべてのメモリが共有された状態から始まる」と言った方が良いかもしれません。 しかし,forkした直後では同一のメモリ内容で始まりますが,親と子で別々の処理が実行されるようになると,別々のメモリ領域が必要になってきます。そこで出てくるのが"Copy On Write"です。例えば,子プロセスの側で,forkする前に確保していた変数の値を書き換えた場合について図に示します。 プロセス2がページテーブルのエントリ「変数」に対応するアドレスに書き込みを行おうとすると,Linuxカーネルでは「この領域は共有されている。このままではまずい」と判断し,そのページのコピーを作り,ページテーブルの番地を書き換えます。つまり,この時点で初めて新たなメモリ領域が確保され,親と子で別々のメモリを持つようになるのです。 子プロセスの側で新たにメモリ領域を確保した場合は当然ながら,子プロセス専用に確保されます。モジュールであろうと変数の内容であろうと同じことです。 Linuxカーネルの側から見れば,Perlの「モジュール」と「変数」に区別はありません。どちらも単に「プロセスが要求してきたメモリ領域」です。Perlでは読み込み済みのモジュールが管理されているので普通に使っている分には一度読み込まれたモジュールのメモリ領域は共有されたままになりますが,Apache::Reloadなどのようにモジュールを読み込みなおしたりすると,同じくCopy On Writeでメモリが消費されるか,新たなメモリ領域を確保してそこへ読み込むことになります。(同じメモリ領域に読み込みなおすのは難しそうなので,多分後者になるのかな? と思いますが) さて,mod_perlでどうすれば良いかを考えるにあたって,子プロセスを複数起動したときのことを考えてみましょう。 いくつ子プロセスをforkしようと,forkする前に確保したメモリは共有されます。 しかし,子プロセスがforkしてからモジュールをロードしたり,変数領域を確保すると,子プロセス数分のメモリを確保しなくてはなりません。 長々と書いてきましたが,「forkするときのCopy On Writeの効果」は前回のエントリにも書いたように,非常に簡単なのです。
  1. forkする前に確保したメモリ領域は子プロセスとの間で共有される
  2. 共有されたメモリ領域に対して書き込みを行うと共有は解除(コピー)される
  3. forkした後に確保したメモリ領域は子プロセスごとに確保される(共有されない)
これだけのことなのです。重要なのは「forkする前」と「forkする後」で全然違うということですね。 では,mod_perlで「forkする前」とはどこでしょうか? きちんとソースを読んだり調査したりしてはいないのですが推測し,確認することは簡単です。そうです。startup.plなどの初期化スクリプトです。そして当然ながら,そのスクリプトを呼び出しているapacheの設定ファイルを解析している段階もforkの前です。メモリ節約の手段として,startup.plでuse/requireして,「モジュールのコードを共有領域に置く」ことが重要だということが良く分かると思います。 しかし,そのモジュールはuseなりrequireなりするだけで良いのでしょうか? そのモジュールは実行時に動的にrequireするようになっていませんか? あるいは,スクリプトから呼ばれたときのロードの高速化のために必要な機能だけをコンパイルするようになっていませんか? 第一段階として「モジュールがきちんとロードされているか」を確認するのは簡単です。startup.plとWebアプリケーションの中でそれぞれ%INCの中身を表示させて,両者を見比べれば良いのです。startup.plで読み込まれていないモジュールは子プロセスごとに読み込み,そのコードは共有されていないのです。 それでは,FastCGIではどうなるのでしょうか? FastCGIの「forkする前」とはどこでしょうか? 実は答えは「実行する方法・フレームワークによりけり」です。 まずはCatalyst Advent Calender - Day 17の設定例の最初にあるFastCGIServerディレクティブによりapacheから直接FastCGIプロセスを起動し,管理させた場合について見てみましょう。この設定をしたときのpstreeを見るとすぐに分かります。 init-+-4*[agetty]                                                  |-apache2-+-51*[apache2]                                      |         `-apache2---3*[testapp_fastcgi]                    |-cron                                                        |-dhcpcd                                                      |-events/0                                                    |-khelper                                                    |-ksoftirqd/0                                                |-kswapd0                                                    |-kthread-+-aio/0                                            |         |-ata/0                                            |         |-kacpid                                            |         |-kblockd/0                                        |         |-khubd                                            |         |-kseriod                                          |         |-2*[pdflush]                                      |         `-reiserfs/0                                        |-login---bash---pstree                                      |-metalog---metalog                                          |-sshd                                                        `-udevd                                                   一つのapache2を親として,3つのFastCGIプロセスが走っています。つまり,forkの呼び出し元はapacheなのです。これではモジュールの共有がまったくできません。すべてのモジュールはプロセスごとに読み込まれます。 一方で下の設定例,FastCGIExternalServerを使った場合はどうでしょうか。(ちょっとさぼって,FastCGIプロセスの起動だけをしました) init-+-4*[agetty]                                              |-cron                                                    |-dhcpcd                                                  |-events/0                                                |-khelper                                                |-ksoftirqd/0                                            |-kswapd0                                                |-kthread-+-aio/0                                        |         |-ata/0                                        |         |-kacpid                                        |         |-kblockd/0                                    |         |-khubd                                        |         |-kseriod                                      |         |-2*[pdflush]                                  |         `-reiserfs/0                                    |-login---bash---pstree                                  |-metalog---metalog                                      |-sshd                                                    |-testapp_fastcgi---5*[testapp_fastcgi]                  `-udevd                                               一つのプロセスを親として,複数の子プロセスが起動されています。親プロセスは自分で呼び出したtestapp_fastcgi.plです。これならばどうにか工夫する余地があります。単純にやるなら,.plで色々useしておくことで共有されます。Catalystのお作法に従うなら各モジュールのsetupでしょうか?(未確認)

Trackback URL for this post:

http://old.typemiss.net/trackback/64
from iandeth. on 火, 2006-07-04 02:55
CPANモジュール: GTop GTop - Perl interface to libgtop perl.apache.org にある mod_perl パフォーマンスチューニングのドキュメントにて解説されていた、ある特定のモジュールを use した際のメモリ増加量を調べる方法。GTop っていうCPANモジュールを、こんな感じに使うことでお手軽に調べることができるようです: gtop.plx #!/usr/bin/perluse strict;use warnings;use GTop;my $gto