MySQL代码导致PHP脚本在popen / exec崩溃

时间:2016-05-03 07:19:12

标签: php mysql linux ubuntu pdo

我在PHP 5.6.19服务器上有以下Ubuntu 14.04代码。此代码只是连接到MySQL 5.6.28数据库,等待一分钟,启动另一个进程,然后退出。

注意:这是完整的脚本,它的目的是证明问题 - 它没有做任何有用的事情。

class DatabaseConnector {
    const DB_HOST = 'localhost';
    const DB_NAME = 'database1';
    const DB_USERNAME = 'root';
    const DB_PASSWORD = 'password';

    public static $db;

    public static function Init() {
        if (DatabaseConnector::$db === null) {
            DatabaseConnector::$db = new PDO('mysql:host=' . DatabaseConnector::DB_HOST . ';dbname=' . DatabaseConnector::DB_NAME . ';charset=utf8', DatabaseConnector::DB_USERNAME, DatabaseConnector::DB_PASSWORD);
        }
    }
}

$startTime = time();

// ***** Script works fine if this line is removed.
DatabaseConnector::Init();

while (true) {
    // Sleep for 100 ms.
    usleep(100000);

    if (time() - $startTime > 60) {
        $filePath = __FILE__;
        $cmd = "nohup php $filePath > /tmp/1.log 2>&1 &";

        // ***** Script sometimes exits here without opening the process and without errors.
        $p = popen($cmd, 'r');

        pclose($p);

        exit;
    }
}

我使用nohup php myscript.php > /tmp/1.log 2>&1 &启动脚本的第一个过程。

这个过程循环应该永远持续但是...基于多个测试,在一天之内(但不是立即),服务器上的过程“无理由地消失”。我发现MySQL代码导致popen代码失败(脚本退出时没有任何错误或输出)。

这里发生了什么?

注释

  • 服务器全天候运行。
  • 记忆不是问题。
  • 数据库连接正确。
  • 文件路径不包含空格。
  • 使用shell_execexec代替popen(以及pclose)时存在同样的问题。

我也知道popen是失败的行,因为我通过在脚本中的某些点登录文件来进行进一步的调试(上面没有显示)。

4 个答案:

答案 0 :(得分:3)

我使用pcntl_fork()和MySQL连接有类似的情况。这里的原因可能是一样的。

背景信息

popen()创建了一个子进程。对pclose()的调用将关闭通信通道,子进程将继续运行直至退出。这是事情开始失控的时候。

子进程完成后,父进程会收到SIGCHLD signal。这里的父进程是运行您发布的代码的PHP解释器。子进程是使用popen()启动的进程(它运行的命令并不重要。)

这里有一件小事你可能不知道或者你在文档中找到并忽略它,因为它在PHP中的一个程序时没有多大意义。在sleep()

的文档中提到了它
  

如果呼叫被信号中断,sleep()将返回非零值。

sleep() PHP函数只是sleep() Linux系统调用的包装(而usleep() PHP函数是usleep() Linux系统调用的包装。)< / p>

系统调用的文档中明确说明了PHP文档中没有说明的内容:

  

sleep()使调用线程处于休眠状态,直到秒数已经过去或信号到达且未被忽略。

返回您的代码。

您的代码中有两个地方,PHP解释器调用usleep() Linux系统函数。其中一个清晰可见:您的PHP代码调用它。另一个是隐藏的(见下文)。

会发生什么(可见部分)

从第二次迭代开始,如果子进程(在前一次迭代时使用popen()创建)恰好在父程序在usleep(100000)调用内时退出,则PHP解释器进程将接收{ {1}}信号及其执行在暂停之前恢复。 SIGCHLD早于预期返回。由于超时很短,肉眼无法观察到这种效应。放10秒而不是0.1秒,你会注意到它。

但是,除了超时中断之外,这不会以致命的方式影响代码的执行。

为什么它会崩溃(看不见的部分)

传入信号损害程序执行的第二个位置隐藏在PHP解释器代码的深处。出于某些协议原因,MySQL客户端库在几个地方使用usleep()和/或sleep()。如果usleep()到达时解释器恰好位于其中一个调用内,则MySQL客户端库代码会意外恢复,并且很多时候,它会以错误的状态结束&#34; MySQL服务器已经消失(错误) 2006)&#34;

您的代码可能会忽略(或吞噬)MySQL错误状态(因为它不会指望它在那个地方发生)。我没有,我花了几天的时间调查,找出上面总结的事实。

