Go for it!

モーターサイクルと自転車とキャンプの日々。

PHPのメモリ割当量について

仕事関係の話でちょっと愚痴っぽいのですが、PHPメモリ割当量について。

下回り(asm, c, c++)を経験しているエンジニアはメモリ割当量は自分で管理するものと考えているはずですが、Lightweight Language専門のアプリケーションエンジニアはメモリ割当量をあまり気にしないようです…。

PHPメモリ制限の設定

PHPは、php.ini内のmemory_limitで設定された値までメモリを使用することが出来ます。例えば以下の設定であれば、実行中のPHPプロセスで2MBまでメモリを使用することが出来ます。

memory_limit=2M

PHP実行中にmemory_limitへ達してしまった場合、FATALエラーを出力して処理を停止します。PHPがapache moduleとして実行されている場合、以下の理由でmemory_limitの上限値を大きくしたくありません。

  • FATALエラーは例外ではないためトラップ出来ない(クライアントへレスポンス出来ない)
  • Apacheはブラウザからのアクセスを非同期で受け付ける(Apache側でクライアント接続タイミングを制御できない)
  • クライアント接続数は最大でMaxClientsの設定値まで増加する
  • クライアント1接続当たりApacheが1プロセス動作する(Linux版PHPではthreadが使えないのでforkする)

これが何を意味しているかというと、php.iniが正しく設定されない場合、Apacheの実行プロセスが使用するメモリ量が、サーバに搭載された物理メモリ量を超えてしまうことがあります。

PHPが使用する最大メモリ量の計算

PHPが使用する最大メモリ量は以下の式で簡単に推計することが出来ます。

memory_limitの値 x httpd.confのMaxClientsの値 = メモリ最大使用量

memory_limitが16M, httpd.conf中のMaxClientsが100として計算すると、メモリ最大使用量は1.6GBとなります。以下が式です。

16MB x 100 = 1600 ≒ 1.6GB

kernelや他のサブシステム(PHP以外のapache module, SMTPサーバやOpenSSHサーバなど)も同様にメモリを使用するので、サーバには最低でも2GB程度の物理メモリを搭載する必要があるでしょう。

搭載された物理メモリ量を超えた場合にどのような動作をするか?

近代的なOperating Systemには仮想メモリ機能があります。いわゆるスワップと呼ばれるもので、プロセスは物理メモリ量+スワップまでメモリを使用することが出来ます。物理メモリが2GB、スワップが2GBの場合、プロセスは合計で4GBのメモリを扱うことが出来ます。

仮想メモリは便利な機能ですが、スワップが発生した際に実行速度が大幅に低下するという致命的な欠点があります。通常、仮想メモリはハードディスク内に保存されるため、物理メモリと仮想メモリを入れ替える際に長い時間が必要です。これは2GBのファイルをコピーしたら時間がかかるのと一緒です。

ウェブアプリケーション開発のヒント

よくある間違いは以下のようなものです。

  1. 似たような変数をたくさん作る
  2. 出力結果をすべてバッファリングする
  3. データベースのレコードセットなどを全て配列に加える

1番はプログラミングの初歩なので「使い回せる変数はちゃんと使い回しましょう」というだけです。

2番および3番は、ウェブアプリケーション開発に慣れたエンジニアでも犯しがちなミスです。

2番は必要な部分にのみバッファリングを施せば良いので、出力データ量が想定できないページではバッファリングすべきではありません。コンテンツの圧縮などについてはApacheのmod_gzipモジュールなどに任せることも可能なので、PHPの機能以外で実現できないか検討する価値があります。

3番はデータセットを一部ずつ出力することで解決できる場合があります。symfonyの場合、全ての出力データは一度sfWebResponseオブジェクトに格納されるため、大きなデータを出力しようとするとメモリを圧迫します。sfWebResponseを使用せずにクライアントへ情報を送信することで問題を回避できます。例えば次のようなサンプルコードを使えば、データベースの結果セットをそのままクライアントへ送信することが出来ます。

while ($rs->next())
{
  $row = $rs->getRow();
  printf("%s,%s,%s\r\n", $row['a'], $row['b'], $row['c']);
}
return sfView::NONE;

どうしても大きなメモリが必要な場合はどうするか?

100MB, 200MB … 2GBと必要なメモリ量が大きくなっていく場合はどうすれば良いでしょうか?バッチジョブとして処理するべきです。

PHPをコマンドラインから実行する(command line interface, 略してCLI)場合、apache moduleが使用するものとは別のphp.iniを利用することが出来ます。php-cli.iniなどのファイルを作成し、その中に必要なメモリ量を記述すれば問題を解決できます。

$ php -i php-cli.ini -f sample.php

職場の社内ブログに書いた内容ですが、有用そうなのでこっちにも書き込んでみました。