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

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

注意:这是完整的脚本,目的是为了certificate这个问题 – 它没有做任何有用的事情。

 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 &开始脚本的第一个进程。

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

这里发生了什么?


笔记

  • 服务器24/7运行。
  • 内存不是问题。
  • 数据库连接正确。
  • 文件path不包含空格。
  • 使用shell_execexec而不是popen (和pclose )时存在同样的问题。

我也知道popen是因为我通过在脚本中的某些点login文件进行进一步debugging(上面没有显示)而失败的线。

分岔后父流程是否完全退出? 我曾经以为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)。

如果你关闭之前的SQL连接(在你的情况):

 DatabaseConnector::$db = null; 

问题将有望消失。

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

背景信息

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

当一个子进程完成时,父进程收到一个SIGCHLD信号 。 这里的父进程是运行你发布的代码的PHP解释器。 子进程是使用popen()启动的进程(不管它运行的是什么命令)。

在这里有一件你可能不知道的东西,或者你在文档中找到的东西,并忽略它,因为在PHP中编程时没有什么意义。 在sleep()的文档中提到:

如果调用被信号中断, sleep()返回一个非零值。

sleep() PHP函数只是sleep() Linux系统调用的一个包装(而usleep()函数是usleep() Linux系统调用的一个包装。

系统调用的文档中明确指出了PHP文档中没有讲到的内容:

sleep()使调用线程睡眠,直到秒秒已过或信号到达不被忽略。

回到你的代码。

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

发生了什么(可见部分)

从第二次迭代开始,如果一个子进程(在前一次迭代中使用popen()创建)发生在父进程在usleep(100000)调用中时退出,PHP解释器进程接收到SIGCHLD信号,暂时没有了 usleep()返回比预期的更早。 由于超时时间短,肉眼无法观察到这种效果。 把10秒,而不是0.1秒,你会注意到它。

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

为什么它崩溃(隐形部分)

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

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

一个办法

问题的解决方案很简单(在知道上面提到的所有内部细节之后)。 在上面的文档引用中暗示: “一个不被忽视的信号到达”

信号可以被掩盖(忽略),当他们的到来是不需要的。 PHP PCNTL扩展提供了函数pcntl_sigprocmask() 。 它封装了sigprocmask() Linux系统调用,用于设置程序从现在开始接收哪些信号(实际上是什么信号被阻塞)。

有两种策略可以实现,具体取决于你所需要的。

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

如果你不在乎孩子什么时候完成,那么你只需打电话:

 pcntl_sigprocmask(SIG_BLOCK, array(SIGCHLD)); 

在开始创建任何子进程之前(在while()之前)。 它使你的进程忽略子进程的终止,并让它运行它的数据库查询,而不会造成不必要的中断。

警告

SIGCHLD信号的缺省处理是调用wait()以便在完成子进程之后清理系统。 如果不处理信号(因为传送被阻止)会发生什么情况在wait()的文档中有解释:

终止但未被等待的孩子成为“僵尸”。 内核维护关于僵尸进程(PID,终止状态,资源使用信息)的最小信息集,以便父母稍后执行等待以获取关于该孩子的信息。 只要僵尸没有通过等待从系统中移除,它将占用内核进程表中的一个插槽,并且如果这个表填充,将不可能创建进一步的进程。 如果一个父进程终止,那么它的“僵尸”子(如果有的话)被init(1) ,它会自动执行一个等待来移除僵尸。

用简单的英文,如果你阻止了SIGCHLD信号的接收,那么你必须调用pcntl_wait()来清理僵尸子进程。

你可以加:

 pcntl_wait($status, WNOHANG); 

while循环内的某个地方(例如在它结束之前)。

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

在代码中没有错误检查时不奇怪。 但是,如果真的是“崩溃”,那么:

  • 如果原因被PHP运行时所困,那么它将会尝试记录一个错误。 你有没有尝试故意创建一个错误的场景来改变reorting / logging正如你所期望的那样工作?

  • 如果错误没有被PHP运行时所捕获,则操作系统应该转储一个核心文件 – 你检查了操作系统的配置吗? 找了核心文件? 分析一下 ?

$ cmd =“nohup php $ filePath> /tmp/1.log 2>&1&”;

这可能不符合你的想法。 当你在大多数版本的nohup的后台运行一个进程时,它仍然保留与父进程的关系; 在孩子进程退出之前,父母是无法获得的 – 而且在孩子出生之前,孩子总是会产生另一个孩子。

这不是保持代码在后台/作为守护进程运行的有效方法。 正确的做法取决于你想要达到的目标。 每60秒就有一次尝试更新流程的具体原因?

(你从来没有明确地关闭数据库连接 – 这是一个问题,因为PHP 应该这样做,当exit被调用)。

你可能想阅读这个和这个

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