一、概要
php.ini 和 php-fpm.conf 中有很多超時相關的配置,那么這些配置到底有什么作用呢?在源碼中又是怎么實現(xiàn)的呢?這篇文章就來講講下面幾種超時配置:
php.ini
- max_execution_time
- max_input_time
php-fpm.conf
- process_control_timeout
- request_terminate_timeout
- request_slowlog_timeout
運行環(huán)境: Mac 10.14.2 + PHP 7.3.7
二、配置解析規(guī)則
解析規(guī)則
php.ini的解析是在php_module_startup()階段完成,ini_entry是在 main.c 中為每個php.ini配置定義的解析規(guī)則,格式如下:
ZEND_INI_ENTRY3_EX(name, default_value, modifiable, on_modify, arg1, arg2, arg3, displayer)
PHP為不同類型的配置定義了很多宏,ZEND_INI_ENTRY3_EX 是它們展開后的最終宏,比如PHP_INI_ENTRY宏
PHP_INI_ENTRY(name, default_value, modifiable, on_modify)
參數(shù)解釋
name: 配置名稱
default_value: 配置默認值
modifiable: 配置的可被設定范圍
這些模式?jīng)Q定著一個 PHP 的指令在何時何地,是否能夠被設定。手冊中的每個指令都有其所屬的模式。例如有些指令可以在 PHP 腳本中用 ini_set() 來設定,而有些則只能在 php.ini 或 httpd.conf 中。
例如 output_buffering 指令是屬于 PHP_INI_PERDIR,因而就不能用 ini_set() 來設定。但是 display_errors 指令是屬于 PHP_INI_ALL 因而就可以在任何地方被設定,包括 ini_set()。
模式 含義 PHP_INI_USER 可在用戶腳本(例如 ini_set())或 Windows 注冊表(自 PHP 5.3 起)以及 .user.ini 中設定 PHP_INI_PERDIR 可在 php.ini,.htaccess 或 httpd.conf 中設定 PHP_INI_SYSTEM 可在 php.ini 或 httpd.conf 中設定 PHP_INI_ALL 可在任何地方設定
on_modify: 配置修改函數(shù)
三、max_input_time、max_execution_time
因為max_input_time 和 max_execution_time 聯(lián)系比較密切,所以放在一起來講。
php.ini 解釋
max_input_time
; Maximum amount of time each script may spend parsing request data. It's a good
; idea to limit this time on productions servers in order to eliminate unexpectedly
; long running scripts.
; Note: This directive is hardcoded to -1 for the CLI SAPI
; http://php.net/max-input-time翻譯過來就是:
max_input_time是每個腳本可以花在解析請求數(shù)據(jù)上的最大時間。在生產(chǎn)服務器上通過限制max_input_time可以清除掉長時間運行的腳本。在CLI模式下會硬編碼為-1,即無限制。max_execution_time
; Maximum execution time of each script, in seconds
; http://php.net/max-execution-time
; Note: This directive is hardcoded to 0 for the CLI SAPI翻譯:
max_execution_time是每個腳本的最大可執(zhí)行時間。在CLI模式下硬編碼為0
配置解析規(guī)則
// max_input_time,默認值為無限制
STD_PHP_INI_ENTRY("max_input_time", "-1", PHP_INI_SYSTEM|PHP_INI_PERDIR, OnUpdateLong, max_input_time, php_core_globals, core_globals)
// max_execution_time,默認值為30s,修改函數(shù)為OnUpdateTimeout
PHP_INI_ENTRY("max_execution_time", "30", PHP_INI_ALL, OnUpdateTimeout)
OnUpdateTimeout()函數(shù)如下,由第二節(jié)可知配置解析發(fā)生在php_module_startup()階段,此時EG(timeout_seconds)被賦值為了max_execution_time,但還沒有設置定時器。
// main.c
static PHP_INI_MH(OnUpdateTimeout)
{
if (stage==PHP_INI_STAGE_STARTUP) {
/* Don't set a timeout on startup, only per-request */
/* EG(timeout_seconds) = max_execution_time */
ZEND_ATOL(EG(timeout_seconds), ZSTR_VAL(new_value));
return SUCCESS;
}
zend_unset_timeout();
ZEND_ATOL(EG(timeout_seconds), ZSTR_VAL(new_value));
zend_set_timeout(EG(timeout_seconds), 0);
return SUCCESS;
}
設置超時定時器
// main.c
int php_request_startup(void)
{
......
if (PG(max_input_time) == -1) {
zend_set_timeout(EG(timeout_seconds), 1);
} else {
zend_set_timeout(PG(max_input_time), 1);
}
......
}
int php_execute_script(zend_file_handle *primary_file)
{
......
if (PG(max_input_time) != -1) {
zend_set_timeout(INI_INT("max_execution_time"), 0);
}
......
}
從上面代碼可以看到,如果設置了max_input_time(即值不等于-1,-1可以認為是在CLI模式下),在php_request_startup()階段會設置一個定時器,超時時間為max_input_time;在php_execute_script()階段會重新設置一個定時器,超時時間為max_execution_time。那么整個PHP腳本執(zhí)行的最大執(zhí)行時間就等于max_input_time + max_execution_time。
如果沒有設置max_input_time的話(即值等于-1),在php_request_startup()階段也會設置一個定時器,但超時時間被設為了EG(timeout_seconds),而EG(timeout_seconds)已經(jīng)在php_module_startup()階段被賦值為max_execution_time,所以此時的超時時間就是max_execution_time;在php_execute_script()階段不會重新設置定時器,前一階段設置的max_execution_time定時器仍然生效著。那么整個PHP腳本的最大執(zhí)行時間就是max_execution_time。

