天天看點

工具系列 | FPM程序管理器詳解

FPM(FastCGI Process Manager)是PHP FastCGI運作模式的一個程序管理器,從它的定義可以看出,FPM的核心功能是程序管理,那麼它用來管理什麼程序呢?這個問題就需要從FastCGI說起了。

FastCGI是Web伺服器(如:Nginx、Apache)和處理程式之間的一種通信協定,它是與Http類似的一種應用層通信協定,注意:它隻是一種協定!

前面曾一再強調,PHP隻是一個腳本解析器,你可以把它了解為一個普通的函數,輸入是PHP腳本。輸出是執行結果,假如我們想用PHP代替shell,在指令行中執行一個檔案,那麼就可以寫一個程式來嵌入PHP解析器,這就是cli模式,這種模式下PHP就是普通的一個指令工具。

接着我們又想:能不能讓PHP處理http請求呢?這時就涉及到了網絡處理,PHP需要接收請求、解析協定,然後處理完成傳回請求。

在網絡應用場景下,PHP并沒有像Golang那樣實作http網絡庫,而是實作了FastCGI協定,然後與web伺服器配合實作了http的處理,web伺服器來處理http請求,然後将解析的結果再通過FastCGI協定轉發給處理程式,處理程式處理完成後将結果傳回給web伺服器,web伺服器再傳回給使用者,如下圖所示。

工具系列 | FPM程式管理器詳解

PHP實作了FastCGI協定的解析,但是并沒有具體實作網絡處理,一般的處理模型:多程序、多線程,多程序模型通常是主程序隻負責管理子程序,而基本的網絡事件由各個子程序處理,nginx、fpm就是這種模式;另一種多線程模型與多程序類似,隻是它是線程粒度,通常會由主線程監聽、接收請求,然後交由子線程處理,memcached就是這種模式,有的也是采用多程序那種模式:主線程隻負責管理子線程不處理網絡事件,各個子線程監聽、接收、處理請求,memcached使用udp協定時采用的是這種模式。

fpm的實作就是建立一個master程序,在master程序中建立并監聽socket,然後fork出多個子程序,這些子程序各自accept請求,子程序的處理非常簡單,它在啟動後阻塞在accept上,有請求到達後開始讀取請求資料,讀取完成後開始處理然後再傳回,在這期間是不會接收其它請求的,也就是說fpm的子程序同時隻能響應一個請求,隻有把這個請求處理完成後才會accept下一個請求,這一點與nginx的事件驅動有很大的差別,nginx的子程序通過epoll管理套接字,如果一個請求資料還未發送完成則會處理下一個請求,即一個程序會同時連接配接多個請求,它是非阻塞的模型,隻處理活躍的套接字。

fpm的master程序與worker程序之間不會直接進行通信,master通過共享記憶體擷取worker程序的資訊,比如worker程序目前狀态、已處理請求數等,當master程序要殺掉一個worker程序時則通過發送信号的方式通知worker程序。

fpm可以同時監聽多個端口,每個端口對應一個worker pool,而每個pool下對應多個worker程序,類似nginx中server概念。

工具系列 | FPM程式管理器詳解

在php-fpm.conf中通過<code>[pool name]</code>聲明一個worker pool:

啟動fpm後檢視程序:ps -aux|grep fpm

具體實作上worker pool通過<code>fpm_worker_pool_s</code>這個結構表示,多個worker pool組成一個單連結清單

接下來看下fpm的啟動流程,從<code>main()</code>函數開始:

<code>fpm_init()</code>主要有以下幾個關鍵操作:

(1)fpm_conf_init_main():

解析php-fpm.conf配置檔案,配置設定worker pool記憶體結構并儲存到全局變量中:fpm_worker_all_pools,各worker pool配置解析到<code>fpm_worker_pool_s-&gt;config</code>中。

(2)fpm_scoreboard_init_main(): 配置設定用于記錄worker程序運作資訊的共享記憶體,按照worker pool的最大worker程序數配置設定,每個worker pool配置設定一個<code>fpm_scoreboard_s</code>結構,pool下對應的每個worker程序配置設定一個<code>fpm_scoreboard_proc_s</code>結構,各結構的對應關系如下圖。

工具系列 | FPM程式管理器詳解

(3)fpm_signals_init_main():

這裡會通過<code>socketpair()</code>建立一個管道,這個管道并不是用于master與worker程序通信的,它隻在master程序中使用,具體用途在稍後介紹event事件處理時再作說明。另外設定master的信号處理handler,當master收到SIGTERM、SIGINT、SIGUSR1、SIGUSR2、SIGCHLD、SIGQUIT這些信号時将調用<code>sig_handler()</code>處理:

(4)fpm_sockets_init_main()

建立每個worker pool的socket套接字。

(5)fpm_event_init_main():

啟動master的事件管理,fpm實作了一個事件管理器用于管理IO、定時事件,其中IO事件通過kqueue、epoll、poll、select等管理,定時事件就是定時器,一定時間後觸發某個事件。

在<code>fpm_init()</code>初始化完成後接下來就是最關鍵的<code>fpm_run()</code>操作了,此環節将fork子程序,啟動程序管理器,另外master程序将不會再傳回,隻有各worker程序會傳回,也就是說<code>fpm_run()</code>之後的操作均是worker程序的。

