php-fpm max_children 警告:ワーカーを増やす前にログで原因を突き止める

php-fpm max_children の警告は、PHPのワーカープロセスがすべてリクエストを処理中で、新しいリクエストが待ち行列に入ったことを示すサインです。この警告が何週間も積み重なっていたNginx・Apache・php-fpm構成のWordPressサーバーで、ワーカーを埋めたリクエストをログから突き止めた結果、設定値を上げる代わりにnginxのレート制限で解決しました。
週次点検では警告が1日27〜187件記録されましたが、同じ期間に訪問者が受けた502・504エラーは4日間で0件でした。ワーカーを埋めたのは訪問者ではなく、管理画面のアドレスを繰り返し要求したボットと、存在しないパスを要求したスキャナーでした。
1️⃣ php-fpm max_children & seems busy
警告が1日66件たまると、ワーカーが66回足りなかったと読みがちです。php-fpmのログには性質の違う2つの警告が混ざっており、ほとんどは上限に達する前に記録される予備的な警告です。
[09-Oct-2026 00:16:18] WARNING: [pool www] seems busy (you may need to increase pm.start_servers, or pm.min/max_spare_servers), spawning 8 children, there are 0 idle, and 15 total children
[09-Oct-2026 13:02:05] WARNING: [pool www] server reached pm.max_children setting (26), consider raising it[seems busy]は待機中のワーカーが0になり新しいプロセスを生成中だという通知で、プロセス管理方式がdynamicならリクエストが少し増えただけでも記録されます。[server reached pm.max_children]は上限26に達してそれ以上生成できなかった瞬間で、ここから新しいリクエストが待たされます。
レストランで店員をもう1人呼ぶことと、席が埋まって客を外に立たせることの違いです。
1日分を種類別に集計した結果、seems busyが58件、max_childrenが6件でした。点検ルーチンは2つの警告を合算して66件と報告しましたが、実際に上限に達したのは6回です。
grep '09-Oct-2026' /var/log/php-fpm/error.log | grep 'pool www' | grep -c 'seems busy'58grep '09-Oct-2026' /var/log/php-fpm/error.log | grep 'max_children' | cut -c2-2109-Oct-2026 13:02:05
09-Oct-2026 13:52:10
09-Oct-2026 18:09:20
09-Oct-2026 18:42:17
09-Oct-2026 18:46:07
09-Oct-2026 19:12:442つ目のコマンドが出力する6つの時刻が原因追跡の基準になり、次の段階ではこの時刻だけを使います。
2️⃣ max_childrenの増設が答えではない理由
上限26を上げれば警告の件数は減るかもしれませんが、ワーカーを埋めたリクエストが何かは変わりません。
一般的な計算は、利用可能なメモリをワーカー1つの平均メモリで割る方法です。実測したサーバーではワーカー26個が平均131MB・最大214MBを使い合計3.4GBで、メモリ15.7GBのうち4.1GBが利用可能だったため、数字の上では30個以上追加で生成できます。
ps -o rss= -u www -C php-fpm | awk '{s+=$1; n++; if($1>m) m=$1} END{print n, s/n/1024, m/1024, s/1024}'26 130.579 214.27 3395.05free -m total used free shared buff/cache available
Mem: 15783 11658 397 340 4383 4124
Swap: 8191 1615 6576nproc41つ目の出力の4つの値はワーカー数、平均メモリ(MB)、最大メモリ(MB)、合計(MB)で、free出力のavailable列が利用可能なメモリです。
CPUが4コアなのでワーカー数を増やしても同時に処理されるリクエストは増えず、リクエスト1つにかかる時間が延びるだけです。訪問者が受けた502・504エラーが0件の状態では上限を上げても得るものはなく、ワーカーを占めていたボットとスキャナーにより多くのプロセスを割り当てることになります。
上限を上げる前に確認すべきは、警告が記録された時刻にワーカーを占めていたリクエストが訪問者かどうかで、それをaccess.logで確かめる手順を次の章にまとめました。
3️⃣ access.logで原因を追う:時刻、IP、パス、レスポンスコードの順
警告の時刻を1つ選べば、その1〜2分のaccess.logに答えがあります。手順は、時刻で行を抽出し、IPごとに集計し、上位IPのパスとレスポンスコードを確認することです。
grep '09/Oct/2026:18:09' /var/log/nginx/access.log | awk '{print $1}' | sort | uniq -c | sort -rn | head 115 140.245.x.x
61 187.15.x.x
17 3.36.x.x
8 91.239.x.x
4 79.127.x.x
3 109.248.x.x
3 109.248.x.x
3 109.248.x.x
2 79.127.x.x
2 40.77.x.xgrep '09/Oct/2026:18:09' /var/log/nginx/access.log | awk '{print $7, $9}' | sort | uniq -c | sort -rn | head 55 / 200
13 /wp-login.php 404
13 /wp-admin/plugins.php 302
13 /wp-admin/index.php 302
13 /wp-admin/edit.php 302
12 /wp-admin/profile.php 302
4 / 499
3 /xmlrpc.php 302
2 /xmlrpc.php 499
2 /wp-content/themes/kinsta/images/duos/small/wheelchair.svg 40410月9日18時09分の場合、1分間に286件が記録され、そのうち1つのIPが115件で、パスは/wp-admin/profile.phpのような管理画面のアドレス、レスポンスコードはすべて302でした。
レスポンスコードがPHPに届いたかどうかを表します。nginxがキャッシュ済みHTMLを直接返した200と444・400はPHPを通らず、302・404とキャッシュされていない200はワーカーを1つ使います。未ログインで管理画面のアドレスを要求すると、WordPressはログインページへの302を返し、その302ひとつがPHPの実行1回です。
1台のサーバーに2つ以上のサイトがあると、全体のaccess.logに両サイトの行が一緒に記録されます。最初はこのボットが1つ目のサイトを攻撃したと判断しましたが、サイト別のログと照合した結果、22,391件中22,390件が2つ目のサイトへのリクエストでした。
4️⃣ ワーカーを埋めた3種類のリクエスト:ボットのwp-admin、スキャナーの404、キャッシュのプリロード
同じ手順を警告が記録された時刻すべてに当てはめた結果、ワーカーを埋めたリクエストは3種類でした。
1つ目は管理画面のアドレスを繰り返し要求したボットです。1つのIPが3時間で22,391件を送り、そのうち9,944件が/wp-admin/profile.phpを含む4つのパスで、未ログインの302 1回がPHPの実行1回なので、9,944回ワーカーを使いました。
2つ目は存在しないパスを要求する脆弱性スキャナーです。User-AgentにGooglebotと書かれていてもGooglebotではなく、1日14,672件を送ったIPは逆引きDNSがgoogleusercontent.comで終わるGoogle Cloudの仮想サーバーで、Googlebotはgooglebot.comで終わります。同じIPがbingbot、Applebot、GPTBot、ClaudeBotを交互に使っていました。
.envのようなファイルへのリクエストはnginxが444で0.04秒で遮断しますが、/userfilesや/secrets.ymlのように遮断ルールにない普通のパスはWordPressが404ページを生成することになります。この404ページは266KB・1.4〜2.9秒で通常の記事生成と同じコストで、1つのIPが1日4,069回要求しました。
3つ目はサーバー自身です。記事や翻訳を保存するとキャッシュが空になり、WP Rocketのプリロードが全アドレスを再要求し、1日8,489件・毎分最大60件でした。Rank Mathのインデックス状態チェックもAction Schedulerを通じて1日754回ワーカーを1つ使いました。どちらも作業時間帯に集中し訪問者のエラーにはつながらなかったため、対処の対象から外しました。
下のシミュレーターで、各リクエストがどの層で終わり、PHPワーカーを使うかどうかを確認できます。
| リクエスト | 止まる層 | 応答 | ワーカー |
|---|---|---|---|
| ボットのwp-admin | WordPress | 302(ログインページへ) | 1つ使用 |
| スキャナー .env系 | nginxの遮断ルール | 444(0.04秒) | 使用しない |
| スキャナー 普通の偽パス | WordPress | 404ページ(266KB・1.4〜2.9秒) | 1つ使用 |
| キャッシュのプリロード | WordPress | 200・キャッシュファイル保存 | 1つ使用 |
| キャッシュ済み記事の訪問 | キャッシュファイル | 200(nginxがすぐ応答) | 使用しない |
| キャッシュされていない記事の訪問 | WordPress | 200・キャッシュファイル保存 | 1つ使用 |
5️⃣ nginxのレート制限:未ログインリクエストの制限と本当のIPの復元
遮断ではなくレート制限を選びました。パスを塞げばスキャナーはパスを変え、IPを塞げば翌日には別のIPが来るからです。未ログインでPHPまで届くリクエストだけをIPごとに毎分30件・瞬間60件に制限し、超過分には429で応答します。
CDNの背後にあるサーバーでは、この設定だけでは合いません。nginxが見るIPはCDNエッジのアドレスなので、そのエッジを通る訪問者全体が1つのIPにまとめられるためで、エッジのIP一覧をset_real_ip_fromで登録し、X-Forwarded-Forの本当のIPを先に復元する必要があります。ログインCookieのあるリクエストは免除し、User-Agentでは免除しません。
# httpコンテキスト
real_ip_header X-Forwarded-For;
real_ip_recursive on;
set_real_ip_from 203.0.113.0/24; # CDNエッジ一覧
map $http_cookie $php_anon_key {
default $binary_remote_addr;
~*wordpress_logged_in '';
}
limit_req_zone $php_anon_key zone=php_anon:10m rate=30r/m;
# PHPへ渡すlocation
location @apache {
limit_req zone=php_anon burst=60 nodelay;
limit_req_status 429;
proxy_pass http://127.0.0.1:8080;
}キャッシュ済みのページはtry_filesがnginxの中で応答するのでこのlocationには来ず、admin-ajax.phpは別のlocationなので制限に含まれません。
検証は同時リクエストで行います。順次リクエストは404 1つが1.5秒なので毎分30件を超えず制限にかかりません。20個ずつ同時に送って100件要求すると404が67件・429が33件となり、制限の動作を確認できます。
seq 1 100 | xargs -P 20 -I{} curl -s -o /dev/null -w '%{http_code}\n' https://example.com/zz-{} | sort | uniq -c出力に429が集計されれば制限は動作しています。サーバー自身のIPを免除している場合は、一時的に免除を解除して実行するか、別のネットワークから実行します。
6️⃣ 適用前後の比較
未ログインの管理画面アドレスの制限は10月9日夜、PHPに届くリクエストの制限は11日夜に適用しました。管理画面アドレスの302は9日の9,996件から10日11件、11日1件に減り、3時間で22,391件を送ったIPは翌日からリクエストが0件です。
スキャナー側は適用直後のため1日分の数字がなく、WordPressが生成した404ページ数(9日2,563・10日2,612・11日1,630)が来週の点検でどこまで下がるかで判断します。訪問者の502・504エラーは適用前後とも0件です。
この数字はNginx・Apache・php-fpmで運用するWordPressサーバーのログを直接集計した値で、警告の件数ではなく訪問者のエラーとPHPに届いたリクエスト数を基準に判断しました。
🔢 FAQ & おすすめコンテンツ
ℹ️ 提携について
当サイトのコンテンツにはアフィリエイトリンクが含まれています。訪問者がこのリンクを通じて商品やサービスを購入すると、当サイトは販売元から手数料を受け取ります。この過程において、購入者が支払う金額(イベント割引適用で金額が下がります↓)は上昇しません。掲載されている価格・割引・在庫情報は作成時点のものであり、実際とは異なる場合がありますので、ご購入前に販売元にて最終確認をお願いいたします。商品の選定および評価は独自の基準に基づいて行われており、手数料の支払いの有無は紹介順や評価内容に影響を与えません。