深入理解 PHP-FPM 的最佳配置
深入理解 PHP-FPM 的最佳配置
對大多數開發者來說,PHP-FPM 的配置并不是日常工作中需要深入研究的東西。這沒什么問題,畢竟不是每個人都想或需要在服務器調優上花時間。
況且,現在有很多托管服務(寶塔, 1panel等)可以幫你把服務器配置好,安裝所有依賴(包括 PHP-FPM),你只需要在控制面板點幾下就能部署代碼。也許你們公司有專門的運維,或者有資深開發在負責這塊。即便真要自己配置 PHP-FPM,多半也就是翻幾篇文章,改改參數,或者直接用默認配置。這很正常——誰有那么多時間去鉆研每個服務器配置細節,尤其這只是工作的一小部分。
但隨著應用不斷迭代、用戶越來越多,你可能會發現服務器開始變慢,請求處理時間越來越長,內存占用接近上限,甚至服務器直接掛掉。
最近我的一臺服務器就遇到了類似問題,所以我決定花點時間搞清楚 PHP-FPM 到底是怎么工作的,不同配置會帶來什么影響。我看了很多文章、討論和評論,然后自己做了些測試來驗證。以下是我的一些心得。
問題排查
如果問題確實出在 PHP-FPM 上,有幾個排查方向。首先檢查 PHP-FPM 的日志,重點關注 max children 相關的警告。PHP-FPM 的主進程會按需生成子進程,直到達到 max children 的上限。每個子進程一次只能處理一個請求(比如對你應用的一次訪問)。所以如果 max_children 設置成 5,而同時有 10 個用戶在訪問應用,日志里很可能會出現這樣的警告:
WARNING: [pool www] server reached pm.max_children setting (5), consider raising it
這會導致部分請求被延遲,直到有子進程空閑出來。可以用下面的命令檢查日志里有沒有這類警告。如果你用的是 PHP-FPM 8.2:
sudo grep max_children /var/log/php8.2-fpm.log.1 /var/log/php8.2-fpm.log
注意你系統上的日志路徑可能不同,記得先確認。另外,除了替換 PHP 版本號,有些系統的日志文件名里不帶版本號,那就直接用 php-fpm:
sudo grep max_children /var/log/php-fpm.log.1 /var/log/php-fpm.log
后面提到的所有 php-fpm 命令都是同樣的道理。我會用 php-fpm8.2 或 php8.2-fpm,因為這是我的版本,你的可能是 php-fpm7.4(php7.4-fpm)或者直接就是 php-fpm。
不想打開配置文件的話,可以用這個命令快速查看當前配置:
sudo php-fpm8.2 -tt
這樣就能找到 pm.max_children 這一行,確認 max_children 是不是真的設置成了 5:
[19-Mar-2024 22:48:10] NOTICE: pm.max_children = 5
另外要關注的是服務器內存使用情況。用 htop 按內存排序,可以看到內存是不是快用完了,PHP-FPM 進程占了多少。
這可能是 max_children 設置太高,生成的子進程太多,服務器內存撐不住了。或者,如果重啟 PHP-FPM 后內存使用量下降,然后又慢慢漲回去,那多半是代碼有內存泄漏。理想情況下當然要找到泄漏點并修復,但定位內存泄漏有時候挺難的,尤其在大項目里,而且泄漏可能來自某個必需的第三方庫。
第一個問題可以通過優化配置解決,內存泄漏的話 PHP-FPM 這邊也有些緩解辦法,稍后會講。
順便說一下,重啟 PHP-FPM 的命令可能是這樣的(但你的情況可能不同,所以要確認一下):
sudo service php8.2-fpm restart
如前面說的,重啟 PHP-FPM 能臨時緩解內存泄漏問題(但不是根本解決),給你爭取時間去修復泄漏或調整配置。
配置進程管理器
現在可以開始修改 PHP-FPM 的配置文件了,看看怎么針對實際情況做優化。編輯主配置文件的命令:
sudo nano /etc/php/8.2/fpm/pool.d/www.conf
里面有各種配置項,我們只講幾個對性能影響最大的。首先要決定進程管理器如何控制子進程數量。有 3 個選項:static、dynamic 和 ondemand。大多數情況下默認是 dynamic。這幾個選項有什么區別?假設你確定服務器最多需要 10 個子進程:
static 會始終保持所有 10 個進程運行,理論上最快,因為進程都已經在那兒了,負載上來時不需要 fork 新進程。但代價是即使沒人訪問網站,這 10 個進程也會一直占著內存。
dynamic 可以靈活調整;比如一開始啟動 3 個進程,負載上來時 fork 到最多 10 個,負載下去后減少到 6 個等待連接。這個選項在內存占用和響應速度之間找平衡,至少理論上如此。
最后是 ondemand,一開始不生成任何子進程,負載上來時最多創建 10 個,負載下去后可能又回到 0 個。這個選項(理論上)適合流量不大的中小型應用、預發布環境,或者多租戶共享服務器。由于子進程一直在回收,可以幫助控制內存泄漏,因為進程在內存累積之前就被干掉了。缺點是需要頻繁 fork 新進程,可能影響性能和響應速度。
設置這些選項之前,需要算出 PHP-FPM 進程的最大負載。也就是確定服務器能跑多少個子進程,然后設置 max_children 值。怎么算?這有點麻煩,因為理想情況下要知道單個子進程平均用多少內存。問題是多個進程通常會共享一些內存,所以很難精確算出單個進程的實際內存使用量。
網上有很多腳本和文章教你怎么算 PHP-FPM 進程的平均內存消耗,但大多數我試下來感覺不太對,算出來的值比預期高很多。
有個 Python 腳本在好幾篇文章里都提到了,看起來比較靠譜。可以用這個命令算出每個程序的總內存使用量:
cd ~ &&
wget https://raw.githubusercontent.com/pixelb/ps_mem/master/ps_mem.py &&
chmod a+x ps_mem.py &&
sudo python3 ps_mem.py
注意我用的是 python3,你的系統可能只需要 python。跑完腳本后,可能會看到類似這樣的結果:
2.1 GiB + 127.5 MiB = 2.2 GiB php-fpm8.2 (31)
這說明 31 個 PHP-FPM 進程用了 2.2 GB 內存,平均每個進程約 73 MB。另一個有用的命令可以查看空閑和活動進程數:
sudo service php8.2-fpm status -l
看這一行就能知道子進程的當前狀態:
Status: "Processes active: 0, idle: 30, Requests: 56116, slow: 0, Traffic: 0req/sec"
這里沒有活動進程,30 個空閑進程,加上 1 個主進程,總共 31 個,和 Python 腳本報的一致。
Python 腳本也會報總內存使用量。可以隨時跑 htop 或 free -hl 檢查服務器當前內存使用情況,看看這些數字是否合理、是否對得上。
還有一點,如果真有內存泄漏,單個進程的內存可能會漲到 php.ini 里定義的 memory_limit,默認一般是 128 MB。所以保守起見,可以直接用這個值作為單個進程的平均值。
好,現在可以算 max_children 值了。假設有臺 8 GB 內存的服務器,其他程序用了 2 GB,剩 6 GB。再留 1 GB 作為緩沖,防止意外情況或者未來應用增長、新增進程什么的。這樣就剩 5 GB 給 PHP-FPM。前面算出單個進程用約 73 MB 內存,用 5 GB 除以 73 MB 就得到 max_children 值:
5120 (MB) / 73 (MB) = 70.14
所以這臺服務器的 max_children 應該設置成 70。這樣無論選哪個進程管理器(pm)選項,PHP-FPM 最多都只會生成 70 個子進程。
如果用 pm = static,就不需要設置其他選項了。70 個子進程會立即生成,隨時待命。但要記住代價:這些進程會一直占著 5 GB 內存。
如果用 pm = ondemand,只需要考慮一個額外設置:pm.process_idle_timeout。由于 ondemand 模式下子進程會不斷生成和終止,這個設置告訴 PHP-FPM 什么時候干掉空閑的子進程。默認是 10 秒,需要的話可以改,不過默認值已經挺合理了。
如果用 pm = dynamic,需要考慮幾個額外設置。pm.start_servers 是啟動或重啟 PHP-FPM 時立即生成的子進程數。pm.min_spare_servers 設置最小空閑子進程數。pm.max_spare_servers 決定最大空閑子進程數。
假設我們這樣設置:
pm = dynamic
pm.max_children = 70
pm.start_servers = 20
pm.min_spare_servers = 20
pm.max_spare_servers = 40
實際運行是這樣的。上面例子里 start_servers 設置為 20,PHP-FPM 一啟動就會生成 20 個子進程,占用相應的內存。有請求過來時,這 20 個進程中的部分或全部會變成活動狀態處理請求。沒流量時,這 20 個進程會空閑等待;它們不會被終止,繼續占著內存。
把 min_spare_servers 設置得比 start_servers 低(比如 15)沒什么意義,因為 20 個子進程會立即生成,即使空閑,主進程也不會為了達到 15 的最小值而終止 5 個。而且 min_spare_servers 不能比 start_servers 大,所以最好就把 min_spare_servers 設置成和 start_servers 一樣。
如果大量請求涌入,20 個子進程不夠用,主進程會生成額外的子進程,最多到 max_children 值,這里是 70。假設為了應對流量激增生成了 70 個子進程。過一會兒,流量恢復正常,不再需要 70 個進程了,大部分或全部進程變成空閑。這時主進程會終止空閑子進程,直到 max_spare_servers 值,這里是 40。然后你就剩 40 個空閑進程,它們不會被進一步終止,繼續占著內存。
所以設置這些值時要記住:如果需要生成比 start_servers 更多的進程,流量高峰過后你會剩下那么多子進程(最多到 max_spare_servers 值)在運行。比如需要生成 30 個子進程,你會剩 30 個在運行;需要生成 50 個,過一會兒會剩 40 個(因為 max_spare_servers)在運行。所以如果不想最終可能有 40 個子進程在后臺跑著,可以考慮降低這個值,甚至讓它和 start_servers 一樣。這些情況會一直保持到你重啟 PHP-FPM。重啟后會根據 start_servers 重新生成 20 個子進程。
很多文章里有個公式,建議根據 CPU 核心數來設置 start_servers、min_spare_servers 和 max_spare_servers 以獲得最佳性能:
pm.start_servers = CPU 核心數 x 4
pm.min_spare_servers = CPU 核心數 x 2
pm.max_spare_servers = CPU 核心數 x 4
這個公式據說是基于單個 CPU 核心能并發處理多少進程的某種假設。我不確定是誰開始這么搞的,這些乘數怎么推導出來的,但有一點讓我覺得不對勁——把 min_spare_servers 設得比 start_servers 低,如我上面解釋的,會導致 min_spare_servers 值永遠用不上。而且在我的測試中(稍后會講),用這個方法并沒看到明顯的性能提升。所以我覺得這個公式不該盲目照搬,要根據實際情況調整。
關于 max_children 和 dynamic 相關設置,最好的建議就是邊監控邊調優。每種情況都不一樣——你的資源、負載、整體策略都不同。按照上面的指導原則,從合理的值開始,隨著應用增長和變化,不斷調整配置。
這一節還有個值得注意的選項:pm.max_requests。如果真有內存泄漏,這個設置可以讓子進程在處理一定數量請求后被回收。默認是 0,意味著進程不會因為這個選項被終止。合理的值可能是 500 或 1000 個請求,取決于你的場景。比如設置成 500,子進程處理了 500 個請求后會被終止(釋放累積的內存),然后重新生成。
實際測試
我決定做幾個性能測試來驗證理論:static 處理請求應該最快,因為不需要臨時 fork 子進程;dynamic 應該居中,因為部分進程已經在跑,部分需要按需 fork;ondemand 應該最慢,因為它不停地生成和終止進程。結果如下。
我用 ApacheBench 做測試,按照建議從另一臺服務器發送請求,不是從被測服務器本身發的。被測服務器有 16 GB 內存和 4 個 CPU 核心,PHP-FPM 配合 NGINX,請求走 Laravel 應用。所有測試用例的 max_children 都是 80。我比較的是 90% 請求的響應時間。下面表格里只列出不同 pm 選項之間的毫秒差異,0ms 是最快的。
測試命令示例:
ab -n 1000 -c 10 https://example.com
這個例子會發送 1000 個請求,并發級別是每次 10 個。
先看第一個測試。我想看看明顯達到 max_children 限制時,不同 pm 選項如何影響響應時間。所以發了 25000 個請求,并發級別 1000。
用 dynamic 時的額外值如下(后面簡稱 20/20/40):
pm.start_servers = 20
pm.min_spare_servers = 20
pm.max_spare_servers = 40
結果如下:
| Static | Dynamic | On demand |
|---|---|---|
| +1223ms | +845ms | 0ms |
結果顯示,理論上應該最慢的 ondemand 實際上最快,而 static 出人意料地最慢,比 ondemand 慢了一秒多。
第二個測試發了 10000 個請求,并發級別 100。測試了兩種 dynamic 設置,一種是第一個測試的(20/20/40),另一種用基于 CPU 核心的公式(16/8/16):
| Static | Dynamic (20/20/40) | Dynamic (16/8/16) | On demand |
|---|---|---|---|
| 0ms | +14ms | +2ms | +24ms |
這次 static 最快,基于公式的 dynamic 緊隨其后,ondemand 最慢,符合理論預期。但總的來說,對大多數網站而言,+24ms 算不上什么性能提升,不同選項之間差異不大。
最后一個測試只發了 2000 個請求,并發級別 16。同樣用了兩種 dynamic 設置(20/20/40 和 16/8/16):
| Static | Dynamic (20/20/40) | Dynamic (16/8/16) | On demand |
|---|---|---|---|
| +2ms | +7ms | +10ms | 0ms |
這個規模下 ondemand 再次獲勝,static 緊隨其后,基于公式的 dynamic 墊底。這次差異更小了,第一名和最后一名之間只差 10ms。
最終得到了一些意外結果。測試表明,當并發請求數量遠超 max_children 值,或者遠低于 max_children 值時,ondemand 是最佳選擇。當并發請求數量接近 max_children 值時,static 最佳。但要注意,第二個測試,特別是第三個測試中,響應時間差異相當小。而且如果重新跑這些測試,排名很可能會變。
所以這些結果不能當成鐵律,理論也是如此。我覺得在現代服務器上,fork 一個新子進程已經不是什么昂貴操作了,不會明顯影響響應時間,至少在測試的規模上不會。這就是為什么 ondemand 不該被輕易否定,即使在處理請求速度方面也是如此。
最好的前進方式是做你自己的測試,因為你的負載、設置和每個請求執行的操作可能完全不同,然后根據這些測試,應用看起來最高效的設置。
其他設置
還有幾個額外的設置我們應該了解一下,在 PHP-FPM 出問題或者你需要追蹤慢請求時可能會很有用。
要啟用 slowlog(當然是用來記錄慢請求的),我們需要編輯之前的同一個配置文件:
sudo nano /etc/php/8.2/fpm/pool.d/www.conf
然后找到 slowlog 部分:
slowlog = /var/log/php8.2-fpm.log.slow
取消注釋。那里還有幾個相關選項你應該考慮取消注釋。第一個是 request_slowlog_timeout,默認設置為 5 秒。如果你只想記錄耗時 3 秒及以上的請求,應該取消注釋并修改這個值。第二個是 request_slowlog_trace_depth,默認設置為 20。在 Laravel 應用中,這個值可能太低,無法遍歷所有 vendor 函數并到達實際被調用的代碼,比如你的 controller。所以我認為大多數情況下,50 應該沒問題,但要確認一下是否適合你。
最終整個 slowlog 設置可能是這樣的:
slowlog = /var/log/php8.2-fpm.log.slow
request_slowlog_timeout = 3s
request_slowlog_trace_depth = 50
最后,還有另一個配置文件我們可以編輯,控制當子進程因為某種原因開始失敗時會發生什么。下面是編輯這個文件的示例:
sudo nano /etc/php/8.2/fpm/php-fpm.conf
在那個文件中,我們關注 3 個相互關聯的選項,它們默認都設置為 0 并被注釋掉。如果你打算使用它們,確保先取消注釋。之后,你可以把它們設置為這些值:
emergency_restart_threshold = 10
emergency_restart_interval = 1m
process_control_timeout = 10s
使用的值是你可能在其他一些文章中也會看到的。前兩個設置告訴 PHP-FPM,如果在一分鐘內有 10 個子進程失敗,PHP-FPM 應該自動重啟。第三個設置意味著子進程在響應主進程發送的信號之前會等待 10 秒。所以如果主進程向子進程發送 KILL 信號,它會有 10 秒時間完成任務后再退出。當然,你可以根據需要調整這些值。
在失敗時重啟 PHP-FPM 可能會解決一些問題,但如果問題與即使 PHP-FPM 重啟后仍會重現的東西有關,它會一直重啟,直到你弄清楚到底發生了什么。所以你應該自己決定,當發生意外情況時,是想讓 PHP-FPM 完全失敗,還是讓它自動重啟。
總結
本人討論了 max_children 選項的最佳值。探討了不同進程管理器設置的優勢和缺陷,以及如何測試它們。還了解了一些在調試慢請求或處理失敗時可能有用的額外設置。希望這篇文章對你有幫助,你能夠用文章中的信息作為起點,更好地監控和調優你服務器上的 PHP-FPM。

浙公網安備 33010602011771號