網站突然回應變慢、後台轉圈圈,或是流量一上來就吐出 502、504 錯誤,很多人第一反應是「主機不夠力,該升級了」。但在升級之前,先打開 PHP-FPM 的設定檔看一眼,往往會發現真正的瓶頸不是 CPU,而是 pm.max_children 這個數字設得不對。
PHP-FPM 調校的核心,其實就是回答一個問題:這台主機的記憶體,到底能同時養活幾個 PHP worker?設太少,請求得排隊等空閒的 worker,回應時間拉長;設太多,worker 一起搶記憶體,主機開始用 swap,甚至觸發 OOM 把行程砍掉,反而更慢更不穩。
這篇會把 pm.max_children 的算法拆到可以照著做的程度:怎麼量一個 worker 真正吃多少記憶體(這一步最多人量錯)、怎麼扣掉資料庫與系統的份額、怎麼留安全邊際,以及 worker 數算出來之後,start_servers、max_requests 這些連動參數該怎麼跟著定。重點放在台灣常見的 WordPress 與 WooCommerce 小型 VPS 環境。
PHP-FPM 的 worker 到底在做什麼
PHP-FPM 是一個常駐的行程管理員,手上養著一池 worker 行程,每個 worker 一次只處理一個 PHP 請求。請求進來,分派給一個空閒 worker;處理完,worker 變回閒置狀態等下一個請求。
這裡有個關鍵推論:一個 pool 的最大同時並發數,差不多就等於 worker 數。設定 10 個 worker,代表這個 pool 最多只能同時跑 10 個未被快取攔下來的 PHP 請求。第 11 個請求進來時,就得在 Nginx 與 PHP-FPM 之間的佇列裡等,等到有人空出來、或等到逾時被 Nginx 判定 502 / 504。
所以 worker 數同時決定了兩件事:能扛多少並發,以及會吃掉多少記憶體。這兩件事互相拉扯,調校就是在中間找平衡點。控制這池 worker 的主要參數有三個。
pm:選用哪種行程管理模式,static、dynamic 或 ondemand。pm.max_children:worker 數的硬上限,也就是並發上限。pm.max_requests:一個 worker 處理多少請求後就回收重生,用來壓制記憶體洩漏。
下面先把模式講清楚,再進到 worker 數怎麼算。
static、dynamic、ondemand 三種模式怎麼選
先給結論:一般 WordPress 與 WooCommerce 的 VPS,預設用 dynamic;流量穩定且高、想要可預測性的環境用 static;流量很低、想省記憶體的小機器用 ondemand。三者差在 worker 何時被建立、何時被殺。
static 開機就把 worker 數固定養滿,永遠維持 pm.max_children 個行程待命。請求進來不必等行程啟動,是反應最快的做法,代價是即使沒流量,這些 worker 也一直佔著記憶體。
dynamic 是多數情況的合理預設。它依負載動態增減 worker,平時維持一個下限,流量上來才往上加到 pm.max_children。它需要四個連動參數一起設定。
pm.start_servers:PHP-FPM 啟動時先開幾個 worker。pm.min_spare_servers:閒置 worker 的下限,少於這個數就補。pm.max_spare_servers:閒置 worker 的上限,多於這個數就殺掉一些。pm.max_children:worker 總數的天花板。
ondemand 平時不留 worker,請求進來才生,閒置一段時間(pm.process_idle_timeout)後殺掉。沒流量時幾乎不吃記憶體,很適合後台控制面板、低流量的小站。缺點是閒置之後第一個請求要等 worker 冷啟動,首次回應會慢一拍。
選擇的邏輯其實對應流量型態。內容站匿名流量多、又有整頁快取擋在前面,真正打到 PHP 的請求不多且偏輕,dynamic 給的彈性剛好。WooCommerce 的購物車、結帳、會員中心多半無法整頁快取,登入流量重、外掛多,但只要負載可預測,static 把 worker 預熱好待命會更穩。真的很小、整天沒幾個請求的機器,才考慮 ondemand 省記憶體。
怎麼量一個 worker 真正吃多少記憶體
這是整個 PHP-FPM 調校最容易出錯的一步,因為大多數教學叫你直接看 ps 的 RSS 欄位,而 RSS 會把多個 worker 共用的那塊記憶體重複計算,導致你高估單一 worker 的用量,最後 pm.max_children 設得比實際能力保守很多。
先看最常見的量法。在 Linux 上對 PHP-FPM 行程依記憶體排序:
ps -ylC php-fpm8.3 --sort:rss
行程名稱(這裡的 php-fpm8.3)依你的 PHP 版本而定,不確定可以先用 ps -e | grep php-fpm 找出正確名稱。輸出裡的 RSS 欄位是每個行程的常駐記憶體,單位是 KB。如果想直接算平均,可以用一行指令把所有 worker 的 RSS 加總再除以行程數:
ps --no-headers -o "rss,cmd" -C php-fpm8.3 | awk '{ sum+=$1 } END { printf ("%d%sn", sum/NR/1024,"Mb") }'
問題在於,RSS 包含了 worker 之間共用的記憶體,最典型的就是 OPcache 編譯後的位元碼,以及共用的 PHP 函式庫。這塊共用記憶體只實際佔用一份,但每個 worker 的 RSS 都把它算進去。假設 RSS 顯示 60 MB,其中可能有 30 MB 是共用的,真正屬於這個 worker 的私有記憶體只有 30 MB。如果照 RSS 的 60 MB 去算,等於每加一個 worker 就以為要多付 60 MB,實際只多付私有的那部分,結果是把 worker 數算少了一半。
要量得準,看的是「私有記憶體」而非總 RSS。ps_mem 這支工具會把私有與共用分開列:
sudo ps_mem | grep php-fpm
它的輸出會像「私有記憶體 + 共用記憶體 = 總記憶體」這種格式,例如私有 28 MB、共用 34 MB、合計 62 MB。算 worker 增量成本時,用私有那一欄會比較貼近現實;想保守一點,就在私有值之上抓一點緩衝。
另一個常被混淆的數字是 PHP 的 memory_limit。它是「單一請求」能用的記憶體硬上限(例如 256 MB),是防呆用的天花板,不是每個 worker 的平均用量。實務上 worker 的平均 RSS 通常遠低於 memory_limit,拿 memory_limit 去乘 worker 數會嚴重高估,把記憶體預算算爆。要量就量實際跑起來的數字,不要用設定檔上的上限值代替。
量的時候還要在真實負載下量,不是剛重啟、什麼都還沒跑的狀態。實際操作一下網站:瀏覽幾頁、開 wp-admin、跑一兩筆 WooCommerce 結帳,讓外掛與主題真正載入,這時候量到的 worker 大小才反映線上情況。外掛越多、主題越重,單一 worker 越大,這也是為什麼別人的數字不能直接抄。
pm.max_children 的計算公式與安全邊際
算 pm.max_children 的骨架是一條除法:
pm.max_children = 分給 PHP-FPM 的記憶體 ÷ 單一 worker 平均記憶體
但這個原始值不能直接拿來用,要先處理兩件事,分母怎麼來、結果要打幾折。
分子是「分給 PHP-FPM 的記憶體」,不是主機總記憶體。 一台跑 WordPress 的主機,記憶體還要分給作業系統與背景服務、網頁伺服器(Nginx 或 Apache)、資料庫(MySQL 或 MariaDB)、快取層(Redis 或 Memcached),可能還有監控與備份代理。這些都得先扣掉。一個常見的分配比例是:保留三到四成給資料庫與快取,一到兩成給系統與網頁伺服器,剩下的四到六成才是 PHP-FPM 能用的額度。資料庫尤其不能省,MySQL 自己的緩衝池吃起記憶體毫不手軟,被 PHP worker 擠到去用 swap,整台主機都會被拖慢。
分母是上一節量到的單一 worker 平均記憶體,建議用私有記憶體值或在它之上抓點緩衝。
算出原始值後,再乘一個 0.7 到 0.8 的安全係數。 因為流量會有尖峰、某些請求會特別重、worker 大小也會隨時間漂移,貼著理論極限設等於沒有緩衝,一遇到突發就爆記憶體。打個七到八折,把這個風險吸收掉。算出來的最終值無條件捨去取整數。
把流程串起來看幾個台灣 VPS 常見規格的例子。
2 GB VPS 跑一般 WordPress 部落格
小型部落格、匿名流量為主、整頁快取做得好的情境:
- 主機總記憶體 2 GB
- 量到的單一 worker 平均約 80 MB
- 分給 PHP-FPM 約 900 MB
原始值是 900 ÷ 80 ≈ 11,乘上 0.8 安全係數約得 8。對一個快取良好的低流量站,8 個 worker 通常綽綽有餘。對應的設定:
pm = dynamic
pm.max_children = 8
pm.start_servers = 2
pm.min_spare_servers = 2
pm.max_spare_servers = 4
pm.max_requests = 500
8 GB VPS 跑中型 WooCommerce 商店
外掛多、主題重、結帳流量無法整頁快取的情境:
- 主機總記憶體 8 GB
- 量到的單一 worker 約 150 到 220 MB,保守取 200 MB
- 分給 PHP-FPM 約 3.5 GB
原始值是 3500 ÷ 200 ≈ 17,乘 0.75 約得 12。對應設定:
pm = dynamic
pm.max_children = 12
pm.start_servers = 4
pm.min_spare_servers = 4
pm.max_spare_servers = 8
pm.max_requests = 400
12 個 worker 代表最多 12 個並發 PHP 請求,搭配匿名流量的整頁快取與登入使用者的物件快取,通常足以撐住數百名同時在線、偶有結帳尖峰的訪客。
16 GB 主機跑高流量 WooCommerce
負載高且可預測、前面通常還有 CDN 的情境:
- 主機總記憶體 16 GB
- 單一 worker 約 250 MB(大型外掛堆疊、重主題)
- 分給 PHP-FPM 約 7 GB
原始值是 7000 ÷ 250 = 28,乘 0.75 得 21。這種規格適合改用 static,把 21 個 worker 全部預熱待命,換取最穩定的反應時間:
pm = static
pm.max_children = 21
pm.max_requests = 500
三個例子的共通點是:worker 數隨記憶體預算近乎線性成長,所以每次調高 pm.max_children,都要回頭重新確認記憶體真的夠用,不能只改數字不驗證。
start_servers 與 spare 參數為什麼要這樣定
dynamic 模式下的 start_servers、min_spare_servers、max_spare_servers 不是隨便填,它們在解一個取捨:閒置 worker 留太少,流量一來得現生、首批請求要等冷啟動;留太多,沒流量時白佔記憶體。
一個流傳已久、可當起點的算法是用 CPU 核心數推:start_servers 設為核心數乘 4,min_spare_servers 設為核心數乘 2,max_spare_servers 跟 start_servers 一樣。背後的邏輯是讓平時就有足夠 worker 待命吃下日常負載,又不會閒養過多。實務上不必死守這個公式,重點是把握三個原則。
start_servers介於 spare 的上下限之間,這樣啟動後狀態就落在穩定區間,不會一開機就被判定要補或要殺。min_spare_servers別設太低,否則流量稍有起伏就得頻繁生 worker,反而增加開銷。- 三個值都不能超過
pm.max_children,它是天花板,spare 與 start 都得待在它底下。
如果是 static 模式就單純多了,沒有 spare 概念,開機直接固定養 pm.max_children 個 worker,連動參數都省了。ondemand 則只需要 pm.max_children 加 pm.process_idle_timeout,前者管上限、後者管閒置多久回收。
max_requests 怎麼設才能壓住記憶體洩漏
pm.max_requests 是讓 worker 處理到一定請求數後自動重生的機制,預設值 0 代表永不回收,這個預設值幾乎不該保留。原因是 PHP 程式、擴充套件或某些外掛會有記憶體洩漏,worker 活得越久、RSS 漲得越大,跑上幾天幾週後記憶體悄悄爬高,某天突然撞到 swap 或被 OOM 砍掉。
設一個適中的回收上限,讓 worker 跑到一定請求數就優雅地重啟、釋放累積的記憶體,是低成本的防護。設太低則 worker 回收太頻繁,徒增啟動開銷、偶爾還會有短暫的 CPU 尖峰。實務上的常見區間如下。
- 單純的 WordPress 內容站:800 到 1000。
- 中型內容站或輕量 WooCommerce:500 到 800。
- 外掛多的重型 WooCommerce:300 到 500。
判斷依據很直接:外掛越多、洩漏風險越高,就把數字往下調。如果觀察到 PHP-FPM 的記憶體用量隨天數持續往上爬,例如每天 RSS 增加幾百 MB,就把 pm.max_requests 再調低一點然後繼續觀察。它治的是「症狀」,根治還是要回去找哪個外掛在漏,但在找到之前,這個參數能讓主機不至於被慢慢撐爆。
設定改完之後怎麼驗證是否設對
改完設定不能只看「網站還活著」就收工,要實際確認 worker 數對不對、記憶體有沒有踩線。PHP-FPM 的設定檔通常在 /etc/php/版本/fpm/pool.d/www.conf(Debian、Ubuntu)或 /etc/php-fpm.d/(AlmaLinux、Rocky),改完記得重啟服務,例如 sudo systemctl restart php8.3-fpm,否則設定不會生效。
第一個要盯的是 PHP-FPM 的錯誤紀錄。如果看到這類訊息,代表 worker 不夠用,pm.max_children 該往上調(前提是記憶體還有空間):
[pool www] server reached pm.max_children setting (8), consider raising it
[pool www] seems busy (you may need to increase pm.start_servers, or pm.min/max_spare_servers)
第二個是在有流量時用 top 或 htop 看記憶體與 swap。理想狀態是 worker 跑到接近上限時,記憶體仍有餘裕、swap 維持在零或接近零。一旦開始大量用 swap,表示 pm.max_children 設過頭,要往下調,而不是繼續加。
第三個是用壓測工具實際打一打,比較調校前後的差異。例如用 ApacheBench 模擬 50 個並發、送 1000 個請求:
ab -n 1000 -c 50 https://你的網域/
觀察平均回應時間與失敗率有沒有改善。調校本來就是迭代的,量記憶體、算 worker、套設定、壓測、看紀錄,再依結果微調,跑個一兩輪通常就能收斂到合適的值。
回到最開頭那個情境:網站變慢、冒出 502。在掏錢升級主機之前,先量一次 worker 的私有記憶體、按公式重算 pm.max_children、把 max_requests 從預設的 0 改成合理值,很多時候瓶頸就解開了。記憶體是固定的,調校做的事就是把這塊有限的記憶體,精準切成「夠用又不爆」的 worker 數。把這個數字算對,同一台主機能扛的流量,往往比你以為的多。