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 速率限制解決。

每週檢查中,這個警告每天記錄 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'
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️⃣ 為什麼調高 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.05
free -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.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 筆,其中一個 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 工作程序。

🧭 請求路徑模擬器wp-admin 管理頁面說明模擬器
🗂️ 塞滿工作程序的請求
機器人的 wp-admin 請求
在未登入狀態下請求管理位址時,WordPress 會以 302 導向登入頁面。這一個 302 就是一次 PHP 執行。
掃描器:.env 類
像 .env 這類檔案請求,會被 nginx 封鎖規則以 444 切斷連線。0.04 秒,不經過 PHP。
掃描器:一般假路徑
/userfiles、/secrets.yml 這類不在封鎖規則裡的一般路徑,會讓 WordPress 產生 404 頁面。266KB、1.4–2.9 秒,成本與產生一般文章相同。
快取預載
儲存文章或翻譯時會清空快取,WP Rocket 預載會重新請求所有網址。每分鐘最多 60 筆,每個請求使用一個工作程序。
造訪已快取的文章
有快取檔案時,nginx 會直接回應,不經過 PHP。
造訪未快取的文章
沒有快取檔案時,PHP 會產生頁面,並把那份 HTML 存成快取檔案。
整體流程
請求由上往下走,在回應結束的那一層停下。只有一路走到 WordPress 的請求才會使用 PHP 工作程序。
要求停下的層回應Worker
機器人的 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 邊緣節點的位址,經過該節點的所有訪客都會被當成同一個 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 的請求數。

📊套用速率限制前後以日為單位 · 日誌統計
套用前套用後
管理位址的 302 回應
WordPress 對未登入 wp-admin 請求送出的 302。未登入管理位址限制於 10 月 9 日晚上套用。
02,4994,9987,4979,996
10/99,996套用前
10/1011套用後
10/111套用後
機器人 IP 的請求數
三小時內送出 22,391 筆請求的那一個 IP,每天的請求數。
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 以日為單位統計的結果。

🔢 FAQ & 推薦內容

dynamic 會依請求量產生、結束工作程序,閒置工作程序降到 0 時會記錄 seems busy 警告。static 則一直維持 max_children 個工作程序,沒有產生延遲和警告,但會持續佔用記憶體。本文的伺服器維持 dynamic 26 個;如果記憶體有餘裕、請求量整天都很平穩,static 比較合適。

在警告時間點看起來有特定請求執行很久時開啟。在 pool 設定中指定 [slowlog] 路徑與 [request_slowlog_timeout],超過該時間的請求就會記錄執行位置。本文的案例原因在於請求數量,光靠統計 access.log 就能判斷,因此沒有使用 slowlog。

限制只套用在未經快取、會到達 PHP 的未登入請求上,一個 IP 超過每分鐘 30 筆或瞬間 60 筆時,只有超出的部分會收到 429。在 10 月 11 日的單日統計中,超過這個標準的 IP 只有一個掃描器,而超出狀態在 1 分鐘內就會解除,下一個請求起即恢復正常回應。

沒有減少。預載只集中在儲存文章與翻譯的作業時段,也沒有造成訪客錯誤。判斷標準不是警告次數,而是訪客有沒有收到 502·504 錯誤;只要這個數字是 0,預載就維持原樣。

ℹ️ 聯盟行銷說明
本站內容包含聯盟行銷連結。訪客透過這些連結購買商品或服務時,本站會從賣家獲得佣金。在這個過程中,購買者支付的金額(遇到活動折扣時金額會降低↓)並不會因此提高。刊登的價格、折扣與庫存資訊以撰寫當時為準,可能與實際情況不同,購買前請向賣家做最後確認。商品的挑選與評價依本站自訂的標準撰寫,是否支付佣金不會影響介紹順序或評價內容。

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

Prove your humanity: 4   +   9   =