解决方案

问题的解决方案很简单(在您知道上面公开的所有内部细节之后)。它在上面的文档引言中暗示:&#34;信号到达时不会被忽略&#34;

当不希望信号到达时,可以屏蔽(忽略)信号。 PHP PCNTL扩展名提供了函数pcntl_sigprocmask()。它封装了sigprocmask() Linux系统调用,用于设置程序从现在开始可以接收哪些信号(事实上,要阻止的信号)。

根据您的需要,您可以实施两种策略。

如果您的程序需要与数据库进行通信在子进程处理完成后得到通知,那么您必须在对SIGCHLD的一对调用中包装所有数据库调用以阻止然后取消阻止pcntl_sigprocmask()信号。

如果您在孩子处理完成后并不在意,那么您只需致电:

SIGCHLD

开始创建任何子进程之前(pcntl_sigprocmask(SIG_BLOCK, array(SIGCHLD)); 之前)。 它使您的进程忽略子进程的终止,并让它运行其数据库查询而不会发生意外中断。

警告

while()信号的默认处理是调用wait(),以便在完成的子进程后清除系统。在wait()的文档中解释了如果未处理信号(因为其传递被阻止)会发生什么:

  

终止但未等待的孩子会成为&#34;僵尸&#34;。内核维护有关僵尸进程(PID,终止状态,资源使用信息)的最小信息集,以便允许父进程稍后执行等待以获得关于子进程的信息。只要没有通过等待从系统中删除僵尸,它就会占用内核进程表中的一个插槽,如果该表填充,则无法创建进一步的进程。如果父进程终止,那么它的&#34; zombie&#34; SIGCHLD采用了儿童(如果有的话),自动执行等待以移除僵尸。

用简单的英语,如果你阻止接收init(1)信号,那么你必须拨打pcntl_wait()以清理僵尸子进程。

您可以添加:

SIGCHLD

pcntl_wait($status, WNOHANG); 循环内的某处(例如,在它结束之前)。

答案 1 :(得分:2)

分叉后父进程肯定会退出吗?我以为pclose会等到孩子退出才会退场。

如果它没有退出,我推测因为mySQL连接永远不会关闭,所以当你生成孩子的树时,你最终会达到其连接限制(或其他一些限制)过程

修改1

我只是试图复制这个。我将你的脚本改为每半秒而不是每一分钟,并且能够在大约10分钟内将其删除。

看起来子进程的重复创建正在产生更多的FD,直到它最终还没有:

$ lsof | grep type=STREAM | wc -l
240
$ lsof | grep type=STREAM | wc -l
242
...
$ lsof | grep type=STREAM | wc -l
425
$ lsof | grep type=STREAM | wc -l
428
...

这是因为孩子在分叉时继承了父母的FD(在这种情况下是mySQL连接)。

如果您在popen之前关闭mySQL连接(在您的情况下):

DatabaseConnector::$db = null;

这个问题有望消失。

答案 2 :(得分:1)

  

脚本退出时没有任何错误或输出

如果代码中没有错误检查,那就不足为奇了。但是,如果真的是&#34;崩溃&#34;,那么:

  • 如果原因被PHP运行时捕获,那么它将尝试记录错误。您是否尝试过故意创建错误方案来修改报告/日志记录是否按预期工作?

  • 如果错误没有被PHP运行时捕获,操作系统应该转储核心文件 - 你有checked the OS config吗?寻找核心文件? Analyzed it

  

$ cmd =&#34; nohup php $ filePath&gt; /tmp/1.log 2&gt;&amp; 1&amp;&#34;;

这可能不会做你认为它做的事情。当您使用大多数版本的nohup在后台运行进程时,它仍然保留与父进程的关系;在子进程退出之前,父进程不能被收集 - 并且一个孩子总是会产生另一个孩子。

这不是保持代码在后台运行/作为守护程序的有效方法。正确的方法取决于您想要实现的目标。是否有特定原因试图每60秒更新一次该过程?

(您从未明确关闭数据库连接 - 这不是问题,因为在调用exit时, 应该执行此操作。)

您可能需要阅读thisthis

答案 3 :(得分:0)

我建议在pclose之后进程不会退出。在这种情况下,每个进程都拥有自己与db的连接。经过一段时间后,达到了限制MySQL的连接并且新连接失败。 要了解发生了什么 - 在字符串DatabaseConnector::Init();pclose($p);之前和之后添加一些日志