异步运行PHP任务


144

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

到目前为止,在某些地方,我一直在使用exec()感觉像是被黑客入侵。基本上是这样的:

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

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

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

Answers:


80

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

自己滚动不是很棘手,以下是一些其他可供选择的选项:

  • GearMan-该答案写于2009年,从那时起,GearMan看起来很受欢迎,请参阅下面的评论。
  • 如果您想要完整的开源消息队列,请使用ActiveMQ
  • ZeroMQ-这是一个非常酷的套接字库,可以轻松编写分布式代码,而不必担心套接字编程本身。您可以将其用于单个主机上的消息排队-您只需将Web应用程序推送到队列中,然后连续运行的控制台应用程序将在下一个合适的机会使用该队列
  • beanstalkd-仅在编写此答案时找到了这个,但看起来很有趣
  • dropr是一个基于PHP的消息队列项目,但是自2010年9月以来一直没有得到积极维护
  • php-enqueue是最近(2017年)维护的各种队列系统包装器
  • 最后,有关使用memcached进行消息排队的博客文章

另一种可能更简单的方法是使用ignore_user_abort-将页面发送给用户后,您可以进行最终处理而不必担心过早终止,尽管这样做的确会延长用户页面的加载时间透视。


感谢所有的提示。关于ignore_user_abort的特定内容对我而言并没有真正帮助,我的整个目标是避免给用户不必要的延迟。
davr

2
如果在“感谢注册”响应中设置Content-Length HTTP标头,则浏览器应在收到指定的字节数后关闭连接。这样就使服务器端进程可以运行(假设设置了ignore_user_abort),而无需等待最终用户。当然,在呈现标题之前,您将需要计算响应内容的大小,但是对于简短响应而言,这很容易。
彼得

1
Gearman(gearman.org)是一个很棒的跨平台的开源消息队列。您可以使用C,PHP,Perl或几乎任何其他语言编写工作程序。有适用于MySQL的Gearman UDF插件,您也可以从PHP或Gearman pear客户端使用Net_Gearman。
贾斯汀·斯旺哈特

我会在今天(2015年)推荐的任何自定义工作队列系统上使用Gearman。
彼得

另一个选择是设置一个节点js服务器来处理请求并返回一个介于两者之间的任务的快速响应。节点js脚本中的许多内容都是异步执行的,例如http请求。
Zordon 2015年

22

当您只想执行一个或几个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不起作用
Baptiste Pernet 2014年

17

分叉过程的另一种方法是通过卷曲。您可以将内部任务设置为Web服务。例如:

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

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

您的服务可以使用mysql或任何您喜欢的点来跟踪任务队列:它们全部包装在服务中,并且脚本仅占用URL。这样可以释放您的服务,必要时将其移动到另一台计算机/服务器(即易于扩展)。

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

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


1
我不喜欢这种做法,因为它重载Web服务器
Oved的Yavine

7

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

我已经完成的几件事是:

  • 调整图像大小-并通过将轻载队列传递给基于CLI的PHP脚本,重新调整大图像(大于2mb)的效果很好,但是尝试在mod_php实例中调整相同图像的大小经常会遇到内存空间问题(I将PHP进程限制为32MB,并且调整大小不止于此)
  • 不久的将来的检查-beantalkd有延迟可用(使此作业仅在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;or 的默认值die();),它将首先暂停几秒钟。


我喜欢Beantalkd的外观,一旦添加了持久性,我认为它将是完美的。
davr

多数民众赞成在代码库中已经稳定。我也很期待“命名的工作”,所以我可以把东西扔在那里,但是知道如果已经有一个,就不会添加。适合定期活动。
Alister Bulman,2009年

@AlisterBulman您可以为“长时间运行的cli脚本然后从队列中提取作业”提供更多信息或示例。我正在尝试为我的应用程序构建这样的cli脚本。
Sasi varna kumar,2016年

7

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

此函数将所有响应数据刷新到客户端并完成请求。这允许执行耗时的任务,而无需断开与客户端的连接。

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

  1. 首先制作所有主要代码。
  2. 执行fastcgi_finish_request()
  3. 使所有沉重的东西。

再次需要php-fpm。


5

这是我为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);
        }
    }
}

4

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

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

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


1
您在第一句话中指的是哪种方法?
西蒙·伊斯特


2

按照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);
}

?>

2
如果对源代码进行注释,那将真的有帮助。我不知道那里发生了什么,哪些部分是示例,哪些部分可重复使用以达到我的目的。
Thomas Tempelmann

