博客 / 詳情

返回

PHP執行時間那點事

説起php的執行時間,相信每一個phper都遇到過這方面的問題,特別是在CGI模式下,一般我們都會通過修改max_execution_time或者在代碼開頭添加set_time_limit(0)來解決問題,但下面這個場景大家可能也曾經遇到過:
我們先將php.ini的執行時間設置為60S

max_execution_time = 60

再在代碼的開頭設置執行時間為60S,讓兩者統一
然後運行sleep讓程序模擬運行20S:

set_time_limit(60);
sleep(20);
echo 1;

會發現程序在執行到第16S的時候就報出了502 Bad Gateway

TIM截圖20191206163416.jpg

説好的可以執行60S呢?江湖規矩報錯先翻看日誌,查看php-fpm.log,可以發現有這麼一段信息

[06-Dec-2019 12:44:13] WARNING: [pool www] child 19910, script '/home/wwwroot/public/index.php' (request: "GET /index.php") execution timed out (16.120721 sec), terminating
[06-Dec-2019 12:44:13] WARNING: [pool www] child 19910 exited on signal 15 (SIGTERM) after 2573.443300 seconds from start
[06-Dec-2019 12:44:13] NOTICE: [pool www] child 21861 started

這三行日誌分別告訴了我們三個信息
1.子進程19910的執行時間超過了16S被終結了
2.子進程19910在啓動了2573.44S後被關閉了
3.子進程19910在關閉後的同一秒子進程child 21861被fork出來開始運行

也就是説,PHP-CGI在執行的過程中,除去我們之前已經設置好的兩個參數,應該還有另外一個參數限制了這個進程的執行時間,打開php目錄下的php-fpm.conf看看有無異常

...
pm.min_spare_servers = 16
pm.max_spare_servers = 60
request_terminate_timeout = 15
request_slowlog_timeout = 0
slowlog = var/log/slow.log
...

可以看到有一個參數request_terminate_timeout = 15 和我們的超時閾值非常接近,翻看註釋找到關於這個參數的解釋:

; The timeout for serving a single request after which the worker process will
; be killed. This option should be used when the 'max_execution_time' ini option
; does not stop script execution for some reason. A value of '0' means 'off'.

大概的意思是這個參數的設置是當max_execution_time啓用時,為了防止php子進程因為某些原因無法停止運行而設置的一個保護措施,當然這個保護措施比較簡單粗暴,就是直接kill超時的子進程,然後直接fork一個新的

結合解釋,我們就很好理解前面日誌中出現的三條信息了:因為執行時間超過了max_execution_time設置的閾值,子進程19910被直接kill了,然後又生成了一個新的子進程21861

於是我們將php-fpm.conf中的request_terminate_timeout改為30,重啓php,再次執行之前的代碼,不再報502了

看到這裏,可能會有小夥伴會説,PHP的執行執行時間影響的參數有點多,真的記不住應該改那個才真正的有效,我們不妨從php運行的架構來梳理一下,不太清楚php運行架構的小夥伴可以看看我之前寫的《淺析PHP-FPM、CGI、Fast CGI的關係》

php-fpm架構.jpg

PHP-FPM的程序架構是由一個master進程來進行管理一個PHP-CGI的子進程池,當一個請求由master進程轉發到worker時,master進程便會開始計時,當超過設定的執行時間時master進程,便會直接kill掉超時的worker進程(程序的世界也不好混),而我們設置max_execution_time設置的時間是對於workder進程而言的,所以無論單個worker進程的執行時間設置多少,都不得超過fpm中的request_terminate_timeout,否則一律kill


文末,再補充兩條在翻看手冊時翻到的知識點:

在代碼中使用set_time_limit()會從零開始重新啓動超時計數器

換句話説,如果超時默認是30秒,在腳本運行了了25秒時調用 set_time_limit(20),那麼,腳本在超時之前可運行總時間為45秒。
這個相對簡單,就不上測試代碼了,大家有空可以驗證一下

set_time_limit()函數和配置指令max_execution_time隻影響腳本本身執行的時間

其他發生在諸如使用system()的系統調用,流操作,數據庫操作等的腳本執行的最大時間不包括其中。也就是説,比如sleep或者file_get_contents等操作消耗的時間是不會計入max_execution_time的超時時間中的

所以其實我前文寫的代碼即使把sleep設置為999也不會報執行超時的錯誤
,用代碼驗證下:

set_time_limit(10);
sleep(20);

可以發現程序確實沒有報超時的錯誤,接着我們再編寫一段代碼,讓php執行非系統調用和數據流等操作的耗時任務

set_time_limit(10); //將計數器清零,允許執行時間為10S
sleep(10);
$json[] = str_repeat("123456789,",10000);   //生成一個內容量較大的數組
$count = 1000000;
//循環執行1000000次數據,json_encode和json_decode在對於長字符串的轉化效率不高,所以比較耗時
while ($count--){
    $string = json_encode($json);
    json_decode($string,true);
}

TIM截圖20191206162953.jpg

結果如上圖,程序一共執行了20S,其中有10S是在sleep也就是系統調用不算入執行超時時間,另外10s執行的是一段cpu密集型的操作,符合算入max_execution_time超時時間的要求,於是符合條件拋出錯誤

文末總結:有空可以多翻翻手冊,每天都有新發現

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.