ChatGPT解决这个技术问题 Extra ChatGPT

异步运行 PHP 任务

我在一个有点大的 Web 应用程序上工作,后端主要是 PHP。代码中有几个地方我需要完成一些任务,但我不想让用户等待结果。例如,在创建新帐户时,我需要向他们发送一封欢迎电子邮件。但是当他们点击“完成注册”按钮时,我不想让他们等到电子邮件实际发送,我只想开始这个过程,并立即向用户返回一条消息。

到目前为止,在某些地方,我一直在使用 exec() 的感觉。基本上做这样的事情:

exec("doTask.php $arg1 $arg2 $arg3 >/dev/null 2>&1 &");

这似乎有效,但我想知道是否有更好的方法。我正在考虑编写一个在 MySQL 表中对任务进行排队的系统,以及一个单独的长时间运行的 PHP 脚本,该脚本每秒查询一次该表,并执行它找到的任何新任务。如果我需要的话,这还有一个好处,就是让我将来可以在几台工作机器之间分配任务。

我在重新发明轮子吗?有比 exec() hack 或 MySQL 队列更好的解决方案吗?


P
Paul Dixon

我使用了排队方法,它工作得很好,因为您可以推迟处理,直到您的服务器负载空闲,如果您可以轻松地划分“不紧急的任务”,那么您可以非常有效地管理您的负载。

自己动手并不太难,这里有一些其他的选择:

GearMan - 这个答案写于 2009 年,从那时起 GearMan 看起来很受欢迎,请参阅下面的评论。

如果您想要一个完整的开源消息队列,请使用 ActiveMQ。

ZeroMQ - 这是一个非常酷的套接字库,它可以轻松编写分布式代码,而不必过多担心套接字编程本身。您可以将它用于单个主机上的消息队列——您只需让您的 web 应用程序将某些内容推送到一个队列中,持续运行的控制台应用程序将在下一个合适的机会使用该队列

beanstalkd - 在写这个答案时才发现这个,但看起来很有趣

dropr 是一个基于 PHP 的消息队列项目,但自 2010 年 9 月以来一直没有得到积极维护

php-enqueue 是最近(2017 年)维护的围绕各种队列系统的包装器

最后,一篇关于使用 memcached 进行消息队列的博文

另一种可能更简单的方法是使用 ignore_user_abort - 一旦您将页面发送给用户,您就可以进行最终处理而不必担心提前终止,尽管这确实具有延长页面加载时间的效果用户视角。


感谢所有的提示。关于 ignore_user_abort 的具体内容对我来说并没有真正的帮助,我的整个目标是避免给用户带来不必要的延迟。
如果您在“感谢您注册”响应中设置 Content-Length HTTP 标头,则浏览器应在收到指定字节数后关闭连接。这会使服务器端进程运行(假设设置了 ignore_user_abort),而不会让最终用户等待。当然,在呈现标题之前,您需要计算响应内容的大小,但这对于简短的响应来说非常容易。
Gearman (gearman.org) 是一个出色的跨平台开源消息队列。您可以使用 C、PHP、Perl 或几乎任何其他语言编写 worker。有适用于 MySQL 的 Gearman UDF 插件,您还可以使用 PHP 中的 Net_Gearman 或 gearman pear 客户端。
Gearman 将是我今天(2015 年)推荐的任何自定义工作排队系统。
另一种选择是设置一个节点 js 服务器来处理请求并返回一个快速响应,中间有一个任务。节点 js 脚本中的许多内容都是异步执行的,例如 http 请求。
T
Timm

当您只想执行一个或多个 HTTP 请求而无需等待响应时,也有一个简单的 PHP 解决方案。

在调用脚本中:

$socketcon = fsockopen($host, 80, $errno, $errstr, 10);
if($socketcon) {   
   $socketdata = "GET $remote_house/script.php?parameters=... HTTP 1.1\r\nHost: $host\r\nConnection: Close\r\n\r\n";      
   fwrite($socketcon, $socketdata); 
   fclose($socketcon);
}
// repeat this with different parameters as often as you like

在被调用的 script.php 中,您可以在第一行调用这些 PHP 函数:

ignore_user_abort(true);
set_time_limit(0);

这会导致脚本在 HTTP 连接关闭时继续运行而没有时间限制。


如果 php 在安全模式下运行,set_time_limit 无效
r
rojoca

派生进程的另一种方法是通过 curl。您可以将内部任务设置为 Web 服务。例如:

http://domain/tasks/t1

http://domain/tasks/t2

然后在您的用户访问的脚本中调用该服务:

$service->addTask('t1', $data); // post data to URL via curl

您的服务可以使用 mysql 或任何您喜欢的方式跟踪任务队列:它全部包含在服务中,您的脚本只是使用 URL。如果需要(即易于扩展),这可以让您腾出时间将服务移动到另一台机器/服务器。

