こんにちは。PHPの表層的な文法をマスターし、いざ大規模なトラフィックを捌こうとしたり、バッチ処理で巨大なデータを扱ったりしたとき、「なぜか突然プロセスが死ぬ」「OOM Killerに刈り取られる」といった壁にぶつかったことはありませんか?
他の言語、例えばGoやJavaであればメモリ管理の挙動をある程度コントロールしやすいですが、PHPは「1リクエスト=1プロセス(またはスレッド)」という独特のライフサイクルを持っています。そして、私たちが設定する `memory_limit` と、背後でうごめくOS(Linux)のメモリ管理機構――特に「オーバーコミット(Overcommit)」と「スワップ」の組み合わせは、Webシステムの生死を分ける隠れた重要ポイントです。
今回は、PHPの内部エンジンがメモリをどう要求し、OSがそれをどう裁いているのか。その深淵を一緒に覗いてみましょう。ここを理解すると、本番環境のチューニングで見えてくる景色がガラリと変わりますよ。
—
1. `memory_limit` は「PHPの財布」、OSメモリは「社会の信用」
私たちが `php.ini` で設定する `memory_limit = 256M`。
これを見ると、「PHPはこのサイズを超えたら即座にプロセスを自爆させるんだな」と思いがちですよね。半分正解ですが、半分は違います。
PHPのZendエンジンは、変数や配列、オブジェクトを作るたびに、内部アロケータ(`emalloc()`)を通じてOSにメモリを要求します。
Zendエンジン内部では、現在のリクエストが消費したメモリ量をバイト単位で厳密に追跡しており、`emalloc()` の総量が `memory_limit` を超えようとした瞬間に、あの有名なエラーを発生させます。
Fatal error: Allowed memory size of 268435456 bytes exhausted (tried to allocate 1048576 bytes) in …
おや、これなら安全そうに見えますよね?「PHPが自分で自分を制限してくれるなら、OSのメモリが枯渇することはないのでは?」と。
しかし、ここにしっぽを掴みにくい罠があります。実は、PHPの `memory_limit` は「OSから物理メモリをどれだけ奪うか」の直接的な制御にはなっていないのです。
—
2. Linuxの「オーバーコミット」がPHPを狂わせる理由
皆さんもご存知の通り、Linuxカーネルは非常に「太っ腹」です。プロセスが「○GBのメモリが欲しい!」と言ってきたとき、Linuxは実際にその分の物理メモリ(RAM)やスワップ領域が手元になくても、「よし、あげるよ!」と簡単に快諾してしまいます。これが メモリのオーバーコミット(Memory Overcommit) です。
Linuxのデフォルト設定(`vm.overcommit_memory = 0` または `1`)では、プロセスが要求した瞬間にはメモリを割り当てず、実際にそのメモリ領域に「書き込み(Write)」が発生したタイミングで初めて物理ページを割り当てる(Demand Paging)という戦略をとります。
ここで、PHPの裏側で何が起きているか想像してみてください。
1. 大量のレコードを一括取得するORMのクエリを実行したとします。
2. Zendエンジンは、巨大な配列やオブジェクトのツリーをメモリ上に構築しようと `emalloc()` を連打します。
3. この時点では、`memory_limit`(例: 512M)の枠内であれば、PHPはエラーを出さずに処理を続けます。
4. しかし、OS側から見ると、複数のPHP-FPMワーカーがそれぞれ「最大512MB使うかもしれない権利」を主張し、さらにMySQLやNginxもメモリを食っています。実際の物理メモリの総量を超えたメモリ割り当て約束(Commit)が、OS全体で行われている状態になります。
スワップアウトと「デス・スパイラル」の恐怖
物理メモリが足りなくなると、Linuxは慌てて スワップ(Swap) 領域――つまりHDDやSSDなどのストレージの一部をメモリの身代わりとして使い始めます。
ここで最悪の事態(デス・スパイラル)が起きます。
ストレージはRAMに比べて圧倒的に低速です。PHP-FPMのワーカーがスワップ領域に追い出され、再びCPUに戻って処理を続ける際、データの読み書きでディスクI/Oネックが発生します。
リクエストの処理速度が極端に落ちるため、Webサーバー(NginxやApache)には次々と新しいリクエストが溜まり、さらなるPHP-FPMワーカーが起動し、メモリ要求が爆発的に増加します。
そしてついに、物理メモリもスワップも完全に枯渇したとき、Linuxカーネルの最終防衛ラインが作動します。
—
3. OOM Killerの慈悲なき鉄槌
メモリが完全に行き詰まったとき、Linuxカーネルはシステム全体が完全にフリーズ(Kernel Panic)するのを防ぐため、OOM Killer(Out of Memory Killer)を発動させます。
OOM Killerは、どのプロセスを殺せば最も効率よくメモリを解放できるかを計算(`oom_score` の算出)し、最も迷惑をかけている大きなプロセス(多くの場合、メモリを食い潰したPHP-FPMの子プロセス)を容赦なく強制終了(SIGKILL)します。
PHP-FPMの子プロセスが前触れもなくOSに殺されると、Nginx側には以下のような残酷なレスポンスが返されます。
502 Bad Gateway
あるいは、途中でコネクションがプツリと切断されます。アプリケーションのエラーログには何も残らず、`dmesg` を叩いて初めて「Out of memory: Kill process … (php-fpm)」の文字を見つけて冷や汗をかく……。ベテランエンジニアなら一度は経験する悪夢ですね。
—
4. アーキテクトが実践すべき「真のメモリ防衛策」
では、このOSレベルの罠からPHPアプリケーションを守るには、どう設計し、どう設定すべきでしょうか。現場で即座に使える実践的なアプローチを共有します。
① `memory_limit` は「少なめ」に、そして「リクエストの性質で分離」する
すべてのバーチャルホストやプールで `memory_limit = 512M` のような雑な設定をしてはいけません。
APIのエンドポイントや軽量なCRUD処理であれば `-1` や `256M` は論外で、`128M` や `64M` で十分に動作するはずです。逆に、CSVインポートやPDF生成のような重い処理を行うエンドポイントは、FPMのプールを完全に分離し、専用のプールにのみ大きめの `memory_limit` を割り当ててください。
② `pm.max_requests` でメモリリークを物理的に封じ込める
PHPの拡張モジュール(C言語製のものなど)のバグや、極稀にある複雑な循環参照によるメモリリークにより、リクエストを重ねるごとにメモリ使用量がジワジワと増えていく現象(メモリリーク)が起きます。
これを防ぐため、`php-fpm.conf` の設定を見直しましょう。
; php-fpm.conf の設定例
pm = dynamic
pm.max_children = 50
pm.start_servers = 10
pm.min_spare_servers = 5
pm.max_spare_servers = 20
; ★極めて重要:一定回数を処理したらプロセスを安全に再起動させる
pm.max_requests = 500
`pm.max_requests = 500` と設定しておくと、1つのPHP-FPMワーカーが500回のリクエストを処理した時点で、自発的に静かにプロセスを終了し、マスタープロセスが新しい綺麗な(メモリがクリーンな)プロセスを立ち上げてくれます。これにより、メモリリークの累積による突然のOOM Killer死を防げます。
③ 巨大データは「舐めるように」処理する(カーソルとジェネレータ)
PHPでやってしまいがちな最大の罪は、数万件のレコードを一度に `PDO::fetchAll()` や `ORM::all()` でメモリ上に展開することです。
メモリ効率を極限まで高めるには、Zendエンジンのジェネレータ(`yield`)や、PDOのバッファなしクエリ(Unbuffered Query)を使い、メモリ上に常駐するオブジェクトの数を常に一定に保つ必要があります。
query(‘SELECT FROM huge_table’)->fetchAll();
// 良い例:ジェネレータを使って一行ずつストリーミング処理する
function yieldLargeData(PDO $db): \Generator {
$stmt = $db->query(‘SELECT FROM huge_table’);
// バッファなし(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => false)を前提とする
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
yield $row; // 1行分のメモリしか消費しない
}
}
foreach (yieldLargeData($db) as $user) {
// メモリを爆発させずに安全に処理を継続
processUser($user);
}
—
結びにかえて
PHPの `memory_limit` とOSのメモリ管理の関係を紐解くと、Webアプリケーションが単なる「コードの集合体」ではなく、OSという土台の上で動く「生きたプロセス」であることがよく分かりますよね。
「なぜこの設定値なのか」「OSは今、メモリをどう扱っているのか」。
この視点を持つだけで、あなたの書くコードはより堅牢になり、本番環境のトラブルに怯える夜はなくなります。
ここを深く理解したあなたなら、もう次のステージのアーキテクチャが見えているはずです。さあ、セキュアで高速なPHPの世界をさらに極めていきましょう。