はじめに
皆さん、こんにちは。
技術開発部のWです。
普段はWebエンジニアとしてフロントエンドやサーバーサイドの開発に携わっています。
最近、PHPのアプリケーションでメモリ超過エラーが発生する事象に遭遇しました。
このエラーは、Webサーバーがメモリを使い切ってしまい、処理が停止されてしまう現象です。
この記事では、PHPのメモリ超過問題の調査についてまとめます。
問題の概要
当社のプロダクトにてエクセル出力時に多くのセルへの出力を行う場合の調査時に予期しないメモリ使用量を消費し、最終的に「Allowed memory size of X bytes exhausted」というエラーが表示されました。
この問題が発生すると、処理が完了せず、システム全体のパフォーマンスが低下することがあります。
発生したエラー
PHP Fatal error: Allowed memory size of 536870912 bytes exhausted (tried to allocate 20480 bytes) in /path/to/script.php on line 123
原因の仮説
1.データの読み込み方法の可能性
データベースから一度に大量のレコードを取得している処理にてメモリを消費している可能性があります。
2.不要なオブジェクトが残っている可能性
処理が終了した後に不要なオブジェクトや配列がメモリに残っていることが、メモリの無駄遣いにつながっている可能性があります。
3.メモリ制限設定の可能性
PHPのmemory_limit
設定が原因である可能性もありますが、これについては根本的な解決策にはならないと考えました。
調査
1. PHP設定の確認
まず、PHPの設定を確認しました。PHPのメモリ制限 (memory_limit
) はphp.ini
で設定できます。
確認方法:
php -i | grep memory_limit
2. メモリ使用量の確認
ログ出力
計測用のログを仕込み、特にメモリ使用量が増加している処理を特定しました。また、パフォーマンスへの影響を調査するため、CPU使用率も計測して出力しています。
private function startLog($startMemory) { $loadAvg = sys_getloadavg(); // 1分、5分、15分平均のCPU負荷 $endMemory = memory_get_usage(); $memoryUsed = $endMemory - $startMemory; $memoryUsedMB = number_format($memoryUsed / 1024 / 1024, 2) . ' MB'; // メモリ使用量(MB単位) Log::info('start', [ 'memory_used' => $memoryUsedMB, // メモリ使用量 'memory_usage' => number_format($endMemory / 1024 / 1024, 2) . ' MB', // 終了時のメモリ使用量(MB単位) 'cpu_load_1m' => number_format($loadAvg[0], 2) . '%', // 1分間の平均CPU負荷 'cpu_load_5m' => number_format($loadAvg[1], 2) . '%', // 5分間の平均CPU負荷 'cpu_load_15m' => number_format($loadAvg[2], 2) . '%', // 15分間の平均CPU負荷 ]); } private function endLog($iniStartTime, $iniStartMemory) { // リクエスト終了時間を計測 $endTime = hrtime(true); $duration = $endTime - $iniStartTime; $duration_seconds = number_format($duration / 1e9, 6); $tmpSeconds = $duration / 1e9; // 時間、分、秒に変換 $hours = floor($tmpSeconds / 3600); $minutes = floor(($tmpSeconds % 3600) / 60); // 終了時のメモリ使用量を記録 $endMemory = memory_get_usage(); $memoryUsed = $endMemory - $iniStartMemory; $memoryUsedMB = number_format($memoryUsed / 1024 / 1024, 2) . ' MB'; // メモリ使用量(MB単位) // システムのCPU負荷を再度取得 $loadAvg = sys_getloadavg(); // 計測結果をログに出力 Log::info('end', [ 'duration' => "処理時間: " . $hours . "時間" . $minutes . "分" . $duration_seconds . " 秒", // フォーマットされたリクエスト処理時間(時間:分:秒) 'memory_used' => $memoryUsedMB, // メモリ使用量 'memory_usage' => number_format($endMemory / 1024 / 1024, 2) . ' MB', // 終了時のメモリ使用量(MB単位) 'cpu_load_1m' => number_format($loadAvg[0], 2) . '%', // 1分間の平均CPU負荷 'cpu_load_5m' => number_format($loadAvg[1], 2) . '%', // 5分間の平均CPU負荷 'cpu_load_15m' => number_format($loadAvg[2], 2) . '%', // 15分間の平均CPU負荷 ]); }
Laravelのログへ出力
$startTime = hrtime(true); $startMemory = memory_get_usage(); // 計測開始 $this->startLog($startMemory); // 特定の処理 // 計測終了 $this->endLog($startTime, $startMemory);
これをスクリプト内の複数の場所に挿入することで、メモリがどの時点で急激に増加しているかを追跡しました。
結果、SQLで大量のデータを一度に取得している箇所が、メモリ超過エラーの原因の大部分を占めていました。
ログ出力例
[2024-12-10 17:48:32] local.INFO: start {"memory_used":"0.00 MB","memory_usage":"131.99 MB","cpu_load_1m":"5.26%","cpu_load_5m":"5.56%","cpu_load_15m":"5.12%"} [2024-12-10 17:49:52] local.INFO: end {"duration":"処理時間: 0時間1分79.795247 秒","memory_used":"54.56 MB","memory_usage":"186.55 MB","cpu_load_1m":"5.38%","cpu_load_5m":"5.69%","cpu_load_15m":"5.20%"}
対処例
1. データの読み込み方法の改善
データベースから一度に大量のレコードを取得するのではなく、少しずつデータを取得して処理する方法に変更します。これにより、一度にメモリに読み込むデータ量を減らし、メモリ消費を抑制できます。具体的には、LIMIT句を使ってSQLクエリで一度に取得するレコード数を制限します。
$offset = (int) $offset; // 整数にキャストして不正な値を防ぐ $query = "SELECT * FROM users LIMIT 1000 OFFSET :offset"; $results = DB::select($query, ['offset' => $offset]);
2. 不要なオブジェクトの破棄
処理が終わった後に不要なオブジェクトや配列をunset()
で明示的に破棄し、メモリを解放します。
unset($largeObject);
3. メモリ制限設定の変更
根本的な解決にならないため行わない予定ですが、php.ini
でmemory_limit
を一時的に大きく設定し、メモリ超過エラーが発生しないようにします。
memory_limit = 512M
まとめ
今回の調査により、一部のSQLで大量のデータを一度に取得している箇所が、メモリ超過エラーの原因の大部分を占めることが判明したため、データの読み込み方法の改善に取り組むことになりました。
最初にデータ取得方法を見直し、その後メモリ管理の最適化を進める予定です。現在、解決策の実施にはまだ至っていませんが、これらの改善策を順次実行し、システムのパフォーマンス向上を目指して作業を進めていきます。
ところで、スパイダープラスでは仲間を募集中です。 スパイダープラスにちょっと興味が出てきたなという方がいらっしゃったらお気軽にご連絡ください。