"PHP Fatal error: Allowed memory size of xxx" で落ちる前にできること

PHP Advent Calendar 2013 - 7日目

昨日は@hidenorigotoさんのBeyond MVCでした。

今日は PHPあるある的な問題に対する対策について少しばかり書きたいと思います。

※ 歯ブラシを持たせてみた *1

メモリ不足でよくやる対応

PHPで大量のデータを扱ったりすると、設定された利用可能な最大メモリ使用量を超えて

PHP Fatal error:  Allowed memory size of 524288330 bytes exhausted (tried to allocate 351 bytes) in ....

というエラーが発生して対応に追われたことの経験がある人はかなり多いと思います。
そもそも環境のメモリに余裕があるならmemory_limitを調整してメモリの使用量を増やしたり
あまり余裕がない場合は暫定的対応として、該当処理部分だけ

<?php
...
ini_set('memory_limit', '512M');
...

としたり、バッチ処理だったら

$php -d "memory_limit=512M" hoge.php

としたりしますよね。

今日のお話はメモリ不足エラーになる前にできることがあるよねというお話です。

嫌な兆候を事前に察知

突然データが莫大になりメモリ不足になることもあるでしょうが、事前に使用メモリ量が増大して来るケースも多々あります。
つまり、エラーを起こしてしまう前に "最大メモリ量のxx%を超えたらログに残す" ように仕込めば慌てずに済みますよね。
とはいえ、すでに稼働しているアプリでこのような仕込みを行うためにはどうしたらよいでしょう?

フレームワークのフックポイントを利用する

Webアプリケーションの何かしらのフレームワークを使っているなら、大抵処理を終えた後に何か仕込むことができるようになっていると思います。Symfony2 だと Event Listener があるので、`kernel.response` にメモリ使用量をチェックしてログに吐くようなものを仕込んでおけばOKですね。DIでロガーを渡せば簡単に好きなロガーで書き込みもOK。

<?php
...
    public function checkPeakMemory()
    {
        // 512M のように M で指定されている前提なのでアレでごめんなさい
        list($max) = sscanf(ini_get('memory_limit'), '%dM');
        $peak = memory_get_peak_usage(true) / 1024 / 1024;
        $used = ((int) $max !== 0)? round((int) $peak / (int) $max * 100, 2): '--';
        if ($used > 85) {
            $this->logger->warning(sprintf('Memory peak usage warning: %s %% used. (max: %sM, now: %sM)', $used, $max, $peak));
        }
    }

CakePHP なら Event Manager で同じようなことができるんじゃないでしょうか(未確認)

レガシーなアプリなんだけど

継ぎはぎされた素晴らしい実績のあるレガシーコードでそんな仕組みなどない!手を入れづらい!というケースもあるでしょう。

が、PHPには auto_prepend_fileというphp.ini ディレクティブの設定があり、これで上記と同じようなコードを書いてregister_shutdown_functionで指定しておけばアプリケーションの開始時にファイルが読み込まれ、スクリプト処理が完了したとき、あるいは exit() がコールされたときに登録したメソッドが呼ばれます。
ここで ログを残すであればerror_logでざっくりと指定すれば楽ですね。

この方法のメリットは既存のコードに一切修正を加えずにアプリの処理終了時にもれなく処理を練り込むことができるという点です。今回のような使い方以外でも色々と便利な利用シーンがあると思います。

<?php
register_shutdown_function(function(){
    // 512M のように M で指定されている前提なのでアレでごめんなさい
    list($max) = sscanf(ini_get('memory_limit'), '%dM');
    $peak = memory_get_peak_usage(true) / 1024 / 1024;
    $used = ((int) $max !== 0)? round((int) $peak / (int) $max * 100, 2): '--';
    if ($used > 85) {
        $message = sprintf("[%s] Memory peak usage warning: %s %% used. (max: %sM, now: %sM)\n", date('Y-m-d H:i:s', $_SERVER['REQUEST_TIME']), $used, $max, $peak);
        error_log($message, 3, "/var/tmp/my-errors.log");
    }
});

もし、しきい値を超えた場合は

[2013-12-07 00:39:12] Memory peak usage warning: 87.5 % used. (max: 128M, now: 112.75M)

みたいにログに残すことができます。

横展開

他にも同様のパターンとして "Fatal error: Maximum execution time of XX seconds exceeded" も仕込んでおけますね。実際自分はメモリと実行時間の両方にしきい値を設けてしきい値を超えたらログに残すようにしています。

監視方法での注意

このとき、メールで通知とかしたくなる人もいらっしゃるかも知れませんが、運用しているサービスの特性によってはメールが飛び過ぎて簡単に死ぬのでエラーの監視は別のレイヤーで行うようにしたほうが幸せです。

さて、明日は@makiesさんです。お楽しみに!