PHP-FPM進程池探祕

PHP 7.2以前的版本只支持多進程而不支持多線程;PHP 7.2+ pthreads 擴展提供了Thread、Worker、Threaded 對象,使得創建、讀取、寫入以及執行多線程成爲可能,並可以在多個線程之間進行同步控制;pthreads 多線程開發也僅限於命令行模式,不能用於 web 服務器環境中。

PHP-FPM 在進程池中運行多個子進程併發處理所有連接請求。通過 ps 查看PHP-FPM進程池(pm.start_servers = 2)狀態如下:

root@d856fd02d2fe:~# ps aux -L
USER       PID   LWP %CPU NLWP %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1     1  0.0    1  0.0   4504   692 ?        Ss   13:10   0:00 /bin/sh /usr/local/php/bin/php-fpm start
root         7     7  0.0    1  0.4 176076 19304 ?        Ss   13:10   0:00 php-fpm: master process (/usr/local/php/etc/php-fpm.conf)
www-data     8     8  0.0    1  0.2 176076  8132 ?        S    13:10   0:00 php-fpm: pool www
www-data     9     9  0.0    1  0.2 176076  8132 ?        S    13:10   0:00 php-fpm: pool www
root        10    10  0.0    1  0.0  18376  3476 ?        Ss   14:11   0:00 bash
root        66    66  0.0    1  0.0  34420  2920 ?        R+   15:13   0:00 ps aux -L

從列表中可以看出,進程池www中有兩個尚處於空閒狀態的子進程PID 8和 PID 9。注:NLWP指輕量級進程數量,即線程數量。

 

爲什麼需要PHP-FPM(FastCGI Process Manager)?


 

FastCGI is a kind of CGI which is long-live, which will always be running.

 

PHP-CGI is one kind of the Process Manager of FastCGI, which is within php itself.After changing php.ini, you should reboot PHP-CGI to make the new php.ini work.When a PHP-CGI process is killed, all the PHP code will cannot run.

 

PHP-FPM is another kind of the Process Manager of FastCGI.PHP-FPM can be used to control sub processes of PHP-CGI.

  • FastCGI是語言無關的、可伸縮架構的CGI開放擴展,其主要行爲是將CGI解釋器進程一直保持在內存,不是fork-and-execute,並因此獲得較高的性能。FastCGI支持分佈式部署,可以部署在WEB服務器以外的多個主機上。
  • PHP-CGI作爲PHP自帶的PHP FastCGI管理器對FastCGI的管理方式簡單,也不夠靈活高效。
  • PHP-FPM爲了解決PHP-CGI的不足,爲PHP FastCGI提供了一種新的進程管理方式,可以有效控制進程,平滑重載PHP配置,其master process是常駐內存的,worker process有static、dynamic、ondemand三種管理方式。PHP-FPM進程池中的CGI在接受並處理完pm.max_requests個用戶請求後將會respawn,並不會保證單個CGI是long-live and always be running,而會以更加靈活高效的方式來保證客戶端的連接請求可以被多個CGI處理。
       static  - a fixed number (pm.max_children) of child processes;
       dynamic - the number of child processes are set dynamically based on the
                 following directives. With this process management, there will be
                 always at least 1 children.
                 pm.max_children      - the maximum number of children that can
                                        be alive at the same time.
                 pm.start_servers     - the number of children created on startup.
                 pm.min_spare_servers - the minimum number of children in 'idle'
                                        state (waiting to process). If the number
                                        of 'idle' processes is less than this
                                        number then some children will be created.
                 pm.max_spare_servers - the maximum number of children in 'idle'
                                        state (waiting to process). If the number
                                        of 'idle' processes is greater than this
                                        number then some children will be killed.
      ondemand - no children are created at startup. Children will be forked when
                 new requests will connect. The following parameter are used:
                 pm.max_children           - the maximum number of children that
                                             can be alive at the same time.
                 pm.process_idle_timeout   - The number of seconds after which
                                             an idle process will be killed.

     

PHP-FPM探祕手段:模擬多線程併發執行


1. 什麼是線程:參考本人此篇 爲什麼要使用線程 。

2. 模擬多線程

 1 <?php
 2 /**
 3  * PHP 只支持多進程不支持多線程。
 4  *
 5  * PHP-FPM 在進程池中運行多個子進程併發處理所有請求,
 6  * 同一個子進程可先後處理多個請求,但同一時間
 7  * 只能處理一個請求,未處理請求將進入隊列等待處理
 8  *
 9  */
