自动重启PHP脚本以应对退出问题

29

无论 PHP 脚本是正常退出、因错误退出或达到最大内存使用量而终止,是否有一种自动重新启动 PHP 脚本的方法?


1
在shell脚本中放置一个无限do while循环? - Jesus Ramos
可能会起作用,但我必须同时运行8个脚本线程,并且如果它们退出,所有线程都要重新启动。那么我需要运行8个shell脚本,对吗?这似乎不是很高效。 - Alasdair
1
我发现supervisord在这方面非常有用。你可以使用纯PHP实现此类进程监视/控制/重启/守护程序行为,但为什么不使用已经存在的东西呢? - deceze
PHP守护进程从来都不是非常可靠的。但我用Cron作业每5分钟检查并重启,如果必要的话。如果你能接受1分钟的停机时间,可以尝试使用Cron。 - user557846
我不理解supervisord的文档。你能否请解释一下如何运行同一个PHP脚本的多个实例,并在任何一个退出时自动重启它们?另外,我需要让每个8个实例之间的初始启动时间间隔为1分钟。 - Alasdair
显示剩余4条评论
8个回答

65

使用PCNTL,PHP脚本也可以重新启动自己。

免责声明:此次演示的目的只是为了证明PHP完全有能力重新启动自身,并回答以下问题:

是否有一种方式可以在PHP脚本退出时自动重新启动它,无论其是否已正确退出或因错误而终止?

因此,我们的范围不会涉及Unix进程的任何细节,建议您从PCNTL书籍开始学习,或者参考获取更多详细信息。需要开发人员谨慎决定,仅仅因为我们可以这样做,并不代表这是明智之举。除去所有认真的事情,让我们来玩一些乐趣。

在示例中,我们将假设它是一个PHP CLI脚本,在一个半好的shell中从终端启动,使用以下命令:

$ php i-can-restart-myself.php 0

我们将一个重启计数属性作为指示器,表明进程已经重新启动。

我能自动重新启动PHP脚本吗?

是的,我可以!

<?php
    echo ++$argv[1];     // count every restart
    $_ = $_SERVER['_'];  // or full path to php binary

    echo "\n======== start =========\n";
    // do a lot of stuff
    $cnt = 0;
    while( $cnt++ < 10000000 ){}
    echo "\n========== end =========\n";
    
    // restart myself
    pcntl_exec($_, $argv);

无论是否已正确终止?

是的,如果被终止,我可以重新启动!

<?php
    echo ++$argv[1];    
    $_ = $_SERVER['_']; 
    
    register_shutdown_function(function () {
        global $_, $argv; // note we need to reference globals inside a function
        // restart myself
        pcntl_exec($_, $argv);
    });

    echo "\n======== start =========\n";
    // do a lot of stuff
    $cnt = 0;
    while( $cnt++ < 10000000 ){}
    echo "\n========== end =========\n";
    
    die; // exited properly
    // we can't reach here 
    pcntl_exec($_, $argv);

或因错误而终止?

同上!

<?php
    echo ++$argv[1];    
    $_ = $_SERVER['_']; 
    
    register_shutdown_function(function () {
        global $_, $argv; 
        // restart myself
        pcntl_exec($_, $argv);
    });

    echo "\n======== start =========\n";
    // do a lot of stuff
    $cnt = 0;
    while( $cnt++ < 10000000 ){}
    echo "\n===== what if? =========\n";
    require 'OOPS! I dont exist.'; // FATAL Error:

    // we can't reach here
    echo "\n========== end =========\n";        
    die; // exited properly
    // we can't reach here 
    pcntl_exec($_, $argv);

但是你知道我会想要更多的,对吗?

当然!我可以在信号kill、hub甚至Ctrl-C上重新启动!

