如何捕获致命错误:PHP中超过30秒的最大执行时间

81

我一直在玩我的开发系统,成功导致了这个问题:

致命错误:超出30秒最大执行时间

当我做一些不现实的事情时发生了这个问题,但是它确实可能会发生在用户身上。

有人知道是否有一种方法可以捕获这个异常吗?我已经阅读了相关内容,但每个人似乎都建议增加允许的时间。


1
我相信一旦执行时间超过限制,脚本就会被终止。在这种情况下,可能已经捕获异常的脚本也已经被杀死了。 - horatio
致命错误无法被捕获(它们不是异常)或处理。您只能通过注册关闭函数来优雅地处理脚本终止,但脚本之后将会结束。 - Gordon
可能是如何捕获PHP致命错误的重复问题。 - Gordon
不,可能的重复并不是“完全相同”的。它并没有询问由脚本超时引起的致命错误,而只是一般情况下如何捕获致命错误。但由于一般情况也适用于这种特殊情况,因此它有资格成为可能的重复。如果问题是如何解决脚本超时错误,则它可能与这些问题大部分重复:http://stackoverflow.com/search?q=maximum+execution+time+php ;) - Gordon
我不能一般性地回答这个问题,但在某些情况下,您可以将某个操作的时间限制在小于max_execution_time的时间内,然后捕获它。例如,curl_setopt($client, CURLOPT_TIMEOUT, 27);会导致curl在27秒后放弃,以避免致命错误的发生。 - cesoid
8个回答

98

9
这可能是我从未见过的 PHP 最好保守的秘密。太棒了。 - Anthony
2
我已经测试过了,但仍然显示致命错误消息。有可能避免它吗?无论如何,我使用了ini_set('display_errors', '0');。 - B L Praveen
8
sleep()实际上不会影响时间限制计数器,这很奇怪,但是确实如此。因此,您应该做一些其他事情来测试您的shutdown()函数 - 一对嵌套的for循环,每个循环迭代一百万次就可以了。 - Bobby Jack
4
虽然有点奇怪,但这是有道理的,因为让一个线程睡眠会暂停执行一段时间。由于它没有在执行,所以这段时间不被视为已经执行的时间。 - Joseph Pla
这是一个不错的选择。在我的情况下,使用这个函数可以作为最后的资源,通过JavaScript将用户重定向到另一个页面/服务器。 - Paulo Bueno
@BLPraveen 只需在该函数顶部放置 ob_get_clean()。 - Martin Zvarík

36

你唯一的选择是增加脚本允许执行的时间(将其设置为0将使它无限制,但这不是推荐的方法),或者创建一个新的线程并希望一切顺利。

之所以不能捕获这个错误是因为它实际上没有被抛出。代码中没有一行触发了这个错误,而是PHP说,“不行,很抱歉,这太长了。是时候关闭了。” 这是有道理的。想象一下,如果一个最大执行时间为30秒的脚本捕获了这个错误并花费了另外30秒的时间...在设计不良的程序中,这将开启一些相当恶意的攻击机会。至少,这将为DOS攻击创造机会。


将异常时间设置为0,然后执行自己的超时操作怎么样? - ingh.am
那在某些情况下可能有效,但并非所有情况都适用。最好使用某种循环形式,以便可以定期测试。 - cwallenpoole
嗯,我注意到这个问题是在接收数据包块时。我将数据包大小设置得非常小,发送时间非常长。我的想法是,如果有大量数据需要发送,将数据包大小设置为1024字节之类的大小,我可以在循环中设置自己的超时时间,并在几秒钟内做出反应以警告用户。 - ingh.am
5
我反对被PHP“看护”,无论是关于编程漏洞是否允许无限循环或DOS攻击等问题。我认为应该由程序员来处理错误。也许超时发生在cURL期间,我希望页面可以无限刷新直到重新连接到URL(因为T1线路断开了,或者远程服务器出了问题,谁知道呢?)。我不想让我的预处理器,无论是php、asp、cgi等,为我做出决定。我认为预处理器,甚至客户端语言如JavaScript都不应该代表我做出道德或伦理方面的决策。 - TARKUS
你可以选择其他与超时相关的选项,但这些不在本问答中涵盖。最常见的做法是确保请求不会超过60秒。此时,您也不需要面向公众。 - cwallenpoole

22

这不是一个异常,而是一个错误。 异常和错误之间有重要的区别,首先最重要的是错误无法使用try/catch语义捕获。

PHP脚本围绕短执行时间的范例构建,因此默认情况下配置PHP以假定如果脚本运行时间超过30秒,则必须陷入无限循环并因此应该终止。 这是为了防止错误的PHP脚本意外或恶意地造成拒绝服务。

但是,有时候脚本需要比默认分配的更多运行时间。

您可以尝试更改最大执行时间,通过使用set_time_limit() 或者通过更改 php.ini 文件中的max_execution_time 的值来提高限制。 您还可以通过将执行时间设置为0来完全删除限制,但是这不被推荐。

set_time_limit() 可能会被机制禁用,例如disable_functions,因此可能无法使用,同样您可能无法访问 php.ini。 如果两者都是这种情况,则应联系您的主机寻求帮助。

一个例外是从命令行运行的PHP脚本。 在这些运行条件下,PHP脚本可能是交互式的,并且需要花费很长时间处理数据或等待输入。 因此,默认情况下在从命令行运行的脚本上没有max_execution_time限制。


