php-fpm max_children 警告:增加工作程序前先從日誌找出原因

php-fpm max_children 警告表示 PHP 工作程序全都在處理請求,新的請求因此進入佇列。在一台這個警告已累積好幾週、由 Nginx、Apache 與 php-fpm 組成的 WordPress 伺服器上,透過日誌找出塞滿工作程序的請求後,最後沒有調高設定值,而是用 nginx 速率限制解決。
每週檢查中,這個警告每天記錄 27–187 次,但同一期間的四天裡,訪客收到的 502·504 錯誤為 0 次。塞滿工作程序的不是訪客,而是反覆請求管理位址的機器人,以及請求不存在路徑的掃描器。
1️⃣ php-fpm max_children & seems busy
警告一天累積 66 次時,很容易解讀成工作程序不足了 66 次。php-fpm 日誌裡混著兩種性質不同的警告,大部分是在達到上限之前就記錄的預警。
[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 個、無法再產生的時刻,從這時起新請求就得等待。
就像餐廳多叫一位員工來幫忙,和座位全滿只好讓客人在外面等的差別。
把一天的紀錄依種類統計後,seems busy 有 58 次、max_children 有 6 次。檢查例行程序把兩種警告加總回報為 66 次,但實際達到上限的只有六次。
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:44第二個指令輸出的六個時間點,就是追查原因的基準,下一步只用這幾個時間點。
2️⃣ 為什麼調高 max_children 不是解答
調高上限 26,警告次數或許會減少,但塞滿工作程序的是哪些請求並不會改變。
一般的算法是用可用記憶體除以單一工作程序的平均記憶體。實測的伺服器上,26 個工作程序平均使用 131MB、最多 214MB,用剩下的可用記憶體來算,單看數字還有餘裕再產生更多工作程序。
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第一個輸出的四個值依序是工作程序數、平均記憶體(MB)、最大記憶體(MB)、合計(MB),free 輸出中的 available 欄就是可用記憶體。
工作程序比 CPU 核心數多時,再增加工作程序也不會讓同時處理的請求變多,只會拉長每個請求所花的時間。在訪客收到的 502·504 錯誤為 0 次的情況下,調高上限沒有任何好處,只是把更多程序分給原本就佔著工作程序的機器人和掃描器。
調高上限之前要確認的是,警告記錄當下佔用工作程序的請求是不是訪客;用 access.log 確認的步驟整理在下一章。
3️⃣ 用 access.log 追查原因:依序看時間、IP、路徑、回應碼
選一個警告時間點,答案就在那 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 404以 10 月 9 日 18:09 為例,1 分鐘內記錄了 286 筆,其中一個 IP 就佔了 115 筆,路徑是 /wp-admin/profile.php 這類管理位址,回應碼全部是 302。
回應碼代表請求有沒有到達 PHP。nginx 直接以快取 HTML 回應的 200,以及 444·400 都不經過 PHP;302·404 與未快取的 200 則各使用一個工作程序。在未登入狀態下請求管理位址時,WordPress 會以 302 導向登入頁面,這一個 302 就是一次 PHP 執行。
一台伺服器上有兩個以上網站時,全域 access.log 會一起記錄兩個網站的記錄行。起初看起來像是這個機器人在攻擊第一個網站,但與各網站的日誌比對後,22,391 筆中有 22,390 筆是第二個網站的請求。
4️⃣ 塞滿工作程序的三種請求:機器人的 wp-admin、掃描器的 404、快取預載
把同樣的步驟套用到所有記錄到警告的時間點後,發現塞滿工作程序的請求有三種。
第一種是反覆請求管理位址的機器人。一個 IP 在三小時內送出 22,391 筆請求,其中 9,944 筆是包含 /wp-admin/profile.php 在內的四個路徑;未登入的一次 302 就是一次 PHP 執行,所以用掉了 9,944 次工作程序。
第二種是請求不存在路徑的漏洞掃描器。即使 User-Agent 寫著 Googlebot,也不是 Googlebot:一天送出 14,672 筆請求的 IP,是反向 DNS 以 googleusercontent.com 結尾的 Google Cloud 虛擬伺服器,而 Googlebot 會以 googlebot.com 結尾。同一個 IP 還輪流使用 bingbot、Applebot、GPTBot、ClaudeBot。
像 .env 這類檔案請求,nginx 會在 0.04 秒內以 444 封鎖,但 /userfiles 或 /secrets.yml 這種不在封鎖規則裡的一般路徑,會讓 WordPress 產生 404 頁面。這個 404 頁面有 266KB、需要 1.4–2.9 秒,成本與產生一般文章相同,而一個 IP 一天就請求了 4,069 次。
第三種是伺服器本身。儲存文章或翻譯時會清空快取,WP Rocket 預載會重新請求所有網址,一天 8,489 筆,每分鐘最多 60 筆。Rank Math 的索引狀態檢查也透過 Action Scheduler 一天使用一個工作程序 754 次。這兩者只集中在作業時段,也沒有造成訪客錯誤,因此不列入處理對象。
在下方的模擬器中,可以看到每個請求在哪一層結束,以及是否使用 PHP 工作程序。
| 要求 | 停下的層 | 回應 | Worker |
|---|---|---|---|
| 機器人的 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 邊緣節點的位址,經過該節點的所有訪客都會被當成同一個 IP,因此要先用 set_real_ip_from 設定邊緣節點 IP 清單,從 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.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 筆;三小時內送出 22,391 筆請求的 IP,從隔天起請求數為 0。
掃描器這邊剛套用限制,還沒有一整天的數字,要看 WordPress 產生的 404 頁面數(9 日 2,563、10 日 2,612、11 日 1,630)在下週檢查時降到多少來判斷。訪客的 502·504 錯誤在套用前後都是 0 次。
這些數字是直接統計以 Nginx、Apache、php-fpm 運作的 WordPress 伺服器日誌所得,判斷依據不是警告次數,而是訪客錯誤與到達 PHP 的請求數。
🔢 FAQ & 推薦內容
ℹ️ 聯盟行銷說明
本站內容包含聯盟行銷連結。訪客透過這些連結購買商品或服務時,本站會從賣家獲得佣金。在這個過程中,購買者支付的金額(遇到活動折扣時金額會降低↓)並不會因此提高。刊登的價格、折扣與庫存資訊以撰寫當時為準,可能與實際情況不同,購買前請向賣家做最後確認。商品的挑選與評價依本站自訂的標準撰寫,是否支付佣金不會影響介紹順序或評價內容。