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

公正取引委員会の広告文 Ver 3.1
公正取引委員会の広告文 Ver 3.2(モバイル専用)
テスト環境 RHEL系AlmaLinux 9.8rootアカウント2026-10-12 確認

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'
58
grep '09-Oct-2026' /var/log/php-fpm/error.log | grep 'max_children' | cut -c2-21
09-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:44

2つ目のコマンドが出力する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.05
free -m
               total        used        free      shared  buff/cache   available
Mem:           15783       11658         397         340        4383        4124
Swap:           8191        1615        6576
nproc
4

1つ目の出力の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.x
grep '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 404

10月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 管理画面説明用
🗂️ ワーカーを埋めたリクエスト
ボットのwp-adminリクエスト
未ログインで管理画面のアドレスを要求すると、WordPressがログインページへの302を返します。その302ひとつがPHPの実行1回です。
スキャナー:.env系
.envのようなファイルへのリクエストは、nginxの遮断ルールが444で接続を切ります。0.04秒で、PHPは通りません。
スキャナー:普通の偽パス
/userfilesや/secrets.ymlのように遮断ルールにない普通のパスは、WordPressが404ページを生成します。266KB・1.4〜2.9秒で、通常の記事生成と同じコストです。
キャッシュのプリロード
記事や翻訳を保存するとキャッシュが空になり、WP Rocketのプリロードが全アドレスを再要求します。毎分最大60件で、リクエストごとにワーカーを1つ使います。
キャッシュ済み記事の訪問
キャッシュファイルがあればnginxが直接応答し、PHPは通りません。
キャッシュされていない記事の訪問
キャッシュファイルがなければPHPがページを生成し、そのHTMLをキャッシュファイルとして保存します。
全体の流れ
リクエストは上から下へ下りていき、応答が終わる層で止まります。PHPワーカーを使うのはWordPressまで下りたリクエストだけです。
リクエスト止まる層応答ワーカー
ボットのwp-adminWordPress302(ログインページへ)1つ使用
スキャナー .env系nginxの遮断ルール444(0.04秒)使用しない
スキャナー 普通の偽パスWordPress404ページ(266KB・1.4〜2.9秒)1つ使用
キャッシュのプリロードWordPress200・キャッシュファイル保存1つ使用
キャッシュ済み記事の訪問キャッシュファイル200(nginxがすぐ応答)使用しない
キャッシュされていない記事の訪問WordPress200・キャッシュファイル保存1つ使用
ブラウザの中だけで動く説明用シミュレーターです。応答と時間は2026年10月のNginx + Apache構成サーバーのログと実測から取った値です。

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に届いたリクエスト数を基準に判断しました。

📊レート制限の適用前後1日単位・ログ集計
適用前適用後
管理画面アドレスの302応答
未ログインのwp-adminリクエストにWordPressが返した302。未ログインの管理画面アドレス制限は10月9日夜に適用。
02,4994,9987,4979,996
10/99,996適用前
10/1011適用後
10/111適用後
ボットIPのリクエスト数
3時間で22,391件を送ったIP 1つの1日のリクエスト数。
05,59811,19616,79322,391
10/922,391適用前
10/100リクエストなし
10/110リクエストなし
WordPressが生成した404ページ
スキャナーの普通の偽パスへのリクエスト。PHPに届くリクエストの制限は10月11日夜に適用のため、来週の点検の数字で判断。
06531,3061,9592,612
10/92,563適用前
10/102,612適用前
10/111,630適用前・11日夜に適用
数字は2026年10月9日から11日までのnginx access.logを1日単位で集計した値です。

🔢 FAQ & おすすめコンテンツ

dynamicはリクエスト量に応じてワーカーを生成・終了し、待機ワーカーが0になるとseems busyの警告を記録します。staticはmax_childrenの数を常に保つため生成の遅れや警告はありませんが、メモリを常に占有します。この記事のサーバーはdynamicで26を維持しました。メモリに余裕があり、リクエスト量が一日中一定のサーバーならstaticが合います。

警告の時刻に特定のリクエストが長く実行されているように見えるときに有効にします。プール設定で[slowlog]のパスと[request_slowlog_timeout]を指定すると、その時間を超えたリクエストの実行位置が記録されます。この記事の事例は原因がリクエスト数だったため、access.logの集計だけで判別し、slowlogは使いませんでした。

制限はキャッシュを通らずPHPまで届く未ログインのリクエストにだけ適用され、1つのIPが毎分30件・瞬間60件を超えたときに超過分だけが429を受けます。10月11日1日分の集計でこの基準を超えたIPはスキャナー1つだけで、超過状態は1分以内に解除され、次のリクエストからは通常どおり応答します。

減らしていません。プリロードは記事や翻訳を保存した作業時間帯に集中し、訪問者のエラーにはつながらなかったためです。判断基準は警告の件数ではなく、訪問者が受けた502・504エラーの有無で、その数が0ならプリロードはそのままにします。

ℹ️ 提携について
当サイトのコンテンツにはアフィリエイトリンクが含まれています。訪問者がこのリンクを通じて商品やサービスを購入すると、当サイトは販売元から手数料を受け取ります。この過程において、購入者が支払う金額(イベント割引適用で金額が下がります↓)は上昇しません。掲載されている価格・割引・在庫情報は作成時点のものであり、実際とは異なる場合がありますので、ご購入前に販売元にて最終確認をお願いいたします。商品の選定および評価は独自の基準に基づいて行われており、手数料の支払いの有無は紹介順や評価内容に影響を与えません。

コメントを残す

メールアドレスが公開されることはありません。 ※ が付いている欄は必須項目です

人間であることを証明してください: 1   +   5   =