10 
11 class SimulatedThread
12 {
13     //模擬線程標識
14     private $threadID;
15 
16     //主機名
17     private $host = 'tcp://172.17.0.5';
18 
19     //端口號
20     private $port = 80;
21 
22     public function __construct()
23     {
24         //採用當前時間給線程編號
25         $this->threadID = microtime(true);
26     }
27 
28     /**
29      * 通過socket發送一個新的HTTP連接請求到本機,
30      * 此時當前模擬線程既是服務端又是模擬客戶端
31      *
32      * 當前(程序)子進程sleep(1)後會延遲1s才繼續執行,但其持有的請求是繼續有效的,
33      * 不能處理新的請求,故這種做法會降低進程池處理併發多個請求的能力,
34      * 類似延遲處理還有time_nanosleep()、time_sleep_until()、usleep()。
35      * 而且sleep(1)這種做法並不安全,nginx依然可能出現如下錯誤:
36      * “epoll_wait() reported that client prematurely closed connection,
37      * so upstream connection is closed too while connecting to upstream”
38      *
39      * @return void
40      */
41     public function simulate()
42     {
43         $run = $_GET['run'] ?? 0;
44         if ($run++ < 9) {//最多模擬10個線程
45             $fp = fsockopen($this->host, $this->port);
46             fputs($fp, "GET {$_SERVER['PHP_SELF']}?run={$run}\r\n\r\n");
47             sleep(1);//usleep(500) 將延遲 500 微妙(us),1 s = 1000000 us
48             fclose($fp);
49         }
50 
51         $this->log();
52     }
53 
54     /**
55      * 日誌記錄當前模擬線程運行時間
56      *
57      * @return void
58      */
59     private function log()
60     {
61         $fp = fopen('simulated.thread', 'a');
62         fputs($fp, "Log thread {$this->threadID} at " . microtime(true) . "(s)\r\n");
63 
64         fclose($fp);
65     }
66 }
67 
68 $thread = new SimulatedThread();
69 $thread->simulate();
70 echo "Started to simulate threads...";

 

PHP-FPM探祕彙總:本人通過運行上述腳本後,發現一些可預料但卻不是我曾想到的結果


1. PHP-FPM配置項pm.max_children = 5,執行sleep(1)延遲,模擬線程數10,simulated.thread記錄如下:

Log thread 1508054181.4236 at 1508054182.4244(s)
Log thread 1508054181.4248 at 1508054182.4254(s)
Log thread 1508054181.426 at 1508054182.428(s)
Log thread 1508054181.6095 at 1508054182.6104(s)
Log thread 1508054182.4254 at 1508054183.4262(s)
Log thread 1508054183.4272 at 1508054183.4272(s)
Log thread 1508054182.4269 at 1508054183.4275(s)
Log thread 1508054182.4289 at 1508054183.43(s)
Log thread 1508054182.6085 at 1508054183.6091(s)
Log thread 1508054182.611 at 1508054183.6118(s)

最新生成的(模擬)線程登記出現在紅色標示條目位置是因爲進程池的併發連接處理能力上限爲5,因此它只可能出現在第六條以後的位置。記錄的時間跨度 1508054183.6118 - 1508054181.4236 = 2.1882(s)。下面是同等條件下的另一次測試結果:

Log thread 1508058075.042 at 1508058076.0428(s)
Log thread 1508058075.0432 at 1508058076.0439(s)
Log thread 1508058075.0443 at 1508058076.045(s)
Log thread 1508058075.6623 at 1508058076.6634(s)
Log thread 1508058076.0447 at 1508058077.0455(s)
Log thread 1508058076.046 at 1508058077.0466(s)
Log thread 1508058077.0465 at 1508058077.0466(s)
Log thread 1508058076.0469 at 1508058077.0474(s)
Log thread 1508058076.6647 at 1508058077.6659(s)
Log thread 1508058076.6664 at 1508058077.6671(s)

有意思的是綠色條目代表的(模擬)線程和紅色條目代表的(模擬)線程的登記時間是一樣的,說明兩個(模擬)線程是併發執行的。記錄的時間跨度 1508058077.6671 - 1508058075.042 = 2.6251(s)。模擬線程數改爲51後,simulated.thread記錄如下:

Log thread 1508304245.2524 at 1508304246.3104(s)
Log thread 1508304245.3112 at 1508304246.3119(s)
Log thread 1508304245.461 at 1508304246.4619(s)
Log thread 1508304246.3131 at 1508304247.3141(s)
Log thread 1508304246.3432 at 1508304247.3439(s)
...
Log thread 1508304254.4762 at 1508304255.4767(s)
Log thread 1508304255.4768 at 1508304255.4768(s)
Log thread 1508304255.3284 at 1508304256.3292(s)
Log thread 1508304255.3584 at 1508304256.3593(s)
Log thread 1508304255.4757 at 1508304256.4763(s)

紅色條目代表的(模擬)線程創建時間最晚。記錄的時間跨度 1508304256.4763 - 1508304245.2524 = 11.2239(s)。

2. PHP-FPM配置項pm.max_children = 10,執行sleep(1)延遲,模擬線程數10,simulated.thread記錄如下:

