面試-PHP篇-PHP-FPM多進程模型

 

此篇爲系列第一篇:PHP-FPM的多進程模型。那麼,我們談論PHP-FPM多進程模型的時候,作爲PHPer的你,可能需要先看看下面一些關於PHP-FPM的多進程模型,是否都有所瞭解

①: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資源,也無法及時應對瞬時高併發,甚至可能短時間造成5xx錯誤。

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

PHP-FPM的進程管理方式和Nginx的進程管理方式類似,在處理用於請求上,並非是主進程接受請求後轉給子進程,而是子進程搶佔式的接受用戶的請求,本質上,其實PHP-FPM的多進程,以及Nginx的多進程,其實都是主進程監聽的同一個端口(被動套接字)後,fork子進程達到多個進程監聽同一個端口的目的。 Linux系統,所有的進程IO操作,都需要和操作系統打交道,也就是說,所有IO操作,操作系統都知道,而這個過程,也就是我們常說的“系統調用”。我們可以從系統調用入手解決這個問題。 系統調用的查看,可以使用strace。

對於如何驗證相對簡單,有2種方式;其一,看php-fpm進程的日誌,這需要配置好合適的php-fpm日誌格式;其二,既然IO數據會通過內核態過度到用戶態進程,那麼,我們通過strace -p <pid>命令去跟蹤系統調用即可。分別跟蹤php-fpm的主進程id以及php-fpm子進程id,然後訪問nginx,由nginx通過fast-cgi協議轉到php-fpm進程上,看在哪個進程上發送了系統調用。

3、爲何在PHP-FPM模式下,PHP代碼很少有人去做連接池

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

當然,PHP-FPM之所以這麼做,①:目的是讓開發不需要關心資源的回收的處理,所以可能你沒怎麼關心過網絡的關閉、文件描述符的關閉等等。②:減少內存溢出的情況。

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

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

4、PHP-FPM模式性能差的體現有哪些,如何優化

先思考爲何性能差,一個應用的性能如果說差,往往會從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、PHP-FPM模式下的Yac爲何無法和Cli模式無法共享內存

我們知道,PHP擴展開發中,首要執行的一個宏,便是 PHP_MINIT_FUNCTION,Yac擴展,需要在PHP-FPM進程啓動的時候,便初始化一塊共享內存,供各個進程來共享使用,因此,要能共享,關鍵就在於需要一個相同的標識,各個進程都知道纔可以。Yac擴展的初始化流程爲:

 

PHP_MINIT_FUNCTION->yac_storage_startup->yac_allocator_startup->create_segments

我們查看 create_segments 的具體實現:

 

static int create_segments(size_t requested_size, zend_shared_segment_posix ***shared_segments_p, int *shared_segments_count, char **error_in)
{
    zend_shared_segment_posix *shared_segment;
    char shared_segment_name[sizeof("/ZendAccelerator.") + 20];

    *shared_segments_count = 1;
    *shared_segments_p = (zend_shared_segment_posix **) calloc(1, sizeof(zend_shared_segment_posix) + sizeof(void *));
    if (!*shared_segments_p) {
        *error_in = "calloc";
        return ALLOC_FAILURE;
    }
    shared_segment = (zend_shared_segment_posix *)((char *)(*shared_segments_p) + sizeof(void *));
    (*shared_segments_p)[0] = shared_segment;

  // 這裏打開共享內存塊需要的Id,也就是 shared_segment_name
    sprintf(shared_segment_name, "/ZendAccelerator.%d", getpid());
    
    // 這裏,打開一塊共享內存
    shared_segment->shm_fd = shm_open(shared_segment_name, O_RDWR|O_CREAT|O_TRUNC, 0600);
    if (shared_segment->shm_fd == -1) {
        *error_in = "shm_open";
        return ALLOC_FAILURE;
    }

上面做了一些註釋,最關鍵的是開啓共享內存需要的系統ID,shared_segment_name,此值,包含了進程的ID。也就是php-fpm的主進程id。這就是,PHP-FPM模式所有進程間能夠通信的奧祕所在(它們有相同的共享內存標識ID)。而,如果我們是想要通過PHP腳本,使用yac擴展讀取這個共享內存,會這樣做:

 

$yac = new Yac();
$key = "something"
$yac->get($key);

在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是什麼。

 

 

 

Nginx+PHP-FPM運行原理

CGI

common gateway interface (公共網關接口)

請求模式:
    Web Brower(瀏覽器) ----(通過http協議傳輸)----> Http Server(服務器nginx/apache) -----> CGI Program -----> Db

Server 與 CGI 通過 STDIN/STDOUT(標準的輸入/輸出)進行數據傳遞
nginx(動態加載模塊) apache(指定加載模塊)

CGI工作原理

每當客戶請求CGI的時候,WEB服務器就請求操作系統生成一個新的CGI解釋器進程(如php-cgi.exe),
CGI 的一個進程則處理完一個請求後退出,下一個請求來時再創建新進程。
當然,這樣在訪問量很少沒有併發的情況也行。可是當訪問量增大,併發存在,這種方式就不 適合了。於是就有了fastcgi。

FastCGI

像是一個常駐(long-live)型的CGI,它可以一直執行着,只要激活後,
不會每次都要花費時間去fork一次(這是CGI最爲人詬病的fork-and-execute 模式)。

一般情況下,FastCGI的整個工作流程是這樣的:
    1.Web Server啓動時載入FastCGI進程管理器(IIS ISAPI或Apache Module)
    2.FastCGI進程管理器自身初始化,啓動多個CGI解釋器進程(可見多個php-cgi)並等待來自Web Server的連接。
    3.當客戶端請求到達Web Server時,FastCGI進程管理器選擇並連接到一個CGI解釋器。 Web server將CGI環境變量和標準輸入發送到FastCGI子進程php-cgi。
    4.FastCGI 子進程完成處理後將標準輸出和錯誤信息從同一連接返回Web Server。
      當FastCGI子進程關閉連接時, 請求便告處理完成。
      FastCGI子進程接着等待並處理來自FastCGI進程管理器(運行在Web Server中)的下一個連接。 
      在CGI模式中,php-cgi在此便退出了。

php-fpm(PHP內置的一種fast-cgi)

php-fpm即php-Fastcgi Process Manager.
php-fpm是 FastCGI 的實現,並提供了進程管理的功能。
進程包含 master 進程和 worker 進程兩種進程。
master 進程只有一個,負責監聽端口,接收來自 Web Server 的請求,而 worker 進程則一般有多個(具體數量根據實際需要配置),
每個進程內部都嵌入了一個 PHP 解釋器,是 PHP 代碼真正執行的地方。

請求步驟

Web Brower(瀏覽器訪問) www.example.com
|
        |
   通過http協議傳輸  
|
        |
    http server
 (服務器nginx/apache)            
|
        |
     配置解析    
路由到 www.example.com/index.php
|
        |
加載 nginx 的 fast-cgi 模塊
|
        |
fast-cgi 監聽 127.0.0.1:9000 地址
通過 fast-cgi 協議將請求轉發給 php-fpm 處理
|
        |
請求到達 127.0.0.1:9000
|
        |
php-fpm 監聽 127.0.0.1:9000
可通過 php-fpm.conf 進行修改

 

 


 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章