作为phper这么多年,很多时间都花在业务、框架上,反而一些细节不经意的就抽自己一巴掌。本文解析set_time_limit配置,以及相关细节。也记录一下此次掉坑经历,给自己以后指一个方向。
起因
服务器某个接口相应时间长达100s,而且报nginx 502错误。检查发现php max_execution_time使用的默认值30s。错误分析:502 gateway错误
,可以定位到是php脚本处理太慢造成的。但是让人怀疑的是为什么30s脚本不停止。
解析
官方解释
The set_time_limit() function and the configuration directive max_execution_time only affect the execution time of the script itself. Any time spent on activity that happens outside the execution of the script such as system calls using system(), stream operations, database queries, etc. is not included when determining the maximum time that the script has been running. This is not true on Windows where the measured time is real.
翻阅官方文档得到答案:该配置项仅仅代表脚本本身的执行时间,而不包括系统调用、流操作、数据库查询所占用的时间(即不计算 sleep,file_get_contents,shell_exec,mysql_query等花费的时间)。
起因中提到的问题就找到原因了:脚本本身查询mysql耗费了太久时间,导致达到nginx proxy_read_timeout 100
时间限制。nginx在发现proxy即fpm没有在规定时间内返回结果,就直接返回调用方502错误。
自我验证
<?php
error_reporting(0);
ini_set('display_errors', 'off');
// set_error_handler 不能捕获致命错误
register_shutdown_function(function () {
echo sprintf("脚本结束时间%f\n", microtime(true));
});
echo sprintf("脚本开始时间%f\n", microtime(true));
for($i=0;$i<100000000;$i++){
sha1(time());
}
echo sprintf("第一次循环结束时间%f\n", microtime(true));
set_time_limit(10);
$i = 0;
while ($i <= 10) {
echo "i=$i ";
sleep(2);
$i++;
}
$end = microtime(true);
var_dump(getrusage());
echo sprintf("\n第二次循环结束时间%f\n", microtime(true));
while (true) {
sha1(time());
}
echo sprintf("\n第三次循环结束时间%f\n", microtime(true));
// output
//脚本开始时间1651993104.293944
//第一次循环结束时间1651993191.067107
//i=0 i=1 i=2 i=3 i=4 i=5 i=6 i=7 i=8 i=9 i=10
//第二次循环结束时间1651993213.096094
//脚本结束时间1651993223.126825
- 第一次循环占用时间 86.77s (时间1)
- 第二次循环占用时间 22.02s (时间2)
- 第三次循环占用时间 10.03s (时间3)
脚本解析:
- 时间1是在set_time_limit执行前的脚本执行时间,说明set_time_limit执行的时候,时间计数器从0重新计数
- 时间2是因为sleep了11次,每次2s。说明循环本身只是占用了0.02s
- 时间3是set_time_limit开始到脚本Fatal Error退出执行之间的时间。虽然第二次循环占用了22s,但是sleep不被计数。
设置真实执行时间
<?php
echo sprintf("脚本开始时间%f\n", microtime(true));
pcntl_async_signals(1);
pcntl_signal(SIGALRM, function () {
echo sprintf("脚本结束时间%f\n", microtime(true));
exit('Stop it!');
});
pcntl_alarm(3);
$i = 0;
while (true) {
$i++;
sha1(time());
echo "i = {$i}\n";
}
echo sprintf("脚本结束时间2%f\n", microtime(true));
// output
脚本开始时间1651994735.305631
i=33万次...
脚本结束时间1651994738.307060
Stop it!
或者使用fork调用,父进程监控子进程运行。
$pid=pcntl_fork();
if ($pid) {
while (true) {
shell_exec('sleep 10&');
}
} else {
sleep(5);
posix_kill(posix_getppid(),SIGKILL);
}
如果是CLI脚本,有更多解决方案:
timeout 5 /usr/bin/php -q /path/to/script
总结
- 不建议使用set_time_limit(0), 因为脚本会一直执行下去,影响服务器负载
- 脚本默认max_execution_time为0,即会一直执行下去,建议设置该配置,因为脚本会永久占用进程。
- set_time_limit 会重置时间计数。比如在脚本已经运行20s的时候调用set_time_limit(10),那么脚本执行时间为30s。如果运行一个耗时任务的时候,一般放在脚本第一行。
- sleep,file_get_contents,shell_exec,mysql_query这些函数包含在设置的时限内,所以这些函数本身执行时间应该自己考虑在内,不要让时间浪费在没必要的事情上。
- set_time_limit 不能使用在安全模式下。
set_time_limit() [function.set-time-limit]: Cannot set time limit in safe mode