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)。
4. PHP-FPM配置項pm.max_children = 5,執行usleep(400)延遲,模擬線程數10,simulated.thread記錄如下:
Log thread 1540308253.6403 at 1540308253.6413(s) Log thread 1540308253.6419 at 1540308253.6427(s) Log thread 1540308253.6427 at 1540308253.644(s) Log thread 1540308253.6437 at 1540308253.6449(s) Log thread 1540308253.6453 at 1540308253.6467(s) Log thread 1540308253.6464 at 1540308253.6472(s)
很顯然,usleep(400)延遲時間內一旦某個模擬線程連接被關閉並執行失敗,后續模擬線程將無法生成並執行。
從以上的記錄可以看出:
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.