添加 http 授权或自定义授权方案(如 Amazon 的 Web 服务)可以让您打开任务以供其他人/服务使用(如果您愿意),您可以更进一步并在顶部添加监控服务以跟踪队列和任务状态。

http://domain/queue?task=t1

http://domain/queue?task=t2

http://domain/queue/t1/100931

它确实需要一些设置工作,但有很多好处。


我不喜欢这种方法,因为它会使 Web 服务器超载
如果您使用一台服务器,我看不出您如何解决这个问题。如果你有不止一个,你将如何解决这个问题?所以真的,这个答案是不在网络服务器上加载这项工作的唯一方法。
N
Nisse Engström

如果只是提供昂贵任务的问题,在支持php-fpm的情况下,为什么不使用fastcgi_finish_request()功能呢?

此函数将所有响应数据刷新到客户端并完成请求。这允许在不打开与客户端的连接的情况下执行耗时的任务。

您实际上并没有以这种方式使用异步性:

首先制作所有主要代码。执行 fastcgi_finish_request()。做所有重的东西。

再次需要 php-fpm。


A
Alister Bulman

我已将 Beanstalkd 用于一个项目,并计划再次使用。我发现它是运行异步进程的绝佳方式。

我用它做的几件事是:

图像调整大小 - 并且将轻负载队列传递给基于 CLI 的 PHP 脚本,调整大 (2mb+) 图像的大小工作得很好,但尝试在 mod_php 实例中调整相同图像的大小经常遇到内存空间问题(我将 PHP 进程限制为 32MB,而调整大小的时间不止这些)

近期检查 - beanstalkd 有可用的延迟(使此作业只能在 X 秒后运行) - 所以我可以在稍后的时间对事件进行 5 或 10 次检查

我编写了一个基于 Zend-Framework 的系统来解码一个“不错”的 url,例如,调整它会调用 QueueTask('/image/resize/filename/example.jpg') 的图像大小。 URL 首先被解码为一个数组(模块、控制器、动作、参数),然后转换为 JSON 以注入队列本身。

然后,一个长时间运行的 cli 脚本从队列中提取作业,运行它(通过 Zend_Router_Simple),如果需要,将信息放入 memcached 中,以便网站 PHP 在完成后根据需要获取。

我还提出的一个问题是 cli 脚本在重新启动之前只运行了 50 个循环,但如果它确实想按计划重新启动,它会立即这样做(通过 bash 脚本运行)。如果出现问题并且我执行了 exit(0)exit;die(); 的默认值),它将首先暂停几秒钟。


我喜欢 beanstalkd 的外观,一旦他们添加持久性,我认为它会是完美的。
那已经在代码库中并且正在稳定中。我也很期待“命名工作”,所以我可以在那里扔东西,但知道如果那里已经有一个,它就不会被添加。适合定期活动。
@AlisterBulman 您能否提供有关“长时间运行的 cli 脚本然后从队列中获取作业”的更多信息或示例。我正在尝试为我的应用程序构建这样的 cli 脚本。
A
Andrew Moore

这是我为我的 Web 应用程序编写的一个简单类。它允许分叉 PHP 脚本和其他脚本。适用于 UNIX 和 Windows。

class BackgroundProcess {
    static function open($exec, $cwd = null) {
        if (!is_string($cwd)) {
            $cwd = @getcwd();
        }

        @chdir($cwd);

        if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN') {
            $WshShell = new COM("WScript.Shell");
            $WshShell->CurrentDirectory = str_replace('/', '\\', $cwd);
            $WshShell->Run($exec, 0, false);
        } else {
            exec($exec . " > /dev/null 2>&1 &");
        }
    }

    static function fork($phpScript, $phpExec = null) {
        $cwd = dirname($phpScript);

        @putenv("PHP_FORCECLI=true");

        if (!is_string($phpExec) || !file_exists($phpExec)) {
            if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN') {
                $phpExec = str_replace('/', '\\', dirname(ini_get('extension_dir'))) . '\php.exe';

                if (@file_exists($phpExec)) {
                    BackgroundProcess::open(escapeshellarg($phpExec) . " " . escapeshellarg($phpScript), $cwd);
                }
            } else {
                $phpExec = exec("which php-cli");

                if ($phpExec[0] != '/') {
                    $phpExec = exec("which php");
                }

                if ($phpExec[0] == '/') {
                    BackgroundProcess::open(escapeshellarg($phpExec) . " " . escapeshellarg($phpScript), $cwd);
                }
            }
        } else {
            if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN') {
                $phpExec = str_replace('/', '\\', $phpExec);
            }

            BackgroundProcess::open(escapeshellarg($phpExec) . " " . escapeshellarg($phpScript), $cwd);
        }
    }
}

O
Omar Aziz