1

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

如果您在网上搜索PHP线程内容,则有些人想出了在PHP上模拟线程的方法。


1

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

当然,在呈现标题之前,您将需要计算响应内容的大小,但是对于简短响应(将输出写入字符串,调用strlen(),调用header(),呈现字符串)来说,这非常容易。

这种方法的优点是不会强迫您管理“前端”队列,尽管您可能需要在后端做一些工作以防止HTTP子进程相互竞争,但这已经是您需要做的,无论如何。


这似乎不起作用。当我使用header('Content-Length: 3'); echo '1234'; sleep(5);then时,即使浏览器仅占用3个字符,它仍要等待5秒钟才能显示响应。我想念什么?
Thomas Tempelmann

@ThomasTempelmann-您可能需要调用flush()来强制立即立即渲染输出,否则输出将被缓冲,直到脚本退出或有足够的数据发送到STDOUT刷新缓冲区为止。
彼得

我已经尝试过许多冲洗方法,可以在此处找到。没有帮助。正如人们从中可以看到的,数据似乎也未压缩发送phpinfo()。我可以想象的唯一另一件事是,我需要首先达到最小缓冲区大小,例如256个字节左右。
Thomas Tempelmann

@ThomasTempelmann-在您的问题或我对gzip的回答中,我没有看到任何东西(通常最有意义的情况是先使最简单的方案开始工作,然后再增加复杂性)。为了确定服务器实际发送数据的时间,您可以使用浏览器插件的数据包嗅探器(例如fiddler,tamperdata等)。然后,如果您发现Web服务器实际上一直保留所有脚本输出,直到无论是否刷新都退出为止,那么您就需要修改Web服务器配置(在这种情况下,PHP脚本无法执行任何操作)。
彼得

我使用虚拟Web服务,因此对其配置几乎没有控制权。我希望找到其他有关可能是罪魁祸首的建议,但似乎您的答案并不像看起来那样普遍适用。显然,有太多事情可能出错。您的解决方案肯定比此处给出的所有其他解决方案容易实施。太糟糕了,它对我不起作用。
Thomas Tempelmann

1

如果您不希望使用完整的ActiveMQ,建议您使用RabbitMQ。RabbitMQ是使用AMQP标准的轻量级消息传递

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


0

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

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);
            }
            ?>

testpage.php

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

PS:如果要发送URL参数作为循环,请按照以下答案进行操作:https : //stackoverflow.com/a/41225209/6295712


0

exec()使用curl 在服务器上或直接在使用curl的另一台服务器上生成新进程根本无法很好地扩展,如果我们选择exec,则基本上是在用长时间运行的进程填充服务器,而其他非Web面对的服务器可以处理这些进程,除非您建立了某种负载平衡,否则使用curl将束缚另一台服务器。

我在某些情况下使用了Gearman,我发现这种用例会更好。我可以使用单个作业队列服务器来基本上处理该服务器需要完成的所有作业的排队并启动工作服务器,每个工作服务器都可以根据需要运行尽可能多的工作进程实例,并扩展所需的辅助服务器,并在不需要时降低它们的速度。这也让我在需要时完全关闭了工作进程,并将作业排队,直到工作人员重新联机。


-4

PHP是一种单线程语言,因此除了使用exec或之外,没有其他正式的方法可以使用它启动异步进程popen。有一个博客帖子大约是在这里。您对于在MySQL中排队的想法也是一个好主意。

您在这里的特定要求是向用户发送电子邮件。我很好奇您为什么要尝试异步执行此操作,因为发送电子邮件是一项非常琐碎且快速的任务。我想如果您正在发送大量电子邮件,而您的ISP因为怀疑是垃圾邮件而阻止了您,那可能是排队的原因之一,但除此之外,我认为没有任何理由可以这样做。


电子邮件只是一个例子,因为其他任务的解释更为复杂,而这并不是问题的实质。我们以前发送电子邮件的方式,直到远程服务器接受邮件后,email命令才会返回。我们发现某些邮件服务器被配置为在接收邮件之前添加较长的延迟(例如10-20秒的延迟)(可能是为了防止垃圾邮件),然后将这些延迟传递给我们的用户。现在,我们正在使用本地邮件服务器将要发送的邮件排队,因此该特定邮件服务器不适用,但是我们还有其他性质相似的任务。
davr

例如:通过带有ssl和端口465的Google Apps Smtp发送电子邮件所花费的时间比平时更长。
Gixty
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.