PHP-FPMのプロセス数・メモリをチューニングする

※本ページには広告(アフィリエイトプログラム等)が含まれます。

さくらのVPSでLaravelやWordPressを運用していると、アクセスが増えたわけでもないのにサイトが急に重くなる、あるいは 502 Bad Gateway が出るという相談をよく見る。原因の大半はPHP-FPMのプロセス設定がメモリに対して過大(または過小)なことにある。ここでは pm.max_children を中心に、実測ベースでチューニングする手順をまとめる。

設定ファイルの場所

Debian 12/13でPHP 8.3を使っている場合、プールごとの設定は以下にある。

/etc/php/8.3/fpm/pool.d/www.conf

複数サイトを1台のVPSに同居させているなら、サイトごとに www.conf をコピーして別プール(別ソケット・別ユーザー)にするのが定石。ここでは1プールを前提に話す。

pm = dynamic / static / ondemand の違い

pm.conf 冒頭で決める pm の値によってプロセスの増減方針が変わる。

  • static: 常に pm.max_children の数だけプロセスを起動しっぱなし。応答は速いが、アクセスがなくてもメモリを食い続ける。
  • dynamic: 最低限のプロセスを常駐させつつ、負荷に応じて増減する。設定項目が多いがバランスが良い。
  • ondemand: アクセスがない時はプロセスをゼロまで落とす。メモリは節約できるが、久しぶりのリクエストでプロセス起動のオーバーヘッドが数十ms乗る。

メモリ1〜2GBクラスの小さいVPSで、常時アクセスがそこまで多くないなら ondemand か控えめな dynamic が現実的。逆に常時それなりのトラフィックがあるなら dynamic で最低限のプロセスを温めておいた方が体感速度は良い。

pm.max_children の決め方

ここが一番事故りやすい。理屈はシンプルで、

pm.max_children = (利用可能メモリ - 他プロセスの使用分) ÷ 1プロセスあたりの平均メモリ

という割り算。問題は「1プロセスあたりの平均メモリ」を勘で決めてしまうこと。必ず実測する。稼働中のFPMプロセスのRSSを見るには次のコマンドを使う。

ps --no-headers -o rss,cmd -C php-fpm8.3 | awk '{sum+=$1; count++} END {print sum/count/1024 " MB average, " count " processes"}'

Laravelでキャッシュ系の処理が重いアプリだと1プロセス40〜80MB程度になることが多い。WordPressの素の状態なら20〜40MB程度で収まることもある。この数字はプラグインやパッケージ次第で大きく変わるので、必ず自分の環境で測る。「他サイトのブログに書いてあった数値」をそのまま使うのは避けたほうがいい。

dynamic時のstart_servers / min_spare_servers / max_spare_servers

pm = dynamic を選んだ場合、以下3つも合わせて設定する。

pm.start_servers = 2
pm.min_spare_servers = 2
pm.max_spare_servers = 6

start_servers はFPM起動時に作るプロセス数。min_spare_servers はアイドル状態でも維持する最低プロセス数、max_spare_servers はアイドルプロセスの上限で、これを超えるとFPMが自動的にプロセスを間引く。start_servers は目安として min_spare_servers + (max_spare_servers - min_spare_servers) / 2 あたりに置くのが公式ドキュメントの推奨。

pm.max_requests でメモリリーク対策

PHPの拡張やライブラリのバグでじわじわメモリが増えていくケースは珍しくない。pm.max_requests を設定しておくと、指定回数リクエストを処理したプロセスを強制的に再生成してくれるので、メモリリークの影響を一定範囲に抑えられる。

pm.max_requests = 500

0(無制限)がデフォルトだが、小規模VPSでは数百程度にしておくと安心材料になる。

listen: unixソケット vs TCP

nginxとPHP-FPMが同一サーバー上にあるなら、unixソケットの方がTCPよりわずかにオーバーヘッドが小さい(ポート経由のTCPスタックを経由しない分)。

listen = /run/php/php8.3-fpm.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660

nginx側は対応するsite confで以下のように書く。

location ~ \.php$ {
    fastcgi_pass unix:/run/php/php8.3-fpm.sock;
    fastcgi_index index.php;
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}

TCP(listen = 127.0.0.1:9000)にするのは、FPMとnginxを別サーバーに分離したい場合くらい。1台構成なら素直にソケットでよい。

request_terminate_timeout とslowlog

特定のリクエストだけ異様に時間がかかってプロセスを占有し続けることがある。request_terminate_timeout で強制終了までの秒数を、slowlog でそのリクエストの内容(どの関数で詰まったか)を記録できる。

request_terminate_timeout = 60s
slowlog = /var/log/php-fpm/www-slow.log
request_slowlog_timeout = 5s

request_slowlog_timeout を超えたリクエストのバックトレースがslowlogに出るので、「どこか分からないが重い」を潰す時にまず見る場所になる。

pm.status_path でステータス確認

今どれだけプロセスが埋まっているか、待ち行列(listen queue)が溜まっていないかを見るには pm.status_path を有効にする。

pm.status_path = /fpm-status

nginx側でこのパスを外部公開しないよう allow 127.0.0.1; deny all; で絞った上で、

curl http://127.0.0.1/fpm-status

を叩くと active processes や max children reached の累積回数が見える。max children reached が増え続けているなら、pm.max_children が実際のトラフィックに対して不足しているサイン。

メモリ2GBのVPSでの計算例

具体例として、メモリ2GB(2048MB)のさくらVPSでLaravelを1サイト動かすケースを考える。

  • OS・nginx・MariaDB・sshdなど常駐プロセスでおよそ600MB使用(実測で必ず確認)
  • スワップに頼りたくないので安全マージンとして300MBを残す
  • PHP-FPMに割り当てられるのは 2048 – 600 – 300 = 1148MB 程度
  • ps で実測した1プロセスあたりRSSが約60MBだった場合、1148 ÷ 60 ≈ 19

この場合 pm.max_children = 19 あたりが上限の目安になる。余裕を見て18くらいに切り詰めておいてもいい。pm.max_spare_servers はこの値以下、pm.min_spare_servers は2〜4程度、pm.start_servers はその中間で設定する。

設定の反映

設定を変更したら再起動して反映する。

php-fpm8.3 -t
systemctl restart php8.3-fpm

-t で構文チェックしてから再起動する癖をつけておくと、設定ミスでFPM自体が起動しなくなる事故を防げる。

まとめ

PHP-FPMのチューニングは「なんとなく大きめの数値にしておく」が一番危ない。メモリを使い切ってOOM Killerに他のプロセスごと落とされるより、pm.max_children を絞って max children reached が増えたら初めて増強する、くらいの慎重さで十分。まずは ps で実測、それから割り算、が結局一番早い。

読んで頂いて有り難うございます!