<?php
    echo ++$argv[1];     
    $_ = $_SERVER['_'];  
    $restartMyself = function () {
        global $_, $argv; 
        pcntl_exec($_, $argv);
    };
    register_shutdown_function($restartMyself);
    pcntl_signal(SIGTERM, $restartMyself); // kill
    pcntl_signal(SIGHUP,  $restartMyself); // kill -s HUP or kill -1
    pcntl_signal(SIGINT,  $restartMyself); // Ctrl-C

    echo "\n======== start =========\n";
    // do a lot of stuff
    $cnt = 0;
    while( $cnt++ < 10000000 ){}
    echo "\n===== what if? =========\n";
    require 'OOPS! I dont exist.'; // FATAL Error:

    // we can't reach here
    echo "\n========== end =========\n";        
    die; // exited properly
    // we can't reach here 
    pcntl_exec($_, $argv);

现在我该如何终止它?

如果你通过按住Ctrl-C来淹没进程,你可能会在关闭过程中抓到它。

我不想看到所有这些错误,我也可以在这方面重新启动吗?

没问题,我也可以处理错误!

<?php
    echo ++$argv[1];     
    $_ = $_SERVER['_'];  
    $restartMyself = function () {
        global $_, $argv; 
        pcntl_exec($_, $argv);
        // NOTE: consider fork and exiting here as error handler
    };
    register_shutdown_function($restartMyself);
    pcntl_signal(SIGTERM, $restartMyself);   
    pcntl_signal(SIGHUP,  $restartMyself);   
    pcntl_signal(SIGINT,  $restartMyself);   
    set_error_handler($restartMyself , E_ALL); // Catch all errors

    echo "\n======== start =========\n";
    // do a lot of stuff
    $cnt = 0;
    while( $cnt++ < 10000000 ){}

    echo $CAREFUL_NOW; // NOTICE: will also be caught

    // we would normally still go here
    echo "\n===== what if? =========\n";
    require 'OOPS! I dont exist.'; // FATAL Error:

    // we can't reach here
    echo "\n========== end =========\n";        
    die; // exited properly
    // we can't reach here 
    pcntl_exec($_, $argv);

尽管乍一看这似乎工作得很好,因为pcntl_exec在同一进程中运行,我们并没有注意到事情出了大问题。如果你想要生成一个新的进程,并让旧进程死亡,这是一个完全可行的替代方案参见下一篇文章,你会发现每次迭代都会启动2个进程,因为我们触发了PHP NOTICE和ERROR,这是由于常见的疏忽而引起的。这可以通过确保在调用pcntl_exec后使用die()exit()来纠正,因为实施错误处理程序后PHP假定容忍度已被接受并继续运行。缩小错误报告级别而不是捕获E_ALL也将更加负责。
我想说的是,即使你能重新启动一个失败的脚本,这也不是允许有错误代码的借口。虽然可能存在可行的用例,但我强烈反对将重启作为脚本因错误而失败的解决方案!正如我们从这些示例中看到的,无法确切知道它在哪里失败,因此我们不能确定它会从哪里重新开始。选择“快速修复”解决方案可能看起来运行良好,但现在可能比以前更有问题。
相反,我更希望通过一些适当的单元测试来解决缺陷,这将有助于找出罪魁祸首,以便我们可以纠正问题。如果PHP内存不足,可以通过谨慎使用资源来避免。 (顺便说一句,您会发现将null分配给变量比使用unset得到相同结果更快)。但是,如果不解决,重启肯定会成为已经存在的问题的合适开端。

1
这对我起作用,但是我不得不直接在第二个参数中传递文件路径,否则进程就会一直处于无动作状态(就像没有调用带有文件的php一样)。我继续为了保险起见直接传递了PHP路径:pcntl_exec('/usr/local/bin/php', [ FILE ]); - Jesse
$_SERVER['_'] 是做什么用的? - Marco Marsala
1
@MarcoMarsala 这里引用了一个shell变量$_,它指向最后一个可执行文件,在我们的情况下,它是PHP解释器二进制文件的完整路径,例如/usr/bin/php,而$argv包含传递给$_的所有参数,因此pcntl_exec($_, $argv)实际上等同于在命令行上键入php i-can-restart-myself.php 1 - nickl-

10

这里有龙。

高级用法,不适合新手。

要启动一个单独的进程,请将restartMyself函数更改为包括pcntl_fork()调用,根据手册,它会生成具有唯一PID的新进程,我们仅在子进程中重新启动脚本。然后,在处理第一个错误(PHP警告)时,我们清除当前进程,释放资源并防止执行继续,同时触发ERROR并启动另一个进程。

