PHP 技能精進之 PHP-FPM 多進程模型

2020-08-13 10:36:00

PHP-FPM 提供了更好的 PHP 進程管理方式,可以有效控制記憶體和進程、可以平滑過載PHP設定。那麼當我們談論 PHP-FPM 多進程模型的時候,作爲 PHPer 的你瞭解多少呢?

首先,讓我們一起看幾個問題:

①:PHP-FPM 啓動進程的方式主要有哪幾種,區別是什麼?

②:PHP-FPM,是主進程接收請求轉給子進程,還是子進程單獨接收請求並處理,如何驗證?

③:爲何在 PHP-FPM 模式下,PHP 程式碼很少有人去做連線池?

④:PHP-FPM 模式效能差的體現有哪些,如何優化?

⑤:PHP-FPM 模式下的 YAC 爲何無法和 CLI 模式無法共用記憶體?

1. 如何啓動進程

PHP-FPM 是多進程模式,由 Master 進程管理 Worker 進程。進程的數量,都可以通過 php-fpm.conf 做具體設定。 PHP-FPM 的進程可以分爲動態模式及靜態模式:

①:靜態(Static)

直接開啓指定數量的 PHP-FPM 進程,不再增加或者減少;啓動固定數量的進程,佔用記憶體高。但在使用者請求波動大的時候,對 Linux 操作系統進程的處理上耗費的系統資源低。

②:動態(Dynamic)

開始時開啓一定數量的 PHP-FPM 進程,當請求量變大的時候,動態增加 PHP-FPM 進程數到上限,當空閒的時候自動釋放空閒進程數到一個下限。

動態模式會根據 max、min、idle children 設定,動態的調整進程數量。在使用者請求較爲波動,或者瞬間請求增高的時候,動態模式下會進行大量進程的建立、銷燬等操作,而造成 Linux 負載波動升高。簡單來說,請求量少,PHP-FPM 進程數少,請求量大,進程數多。優勢就是,當請求量小的時候,進程數少,記憶體佔用也小。

③:按需 (Ondemand)

這種模式下,PHP-FPM 的 Master 不會 Fork 任何子進程,純粹就是按需啓動。

這種模式通常很少使用,因爲它基本無法適應有一定量級的線上業務。由於 php-fpm 是短連線的,所以每次請求都會先建立連線,建立連線的過程必然會觸發上圖的執行步驟。所以,在大流量的系統上 Master 進程會變得繁忙,佔用系統 CPU 資源,不適合大流量環境的部署。

借用一張網路圖片來說明:

需要注意 2 個點,「連線」和「數據」到來。有連線進來再 Fork 進程,同樣可以達到子進程繼承父進程上下文,然後子進程處理使用者請求這個目的。

(關於動態、靜態進程模式的相關參數,可參考 PHP 官方文件。)

我們需要關注的是對於我們自身的業務,應該選擇的 PHP-FPM 模式爲動態還是靜態。

通常來說,對於比較大記憶體的伺服器,設定爲靜態的話會提高效率。因爲頻繁開關 php-fpm 進程也會有時滯,所以記憶體夠大的情況下開靜態效果會更好。數量也可以根據 記憶體/30M 得到。比如說 2GB 記憶體的伺服器,可以設定爲 50;4GB 記憶體可以設定爲 100 等。高配機器選靜態,低配機器(省記憶體)選動態,高配機器用動態不能充分利用記憶體資源和 CPU 資源,也無法及時應對瞬時高併發。

2. 如何進行請求處理和驗證

PHP-FPM 的進程管理方式和 Nginx 的進程管理方式有些類似。在處理請求時,並非由主進程接受請求後轉給子進程,而是子進程「搶佔式」地接受使用者請求。本質上 PHP-FPM 多進程以及 Nginx 多進程,都是在主進程監聽同一個埠後,Fork 子進程達到多個進程監聽同一埠的目的。

Linux 系統所有的進程 IO 操作,都需要和操作系統打交道。也就是說,系統知道所有 IO 操作。這個過程就是我們常說的「系統呼叫」。我們可以從系統呼叫入手解決這個問題。系統呼叫的檢視,可以使用 Strace。

如何驗證相對簡單,我們可以採取 2 種方式:

  • 看 PHP-FPM 進程的日誌。這需要設定好合適的 PHP-FPM 日誌格式;
  • 既然 IO 數據會通過內核態過度到使用者態進程,我們可以通過 strace -p <pid> 命令去跟蹤系統呼叫。分別跟蹤 PHP-FPM 的主進程 ID 以及子進程 ID,然後存取 Nginx,由 Nginx 通過 fast-cgi 協定轉到 PHP-FPM 進程上,看在哪個進程上發送了系統呼叫。

3. 爲何不在 PHP-FPM 下做程式碼連線池 ?

首先,在 PHP-FPM 模式下,一個請求的生命週期註定只有 1 次。也就是說,從 FPM 請求到請求、解析 PHP 指令碼,到 FPM 的 Zend 虛擬機器分配資源執行,再到最後的處理結束,PHP-FPM 會回收這次請求的所有資源。

