【问题标题】:MySQL code causes PHP script to crash at popen/execMySQL 代码导致 PHP 脚本在 popen/exec 处崩溃
【发布时间】:2016-08-28 03:18:45
【问题描述】:

我在Ubuntu 14.04 服务器上有以下PHP 5.6.19 代码。这段代码只是连接到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 代码失败(脚本退出时没有任何错误或输出)。

这里发生了什么?


注意事项

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

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

【问题讨论】:

  • 根本没有错误处理。例如,如果建立 MySQL 连接失败,程序将遇到未捕获的异常并发出呱呱声。
  • 试试exec()而不是popen()
  • /tmp/1.log 是否包含某些内容?
  • @tumber033,使用>> 而不是>。否则,您将覆盖日志。也很有趣,你为什么要这样做,因为内存泄漏?
  • 如果您需要始终如一地执行此操作,为什么不选择通过 CRON 或任务队列运行它?

标签: php mysql linux ubuntu pdo


【解决方案1】:

分叉后父进程确定退出了吗?我原以为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;

希望问题会消失。

【讨论】:

  • 是的——当我与ps aux 核对时,总是最多有一个进程。 shell_execexec 也会发生这种情况。
  • 什么是 FD? lsof 命令有什么作用?我总是为我打印 0。
  • @tumber033 '文件描述符' 对不起,打开文件/套接字/等。您可以通过ulimit -n 检查这些数量的限制。 lsof 列出了当前打开的 FD——我的例子中的 grep 可能比 grep php 或类似的更好。
  • 我想你找到了问题所在。但是如果popen后还需要mysql连接怎么办?
  • 太棒了。据我所知,在 PHP 中没有任何方法可以阻止这种情况。其他解决方案确实超出了问题的范围,但您可以考虑以不同的方式创建子进程以尝试避免这种情况(可能通过“立即”cron),或者序列化从数据库读取的数据并通过标准输入(查看 proc_open) 给子进程进行处理。或者,如果这对容量/性能不是特别敏感,您可以在生成子节点后简单地创建另一个连接!
【解决方案2】:

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

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

  • 如果原因被 PHP 运行时捕获,那么它将尝试记录错误。您是否尝试过故意创建错误场景来验证重新排序/日志记录是否按预期工作?

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

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

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

这不是让您的代码在后台/作为守护进程运行的有效方法。正确的方法取决于您要达到的目标。尝试每 60 秒更新一次进程是否有特定原因?

(您永远不会显式关闭数据库连接 - 这不是问题,因为 PHP 应该在调用 exit 时这样做)。

您可能想阅读thisthis

【讨论】:

  • 简单来说,这个过程的意思是:等待(轮询)db中的数据,然后产生一个新的进程同时处理(或等待)下一批数据,然后处理获取数据(可能很耗时),然后退出。如果一段时间没有数据,它会重新启动以避免潜在的内存泄漏。你推荐什么方法?
  • 首选方法是由在数据库中创建数据的事物同步触发处理。如果做不到这一点,请使用 fork 和 setsid() 正确地守护脚本(如果您有稳定性问题,请考虑使用 DJB 的 daemontools)。作为 cron 作业运行失败(使用并发控制)
【解决方案3】:

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

背景资料

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

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

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

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

sleep() PHP 函数只是sleep() Linux 系统调用的包装器(而usleep() PHP 函数是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 中的一个很大的负面因素。某些 Windows 日期函数也会发生这种情况,即由于夏令时问题。
【解决方案4】:

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2021-09-02
    • 1970-01-01
    • 2012-05-26
    • 1970-01-01
    • 1970-01-01
    • 2011-01-24
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多