一个基本的多进程restartMyself函数:

<?php
    $restartMyself = function () {
        global $_, $argv; 
        if(!pcntl_fork()) // We only care about the child fork
            pcntl_exec($_, $argv);
        die; // insist that we don't tolerate errors  
    };

很遗憾,这仍然会导致多个进程并行运行,并具有相同的重启ID,因为如果我们尝试在错误或信号处理程序中退出脚本,它将触发注册的关闭函数并分叉另一个进程。只有从关闭函数中调用的退出调用最终停止进程。这肯定是一场冒险,追逐重启自身、对您的杀死尝试免疫的进程,如果您确实设法在进程中抓住它们,那就好玩了=) 确保脚本有足够的工作,以便您有足够的时间将其杀死。为确保不死者保持死亡,请尝试:
$ kill -9 [PID]

或者:

$ kill -s KILL [PID]

不会对亡灵进行空话,因为您明智地考虑了僵尸和孤儿,但最终即使是最艰难的过程通常仍然需要一个父级,因此杀死shell会话应该让它们休息。
开玩笑的,这是一个有效的用例,通过销毁旧进程并重新启动来优化清除资源。当然,您会合理地留下结束进程的选项,因为那些信号(如SIGTERM,SIGHUP)一般都是按需求终止的,不期望重生,甚至可以认为忽略它们是恶意的。
所有标准免责声明适用...啦啦啦...
为了确保我们只重启脚本一次,并且这样做只有在给定脚本执行时才会有单个进程运行,我们将需要全局跟踪进程ID(PID)。
完整的单进程分叉自重启脚本:
<?php
    echo ++$argv[1];     // count every restart with argument 1
    $_ = $_SERVER['_'];  // full path to PHP interpreter binary
    $restartMyself = function () {
        global $_, $argv, $pid;    // track PID in global scope
        if(!$pid ??= pcntl_fork()) // only fork if $pid is null
            pcntl_exec($_, $argv); // only restart as the child fork
        die; // exit parent process when called in shutdown function  
    };
    register_shutdown_function($restartMyself);// catch on exit
    pcntl_signal(SIGTERM, $restartMyself);     // catch kill
    pcntl_signal(SIGHUP,  $restartMyself);     // catch kill -s HUP or kill -1
    pcntl_signal(SIGINT,  $restartMyself);     // catch Ctrl-C
    set_error_handler($restartMyself , E_ALL); // catch all errors

    // do a lot of stuff
    for ($cnt = 0; $cnt++ < 999999999;){}

    echo $UNDEFINED_VAR; // WARNING: will be caught, 
                         // script restarted as new process,
                         // and current process terminated

    // we will not reach here anymore

在任何*nix兼容的环境中运行脚本,使用以下命令:
$ php i-can-respawn-myself.php 0

nJoy!


6

假设:

  • 调用相同的脚本文件x次
  • 如果它被中断/结束/停止,则重新调用
  • 无限循环的自动化过程

有一些选项可以考虑来实现你的目标[基于Unix/Linux的解决方案]:

  1. Use a BASH script, so that the PHP script can be resumed after it stopped/ended/interrupted:

    #!/bin/bash
    clear
    date
    
    php -f my_php_file.php
    sleep 100
    # rerun myself
    exec $0
    
  2. The Fat Controller execution handler, these are the key features you're looking for:

  • 用于重复运行其他程序
  • 可以同时运行许多脚本/程序的实例
  • 任何脚本执行完毕后都可以重新运行

与CRON非常相似,但其关键特性正是您所需的

  1. Using CRON, you can have it call your PHP script at intervals of e.g. every minute, then with this code at the very beginning of you PHP script, you will be able to control the number of processes running, exiting if there are already 8:

    exec('ps -A | grep ' . escapeshellarg(basename(__FILE__)) , $results);
    if (count($results) > 7) {
      echo "8 Already Running\n"
      die(0);
    }
    
搜索当前文件路径下正在运行的进程,并返回找到的进程数,如果大于7,则退出。