Log thread 1508061169.7956 at 1508061170.7963(s)
Log thread 1508061169.7966 at 1508061170.7976(s)
Log thread 1508061169.7978 at 1508061170.7988(s)
Log thread 1508061170.2896 at 1508061171.2901(s)
Log thread 1508061170.7972 at 1508061171.7978(s)
Log thread 1508061171.7984 at 1508061171.7985(s)
Log thread 1508061170.7982 at 1508061171.7986(s)
Log thread 1508061170.7994 at 1508061171.8(s)
Log thread 1508061171.2907 at 1508061172.2912(s)
Log thread 1508061171.2912 at 1508061172.2915(s)

由於服務端併發連接處理能力上限達到10,因此最新生成的(模擬)線程登記可出現在任何位置。記錄的時間跨度 1508061172.2915 - 1508061169.7956 = 2.4959(s)。模擬線程數改爲51後,simulated.thread記錄如下:

Log thread 1508307376.5733 at 1508307377.5741(s)
Log thread 1508307376.5748 at 1508307377.5759(s)
...
Log thread 1508307382.5883 at 1508307383.589(s)
Log thread 1508307383.5898 at 1508307383.5899(s)
Log thread 1508307382.5896 at 1508307383.5904(s)
Log thread 1508307382.708 at 1508307383.7088(s)
Log thread 1508307382.7091 at 1508307383.7095(s)
...
Log thread 1508307382.716 at 1508307383.7166(s)
Log thread 1508307382.7172 at 1508307383.7178(s)
Log thread 1508307383.5883 at 1508307384.5891(s)

紅色條目代表的(模擬)線程創建時間最晚。記錄的時間跨度 1508307384.5891 - 1508307376.5733 = 8.0158(s)。

3. PHP-FPM配置項pm.max_children = 5,執行usleep(500)延遲,模擬線程數10,simulated.thread記錄如下:

Log thread 1508059270.3195 at 1508059270.3206(s)
Log thread 1508059270.3208 at 1508059270.3219(s)
Log thread 1508059270.322 at 1508059270.323(s)
Log thread 1508059270.323 at 1508059270.324(s)
Log thread 1508059270.3244 at 1508059270.3261(s)
Log thread 1508059270.3256 at 1508059270.3271(s)
Log thread 1508059270.3275 at 1508059270.3286(s)
Log thread 1508059270.3288 at 1508059270.3299(s)
Log thread 1508059270.3299 at 1508059270.331(s)
Log thread 1508059270.3313 at 1508059270.3314(s)

可見日誌記錄順序與(模擬)線程生成的順序一致,但除紅色標示條目外,其他條目看不出是併發執行的,更像是一個接一個串行順序執行完的。記錄的時間跨度 1508059270.3314 - 1508059270.3195 = 0.0119(s)。

 

從以上的記錄可以看出:

1)這些(模擬)線程是第一次請求執行腳本後就自動生成的,一個(模擬)線程觸發另一個(模擬)線程的創建;

2)這些(模擬)線程中有的雖是在同一個子進程空間中產生並運行的,但有先後順序,即前一個執行完退出後下一個才能創建並運行,再加上這些模擬線程實際上都是以多進程運行的,所以它們併發執行的效率比真正多線程併發執行效率要低。對此要提高併發處理能力的有效途徑是增加子進程數量,避免順序執行,減少連接請求進入隊列長時間等待的概率;

3)前後相鄰(模擬)線程生成時間間隔很小,幾乎是同時產生,或後一個(模擬)線程在前一個(模擬)線程尚未執行結束並退出之前產生,這是併發執行的條件;

4)多個(模擬)線程可以併發執行,也就是說它們模擬了對同一目標任務(這裏就是運行日誌登記,當然也可以是其他目標任務)的多線程併發處理。只是這裏多個(模擬)線程之間是完全獨立的,沒有共享當前進程資源,但是都擁有對磁盤文件simulated.thread的寫操作。

上述第4條說明模擬多線程的基本目標已實現,所以模擬多線程併發的實現是成功的,其最大好處就是可以充分利用進程池併發處理連接請求的能力。PHP-FPM進程池中同一個子進程可先後處理多個請求,但同一時間只能處理一個請求,未處理請求將進入隊列等待處理。換句話,同一個子進程不具有併發處理多個請求的能力。

 

PHP-FPM Pool配置:它允許定義多個池,每個池可定義不同的配置項。以下只是列舉了我在探祕過程中還關注過的其他部分配置項


1. listen:The address on which to accept FastCGI requests.它支持TCP Socket和unix socket兩種通訊協議。可設置listen = [::]:9000。

2. listen.allowed_clients:List of addresses (IPv4/IPv6) of FastCGI clients which are allowed to connect. 該配置項爲逗號分隔的列表,如listen.allowed_clients = 127.0.0.1,172.17.0.5。

3. pm:Choose how the process manager will control the number of child processes. 該配置項設置FPM管理進程池的方式,包括static、dynamic、ondemand三種。

4. pm.max_requests:The number of requests each child process should execute before respawning. This can be useful to work around memory leaks in 3rd party libraries.設置每個子進程處理請求數的上限,對於處理第三方庫中的內存泄漏很有用。

5. pm.status_path:The URI to view the FPM status page.

 

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