zend_set_time() 使用setitimer(ITIMER_PROF, &t_r, NULL); 來實現(xiàn)定時器,ITIMER_PROF會統(tǒng)計包括用戶態(tài)和內(nèi)核態(tài)下所花費的時間,而像sleep()這樣的系統(tǒng)調(diào)用會讓進程掛起,不占用cpu時間片,所以這倆超時時間是不包括sleep()時間的。
當定時器到時間后,ZendVM會拋出E_ERROR,即Fatal error錯誤。
四、process_control_timeout
php-fpm.conf 解釋
; Time limit for child processes to wait for a reaction on signals from master.
; Available units: s(econds), m(inutes), h(ours), or d(ays)
; Default Unit: seconds翻譯:
process_control_timeout是留給子進程處理來自master進程信號的時間限制。
分析
當master進程接收到SIGINT、SIGTERM、SIGQUIT、SIGUSR2這些信號時,會調(diào)用fpm_pctl()來進行處理。
首先master進程會根據(jù) 接收到的信號 和 當前fpm的運行狀態(tài) 來決定發(fā)送給worker進程的是SIGQUIT還是SIGTERM信號,同時注冊時間為process_control_timeout的定時事件。
如果在process_control_timeout時間內(nèi)子進程沒有退出,那么master進程會升級SIGQUIT為SIGTERM,SIGTERM為SIGKILL,并注冊1s的定時事件。SIGKILL就直接終止worker進程了,SIGTERM還能再給worker進程1s的時間。
綜上,process_control_timeout可以理解為master進程留給worker進程結束自己的時間,要是到時間worker還沒搞定那就開始master自己的策略了。
五、request_terminate_timeout、request_slowlog_timeout
因為request_terminate_timeout 和 request_slowlog_timeout 聯(lián)系比較密切,所以放在一起來講。
php-fpm.conf 解釋
request_terminate_timeout
; 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'.
; Available units: s(econds)(default), m(inutes), h(ours), or d(ays)
; Default Value: 0翻譯:執(zhí)行一個請求的超時時間,在這之后worker進程將被終止。這個選項應該被用在
max_execution_time這個ini選項由于某些原因不能停止腳本執(zhí)行的時候。request_slowlog_timeout
; The timeout for serving a single request after which a PHP backtrace will be
; dumped to the 'slowlog' file. A value of '0s' means 'off'.
; Available units: s(econds)(default), m(inutes), h(ours), or d(ays)
; Default Value: 0翻譯:執(zhí)行一個請求的超時時間,在這之后一個PHP的backtrace會被輸出到slowlog文件里。
分析
request_slowlog_timeout 和 request_terminate_timeout用在master進程的心跳檢測中(fpm_pctl_heartbeat()),心跳時間heartbeat 簡化后的算法是
- 在開啟
request_terminate_timeout情況下:request_terminate_timeout/1000*3 - 在未開啟
request_terminate_timeout情況下:request_slowlog_timeout/1000*3或者 0 request_terminate_timeout >= request_slowlog_timeout
第三條規(guī)則是為了保證slowlog不影響到正常的請求,heartbeat 取超時時間的1/3應該是為了避免心跳檢測過于頻繁,因為每次心跳檢測都需要遍歷所有worker進程。
如果超時事件發(fā)生了,那么將直接kill掉worker進程,kill(child_pid, SIGTERM); ,之后內(nèi)核回收資源關閉client_socket,nginx返回502錯誤給瀏覽器。