更新了回答,提供了针对您的目标(Unix/Linux)的经过实际测试的解决方案。 - Zuul

2
为了补充 ow3n 的回答,PHP CLI 脚本就像任何其他命令一样,并且可以在无限的 while 循环中轻松重复。它将与单独运行该命令完全相同,但在结束时会再次运行。只需在想要终止循环时按下 Ctrl+C

编辑:如果您的进程捕获信号(如果它是12因素应用程序,则应该),则需要按住Ctrl+C而不仅仅是按下它。按键会被进程捕获(进程应该自行终止),但while循环会重新启动进程(如果您只想重新启动进程而不是终止它,则这是有用的)。如果您的进程没有捕获信号,那么Ctrl+C将同时杀死进程和while循环。

以下是最简单的方法,没有额外的输出:

while :; do [command]; done

while :; do echo "test" && sleep 1; done

while :; do php myscript.php; done

话虽如此,我有点担心您在评论中的含义:

关于线程:

  1. 我认为在PHP中使用线程需要三思。使用该模型的语言相比PHP拥有更好/更稳定/更成熟的环境(例如Java)。
  2. 使用多个进程而不是多个线程可能看起来效率低下,但请相信我,它在简单性、可维护性、易于调试等方面远远超过了多线程。多线程很快就会让你陷入“维护地狱”,这就解释了“Actor模型”的出现。
  3. 效率是否是业务需求?通常是由于资源有限(例如嵌入式设备),但我怀疑这不是你的情况,否则你不会使用PHP。考虑到如今,“人比计算机昂贵得多”。因此,为了最小化成本,最大化你的人的效率而不是你的计算机效率更为有益(参见Unix哲学中的经济原则)。如果多线程做得好,可以通过优化计算机的使用来节省一些钱,但是将给你的人带来更多的头痛。
  4. 如果你确实必须使用线程,我强烈建议你考虑Actor模型。Akka是一个很好的实现。

关于进程:

  1. 如果您在开发中运行脚本,则每种类型的一个进程就足够了。
  2. 如果您在生产中运行脚本,则应依靠进程管理器来管理进程,例如Upstart。
  3. 避免将进程变成守护进程。
  4. 学习12因素应用程序,特别是进程、并发和处理。

更新:容器使运行和管理一组进程变得更加容易。


2
使用 CRON
#!/bin/sh
process = 'script.php'

if ps ax | grep -v grep | grep $process
then
    echo "$process is running..."
else
    echo "$process not running, relaunching..."
    php /your/php/script.php
fi

1

我个人的意见是:使用PCNTL重启程序,并实现自定义错误处理。

    register_shutdown_function("shutdownHandler");

    function shutdownHandler() { // will be called when php script ends.
        $lasterror = error_get_last();
        switch ($lasterror['type']) {
            case E_ERROR:
            case E_CORE_ERROR:
            case E_COMPILE_ERROR:
            case E_USER_ERROR:
            case E_RECOVERABLE_ERROR:
            case E_CORE_WARNING:
            case E_COMPILE_WARNING:
            case E_PARSE:
                $error = "[SHUTDOWN] lvl:" . $lasterror['type'] .
                         " | msg:" . $lasterror['message'] .
                         " | file:" . $lasterror['file'] .
                         " | ln:" . $lasterror['line'];
                myCustomLogHandler("FATAL EXIT $error\n");
        }
        $_ = $_SERVER['_'];
        global $_; 
        pcntl_exec($_);
    }

1
在几次重启后,我收到了“注意:未定义索引:_”的提示。 - Bluscream
并非所有这些错误常量都是致命的,这意味着它们不会触发关闭操作,请考虑使用 set_error_handler 注册错误处理程序部分。 - nickl-

0

如果想要在脚本停止运行后每次都重新启动它,可以将user1179181的脚本放入一个循环中。

#!/bin/bash

while true
do
    if ps ax | grep -v grep | grep "script.php"
    then
        echo "process is running..."
    else
        clear
        date
        echo "process not running, relaunching..."
        php script.php
    fi
    sleep 10
done

-1

就这样做:

sleep(1);
system('php '.__FILE__);

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