在fork後worker程序傳回了監聽的套接字繼續main()後面的處理,而master将永遠阻塞在<code>fpm_event_loop()</code>,接下來分别介紹master、worker程序的後續操作。

<code>fpm_run()</code>執行後将fork出worker程序,worker程序傳回<code>main()</code>中繼續向下執行,後面的流程就是worker程序不斷accept請求,然後執行PHP腳本并傳回。整體流程如下:

(1)等待請求: worker程序阻塞在fcgi_accept_request()等待請求;

(2)解析請求: fastcgi請求到達後被worker接收,然後開始接收并解析請求資料,直到request資料完全到達;

(3)請求初始化: 執行php_request_startup(),此階段會調用每個擴充的:PHP_RINIT_FUNCTION();

(4)編譯、執行: 由php_execute_script()完成PHP腳本的編譯、執行;

(5)關閉請求: 請求完成後執行php_request_shutdown(),此階段會調用每個擴充的:PHP_RSHUTDOWN_FUNCTION(),然後進入步驟(1)等待下一個請求。

worker程序一次請求的處理被劃分為5個階段:

FPM_REQUEST_ACCEPTING: 等待請求階段

FPM_REQUEST_READING_HEADERS: 讀取fastcgi請求header階段

FPM_REQUEST_INFO: 擷取請求資訊階段,此階段是将請求的method、query stirng、request uri等資訊儲存到各worker程序的fpm_scoreboard_proc_s結構中,此操作需要加鎖,因為master程序也會操作此結構

FPM_REQUEST_EXECUTING: 執行請求階段

FPM_REQUEST_END: 沒有使用

FPM_REQUEST_FINISHED: 請求處理完成

worker處理到各個階段時将會把目前階段更新到<code>fpm_scoreboard_proc_s-&gt;request_stage</code>,master程序正是通過這個辨別判斷worker程序是否空閑的。

這一節我們來看下master是如何管理worker程序的,首先介紹下三種不同的程序管理方式:

static: 這種方式比較簡單,在啟動時master按照<code>pm.max_children</code>配置fork出相應數量的worker程序,即worker程序數是固定不變的

dynamic: 動态程序管理,首先在fpm啟動時按照<code>pm.start_servers</code>初始化一定數量的worker,運作期間如果master發現空閑worker數低于<code>pm.min_spare_servers</code>配置數(表示請求比較多,worker處理不過來了)則會fork worker程序,但總的worker數不能超過<code>pm.max_children</code>,如果master發現空閑worker數超過了<code>pm.max_spare_servers</code>(表示閑着的worker太多了)則會殺掉一些worker,避免占用過多資源,master通過這4個值來控制worker數

ondemand: 這種方式一般很少用,在啟動時不配置設定worker程序,等到有請求了後再通知master程序fork worker程序,總的worker數不超過<code>pm.max_children</code>,處理完成後worker程序不會立即退出,當空閑時間超過<code>pm.process_idle_timeout</code>後再退出

前面介紹到在<code>fpm_run()</code>master程序将進入<code>fpm_event_loop()</code>:

這就是master整體的處理,其程序管理主要依賴注冊的幾個事件,接下來我們詳細分析下這幾個事件的功能。

(1)sp[1]管道可讀事件:

在<code>fpm_init()</code>階段master曾建立了一個全雙工的管道:sp,然後在這裡建立了一個sp[0]可讀的事件,當sp[0]可讀時将交由<code>fpm_got_signal()</code>處理,向sp[1]寫資料時sp[0]才會可讀,那麼什麼時機會向sp[1]寫資料呢?前面已經提到了:當master收到注冊的那幾種信号時會寫入sp[1]端,這個時候将觸發sp[0]可讀事件。

工具系列 | FPM程式管理器詳解

這個事件是master用于處理信号的,我們根據master注冊的信号逐個看下不同用途:

SIGINT/SIGTERM/SIGQUIT: 退出fpm,在master收到退出信号後将向所有的worker程序發送退出信号,然後master退出

SIGUSR1: 重新加載日志檔案,生産環境中通常會對日志進行切割,切割後會生成一個新的日志檔案,如果fpm不重新加載将無法繼續寫入日志,這個時候就需要向master發送一個USR1的信号

SIGUSR2: 重新開機fpm,首先master也是會向所有的worker程序發送退出信号,然後master會調用execvp()重新啟動fpm,最後舊的master退出

SIGCHLD: 這個信号是子程序退出時作業系統發送給父程序的,子程序退出時,核心将子程序置為僵屍狀态,這個程序稱為僵屍程序,它隻保留最小的一些核心資料結構,以便父程序查詢子程序的退出狀态,隻有當父程序調用wait或者waitpid函數查詢子程序退出狀态後子程序才告終止,fpm中當worker程序因為異常原因(比如coredump了)退出而非master主動殺掉時master将受到此信号,這個時候父程序将調用waitpid()查下子程序的退出,然後檢查下是不是需要重新fork新的worker

具體處理邏輯在<code>fpm_got_signal()</code>函數中,這裡不再羅列。

(2)fpm_pctl_perform_idle_server_maintenance_heartbeat():

這是程序管理實作的主要事件,master啟動了一個定時器,每隔1s觸發一次,主要用于dynamic、ondemand模式下的worker管理,master會定時檢查各worker pool的worker程序數,通過此定時器實作worker數量的控制