PHP 有多线程,只是默认情况下没有启用,有一个名为 pthreads 的扩展可以做到这一点。不过,您需要使用 ZTS 编译的 php。 (线程安全)链接:

Examples

Another tutorial

pthreads PECL Extension

更新:因为 PHP 7.2 并行扩展开始发挥作用

教程/示例

参考手册


现在已经过时了,取而代之的是并行。
@T.Todua,谢谢。更新了答案以保持相关性!
D
Darryl Hein

这是我几年来一直使用的相同方法,但我没有看到或发现更好的方法。正如人们所说,PHP 是单线程的,因此您无能为力。

实际上,我为此添加了一个额外的级别,即获取和存储进程 ID。这允许我重定向到另一个页面并让用户坐在该页面上,使用 AJAX 检查进程是否完成(进程 ID 不再存在)。这对于脚本长度会导致浏览器超时的情况很有用,但用户需要等待该脚本完成才能进行下一步。 (在我的例子中,它正在处理带有类似 CSV 文件的大型 ZIP 文件,这些文件最多可向数据库添加 30 000 条记录,之后用户需要确认一些信息。)

我也使用了类似的过程来生成报告。我不确定我是否会对诸如电子邮件之类的东西使用“后台处理”,除非 SMTP 速度慢确实存在问题。相反,我可能会使用一个表作为队列,然后有一个每分钟运行一次的进程来发送队列中的电子邮件。您需要警惕两次发送电子邮件或其他类似问题。对于其他任务,我也会考虑类似的排队过程。


您在第一句话中指的是哪种方法?
K
Kjeld

按照 rojoca 的建议使用 cURL 是个好主意。

这是一个例子。您可以在脚本在后台运行时监控 text.txt:

<?php

function doCurl($begin)
{
    echo "Do curl<br />\n";
    $url = 'http://'.$_SERVER['SERVER_NAME'].$_SERVER['REQUEST_URI'];
    $url = preg_replace('/\?.*/', '', $url);
    $url .= '?begin='.$begin;
    echo 'URL: '.$url.'<br>';
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    $result = curl_exec($ch);
    echo 'Result: '.$result.'<br>';
    curl_close($ch);
}


if (empty($_GET['begin'])) {
    doCurl(1);
}
else {
    while (ob_get_level())
        ob_end_clean();
    header('Connection: close');
    ignore_user_abort();
    ob_start();
    echo 'Connection Closed';
    $size = ob_get_length();
    header("Content-Length: $size");
    ob_end_flush();
    flush();

    $begin = $_GET['begin'];
    $fp = fopen("text.txt", "w");
    fprintf($fp, "begin: %d\n", $begin);
    for ($i = 0; $i < 15; $i++) {
        sleep(1);
        fprintf($fp, "i: %d\n", $i);
    }
    fclose($fp);
    if ($begin < 10)
        doCurl($begin + 1);
}

?>

如果对源代码进行注释,那将非常有帮助。我不知道那里发生了什么,哪些部分是示例,哪些部分可以重复用于我自己的目的。
y
yogibear

有一个名为 Swoole 的 PHP 扩展。

尽管它可能未启用,但在我的主机上可以通过单击按钮启用它。

值得一试。我还没有时间使用它,因为我在这里搜索信息,当我偶然发现它并认为它值得分享时。


P
Peter D

不幸的是,PHP 没有任何类型的本机线程功能。所以我认为在这种情况下你别无选择,只能使用某种自定义代码来做你想做的事。

如果你在网上搜索 PHP 线程的东西,有些人想出了在 PHP 上模拟线程的方法。


P
Peter

如果您在“感谢您注册”响应中设置 Content-Length HTTP 标头,则浏览器应在收到指定字节数后关闭连接。这会使服务器端进程运行(假设设置了 ignore_user_abort),因此它可以在不让最终用户等待的情况下完成工作。

当然,您需要在渲染标头之前计算响应内容的大小,但这对于短响应(将输出写入字符串、调用 strlen()、调用 header()、渲染字符串)非常容易。

这种方法的优点是不强制您管理“前端”队列,尽管您可能需要在后端做一些工作以防止竞争 HTTP 子进程相互踩踏,但这是您已经需要做的事情, 反正。