补充说明:PHP 7 对错误处理进行了重大改进。我认为错误和异常现在都是 Throwable 的子类。这可能使上述内容在 PHP7+ 中不再相关,但我需要更仔细地了解错误处理的具体情况才能确定。


9

是的,我测试了TheJanOnline提供的解决方案。sleep()不计入php执行时间,因此这里是一个带有无限循环的可工作版本:

<?php 
function shutdown() 
 { 
     $a=error_get_last(); 
     if($a==null)   
         echo "No errors"; 
     else 
          print_r($a); 

 } 
register_shutdown_function('shutdown'); 
ini_set('max_execution_time',1 ); 
while(1) {/*nothing*/}
// will die after 1 sec and print error
?>

8

你无法改变这一点。但是,你可以使用register_shutdown_function实现优雅的关闭。

<?php 

ini_set('display_errors', '0');         
ini_set("max_execution_time",15 ); //you can use this if you know your script should not take longer than 15 seconds to finish

register_shutdown_function('shutdown');


function shutdown() 
{ 
       $error = error_get_last();

       if ($error['type'] === E_ERROR) {

      //do your shutdown stuff here
      //be care full do not call any other function from within shutdown function
      //as php may not wait until that function finishes
      //its a strange behavior. During testing I realized that if function is called 
      //from here that function may or may not finish and code below that function
      //call may or may not get executed. every time I had a different result. 

      // e.g.

      other_function();

      //code below this function may not get executed

       } 

} 

while(true)
{

}

function other_function()
{
  //code in this function may not get executed if this function
  //called from shutdown function
} 

?>

如果在无限循环之前声明了 other_function(),那么在关闭时会调用它吗? - Douglas.Sesar
@Douglas.Sesar,无论如何都会调用other_function()。但是不能保证它会完成并执行函数调用下面的任何代码。如果other_function()只需要很少的时间来完成很少的事情,那么它可能会完成并执行函数调用下面的任何代码。但是,如果它花费太多时间来完成任务,那么就不能保证它会完全完成! - pinkal vansia
严格来说,这个解决方案是可行的。我想要感谢您的回答,它为我的略微详细的答案提供了一个启示。 - TARKUS

4

基于@pinkal-vansia的答案,我想出了这个方法。因此,我不是提供原创的答案,而是提供一个实用的答案。在发生超时事件时,我需要一种让页面自动刷新的方式。由于我已经观察到足够多次cURL脚本的超时情况,以至于我知道代码是有效的,但有时由于某些原因它无法连接到远程服务器或完全读取服务的html,并且在刷新后问题就消失了,所以我可以接受脚本自动刷新来“治愈”最大执行超时错误。

<?php //script name: scrape_script.php

ini_set('max_execution_time', 300);

register_shutdown_function('shutdown');

function shutdown() 
{ 
    ?><meta http-equiv="refresh" content="0; url=scrape_script.php"><?php
    // just do a meta refresh. Haven't tested with header location, but
    // this works fine.
}

顺便提一下,对于我运行的爬虫脚本来说,300秒并不算太长,它只需要略微少于这个时间从我正在爬取的页面中提取数据。有时候由于连接不稳定,它可能只会超出几秒钟。知道有时候失败是因为连接时间问题而非脚本处理问题,所以最好不要增加超时时间,而是自动刷新页面并重试。


4

在某些情况下,处理“致命错误:超出30秒最大执行时间”的方法有一些小技巧,可以将其作为异常处理:

function time_sublimit($k = 0.8) {
    $limit = ini_get('max_execution_time'); // changes even when you set_time_limit()
    $sub_limit = round($limit * $k);
    if($sub_limit === 0) {
        $sub_limit = INF;
    }
    return $sub_limit;
}

在你的代码中,你必须测量执行时间并在超时致命错误被触发之前抛出异常。$k = 0.8是允许的执行时间的80%,因此您有20%的时间来处理异常。

try{
    $t1 = time(); // start to mesure time.
    while (true) { // put your long-time loop condition here
        time_spent = time() - $t1;
        if(time_spent >= time_sublimit()) {
            throw new Exception('Time sublimit reached');
        }
        // do work here
    }
} catch(Exception $e) {
    // catch exception here
}

3
如果你的工作被很好地分段,以至于可以进行迭代,那么这将起作用。但是如果存在一个单独的大操作或一系列操作在时间限制之前一直挂起,那么你仍然会遇到大问题。 - Josip Rodin

2

我曾经遇到过类似的问题,这是我解决它的方法:

<?php
function shutdown() {
    if (!is_null($error = error_get_last())) {
        if (strpos($error['message'], 'Maximum execution time') === false) {
            echo 'Other error: ' . print_r($error, true);
        } else {
            echo "Timeout!\n";
        }
    }
}

ini_set('display_errors', 0);
register_shutdown_function('shutdown');
set_time_limit(1);

echo "Starting...\n";
$i = 0;
while (++$i < 100000001) {
    if ($i % 100000 == 0) {
        echo ($i / 100000), "\n";
    }
}
echo "done.\n";
?>

这个脚本原样运行会在结尾打印出“Timeout!”。
你可以修改代码中的行$i = 0;$i = 1 / 0;,这时它会打印出:
Other error: Array
(
    [type] => 2
    [message] => Division by zero
    [file] => /home/user/test.php
    [line] => 17
)

参考资料:

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接