這種方式一是爲了讓開發不需要關心資源的回收處理,所以你可能沒怎麼關心過網路的關閉、檔案描述符的關閉等等。二是爲了減少記憶體溢位的情況。

如果在這種模式下,你實現了連線池,也意味着請求結束,連線池消失,做了一次無用功而已。

「雞肋的」PConnect(持久化鏈接)。持久化鏈接也就是鏈接不釋放。但問題在於,PHP-FPM 是多進程模式,而持久化的鏈接存在於進程中。這就意味着,如果一臺機器有 300 個 FPM 進程,會一次性初始化 300 個持久化鏈接。如果因爲面臨業務活動需求冒然對機器擴容,很可能造成業務的數據庫連線數直接打滿。

4. 如何優化效能

首先,我們應該思考導致效能差可能的原因是什麼。如果一個應用的效能差,我們往往會從 2 個方面來分析,一個是 IO 效能,一個是計算效能。

IO 方面,因爲 PHP-FPM 模式下難以做連線池,所以高併發業務下的網路處理會有劣勢。注意我這裏一直說的都是 PHP-FPM 模式下,在 CLI 模式下還是可以自己做連線池的。只不過這個連線池僅限於 CLI 模式的單進程內,而且這個模式不能用於處理網路請求(比如 HTTP 請求)。因爲 PHP 預設單進程模式,FPM、CLI 都是預設單進程,即便 CLI 可以做連線池 ,也不方便做鏈接保活(不能同時做心跳檢測)。

計算效能上來說,雖然 PHP 是用 C 寫的,如果單純論計算效能是不錯的。但問題在於 PHP 處理請求時,每次都要解析 PHP 指令碼、翻譯 PHP 程式碼爲 Opcode、用 Zend 虛擬機器執行 Opcode,處理結束,釋放資源。經歷這樣的過程 是導致 PHP 計算效能慢的最大原因之一。

如何優化:

  • 對於計算效能來說,使用 Zend OPcache 擴充套件,快取位元組碼。
  • 對於** IO 效能**來說,使用檔案 cache 或者 memcached 減輕對網路 Cache 的壓力;使用 Yac 減輕對 Cache 層的壓力;在同一次請求中;複用鏈接不要每次都用新的;合理設計日誌元件類庫,優化 Logger 減少對檔案操作的次數來減少 IO 的壓力。

關於設計一個合格的 Logger 元件,我們需要注意幾個點:

① 每次請求,只做一次日誌寫操作,不要每次別人呼叫你的函數,你都去執行一次類似 file_put_contents 的操作。

② 相容各種類似錯誤。換句話說,即使 PHP fatal error 了,你也得能把知名錯誤之前的日誌記錄下來。這個實現可以藉助 PHP 類的解構方法來做。也可以使用更好的 register_shutdown_function 來註冊一個勾點,在 PHP 請求結束的時候,回撥此勾點,完成做最後的日誌操作。

5. YAC 爲何無法和 CLI 模式共用記憶體

我們知道,PHP 擴充套件開發中首要執行的一個宏是 PHP_MINIT_FUNCTION。YAC 擴充套件需要在 PHP-FPM 進程啓動時起就初始化一塊共用記憶體,供各個進程來共用使用。因此,實現共用的關鍵在於需要一個讓各個進程都知道的相同標識。

YAC 擴充套件的初始化流程爲:

我們檢視 create_segments 的具體實現:

上面做了一些註釋,最關鍵的是要開啓共用記憶體需要的系統 ID,shared_segment_name,此值包含了進程 ID。也就是 PHP-FPM 的主進程 ID。有相同的共用記憶體標識 ID,就是 PHP-FPM 模式所有進程間能夠通訊的奧祕所在。而如果我們是想要通過 PHP 指令碼使用 yac 擴充套件讀取這個共用記憶體,會這樣做:

在 CLI 模式下,這樣是不可能拿到 PHP-FPM 模式下設定的共用記憶體數據的。因爲 CLI 模式下執行 PHP 指令碼、進程 ID,和 PHP-FPM 模式下的進程 ID 完全不相同。

後面的文章中,我們會找機會講一講進程間通訊,以及基於共用記憶體的通訊。總結來說,多進程要共用記憶體通訊,必須要一開始就協調好一個唯一 ID。這個 ID 多個進程間都要知道。PHP-FPM 是多進程,主進程 fork 子進程出來,子進程自然知道這個唯一 ID 是什麼(因爲 Linux 進程 fork 會把整個進程的堆疊記憶體都 fork 一遍)。但是,php a.php 這樣執行,其實是一個完全獨立的進程,和 PHP-FPM 沒任何關係,這樣的進程,也就不能知道 PHP-FPM 進程裡的那個唯一 ID 是什麼。

轉載地址:https://zhuanlan.zhihu.com/p/100178217