这似乎不起作用。当我使用 header('Content-Length: 3'); echo '1234'; sleep(5); 时,即使浏览器只需要 3 个字符,它仍然会等待 5 秒才能显示响应。我错过了什么?
@ThomasTempelmann - 您可能需要调用 flush() 以强制立即实际呈现输出,否则输出将被缓冲,直到您的脚本退出或将足够的数据发送到 STDOUT 以刷新缓冲区。
我已经尝试了很多方法来冲洗,在 SO 上找到了。没有帮助。从 phpinfo() 可以看出,数据似乎也是以非 gzip 格式发送的。我能想象的唯一另一件事是我需要首先达到最小缓冲区大小,例如 256 字节左右。
@ThomasTempelmann - 我在您的问题或我对 gzip 的回答中看不到任何内容(在添加复杂层之前先让最简单的场景工作通常是有意义的)。为了确定服务器何时实际发送数据,您可以使用浏览器插件的数据包嗅探器(如 fiddler、tamperdata 等)。然后,如果您发现无论是否刷新,网络服务器实际上都保留了所有脚本输出直到退出,那么您需要修改您的网络服务器配置(在这种情况下,您的 PHP 脚本无能为力)。
我使用虚拟 Web 服务,因此我几乎无法控制它的配置。我希望找到其他关于可能是罪魁祸首的建议,但您的答案似乎并不像看起来那样普遍适用。显然,太多的事情可能会出错。您的解决方案肯定比这里给出的所有其他答案更容易实施。太糟糕了,它对我不起作用。
p
phpPhil

如果您不想要完整的 ActiveMQ,我建议您考虑 RabbitMQ。 RabbitMQ 是使用 AMQP standard 的轻量级消息传递。

我还建议查看 php-amqplib - 一个流行的 AMQP 客户端库,用于访问基于 AMQP 的消息代理。


C
Community

我认为您应该尝试这种技术,它将有助于调用尽可能多的页面,所有页面将同时独立运行,而无需等待每个页面响应异步。

cornjobpage.php //主页

    <?php

post_async("http://localhost/projectname/testpage.php", "Keywordname=testValue");
//post_async("http://localhost/projectname/testpage.php", "Keywordname=testValue2");
//post_async("http://localhost/projectname/otherpage.php", "Keywordname=anyValue");
//call as many as pages you like all pages will run at once independently without waiting for each page response as asynchronous.
            ?>
            <?php

            /*
             * Executes a PHP page asynchronously so the current page does not have to wait for it to     finish running.
             *  
             */
            function post_async($url,$params)
            {

                $post_string = $params;

                $parts=parse_url($url);

                $fp = fsockopen($parts['host'],
                    isset($parts['port'])?$parts['port']:80,
                    $errno, $errstr, 30);

                $out = "GET ".$parts['path']."?$post_string"." HTTP/1.1\r\n";//you can use POST instead of GET if you like
                $out.= "Host: ".$parts['host']."\r\n";
                $out.= "Content-Type: application/x-www-form-urlencoded\r\n";
                $out.= "Content-Length: ".strlen($post_string)."\r\n";
                $out.= "Connection: Close\r\n\r\n";
                fwrite($fp, $out);
                fclose($fp);
            }
            ?>

测试页.php

    <?
    echo $_REQUEST["Keywordname"];//case1 Output > testValue
    ?>

PS:如果您想将 url 参数作为循环发送,请遵循以下答案:https://stackoverflow.com/a/41225209/6295712


G
Greg Glockner

使用 exec() 在服务器上生成新进程或使用 curl 直接在另一台服务器上生成新进程根本不能很好地扩展,如果我们选择 exec,您基本上是在用可以由其他非 Web 处理的长时间运行的进程填充您的服务器面对服务器,并且使用 curl 会占用另一台服务器,除非您构建某种负载平衡。

我在一些情况下使用过 Gearman,我发现它更适合这种用例。我可以使用单个作业队列服务器基本上处理所有需要由服务器完成的作业的排队并启动工作服务器,每个工作服务器都可以根据需要运行尽可能多的工作进程实例,并扩大数量根据需要工作服务器,并在不需要时将它们关闭。它还让我在需要时完全关闭工作进程并将作业排队,直到工作人员重新上线。


M
Marc W

PHP 是一种单线程语言,因此除了使用 execpopen 之外,没有其他官方方法可以使用它来启动异步进程。有一篇关于该 here 的博文。您对 MySQL 中的队列的想法也是一个好主意。

您在这里的具体要求是向用户发送电子邮件。我很好奇您为什么要尝试异步执行此操作,因为发送电子邮件是一项非常简单且快速的任务。我想如果您要发送大量电子邮件并且您的 ISP 因涉嫌垃圾邮件而阻止您,这可能是排队的一个原因,但除此之外,我想不出任何理由这样做。


电子邮件只是一个例子,因为其他任务更难解释,这并不是问题的重点。我们过去发送电子邮件的方式是,在远程服务器接受邮件之前,电子邮件命令不会返回。我们发现一些邮件服务器被配置为在接受邮件之前添加较长的延迟(比如 10-20 秒的延迟)(可能是为了对抗垃圾邮件机器人),然后这些延迟会传递给我们的用户。现在,我们正在使用本地邮件服务器来排队要发送的邮件,所以这个特定的不适用,但我们还有其他类似性质的任务。
例如:通过具有 ssl 和端口 465 的 Google Apps Smtp 发送电子邮件比平时花费更长的时间。