异步运行PHP任务
我工作在一个有点大的Web应用程序,而后端主要是在PHP中。 代码中有几个地方需要完成一些任务,但我不想让用户等待结果。 例如,在创建新帐户时,我需要向他们发送一封欢迎邮件。 但是,当他们点击“完成注册”按钮时,我不想让他们等到电子邮件实际发送,我只是想启动过程,并立即返回一个消息给用户。
直到现在,在一些地方,我一直在用exec()来进行攻击。 基本上做这样的事情:
exec("doTask.php $arg1 $arg2 $arg3 >/dev/null 2>&1 &");
这似乎工作,但我想知道如果有更好的办法。 我正在考虑编写一个在MySQL表中排队任务的系统,以及一个单独的长时间运行的PHP脚本,每秒查询一次这个表,并执行它找到的任何新任务。 如果需要的话,这也可以让我在未来的几个工人机器之间分配任务。
我正在重新发明轮子吗? 有没有比exec()黑客或MySQL队列更好的解决方案?
我已经使用了排队方法,它可以很好地工作,因为您可以延迟处理,直到您的服务器负载空闲,让您可以相当有效地管理您的负载,如果你可以轻松地分割出“不紧急的任务”。
滚动你自己不是太棘手,这里有一些其他的选项来检查:
- GearMan – 这个答案是在2009年写的,从那以后GearMan看起来很受欢迎,见下面的评论。
- ActiveMQ,如果你想要一个完整的开源消息队列。
- ZeroMQ – 这是一个非常酷的套接字库,可以很容易地编写分布式的代码,而不必担心套接字编程本身。 您可以将它用于单个主机上的消息队列 – 您只需将Web应用程序推送到队列中,即可以在下一个合适的机会中持续运行的控制台应用程序
- beanstalkd – 只有在写这个答案的时候才发现这个,但看起来很有趣
- dropr是一个基于PHP的消息队列项目,但从2010年9月起还没有被积极维护
- php-enqueue是一个最近(2017)维护包装各种队列系统
- 最后,有关使用memcached进行消息队列的博客文章
另一个也许更简单的方法是使用ignore_user_abort – 一旦你发送了页面给用户,你可以做你最后的处理,而不用担心提前终止,尽管这样做的确会延长用户的页面加载时间透视。
fork过程的另一种方式是通过curl。 您可以将您的内部任务设置为web服务。 例如:
- HTTP://域名/任务/ T1
- HTTP://域名/任务/ T2
然后在你的用户访问的脚本中调用服务:
$service->addTask('t1', $data); // post data to URL via curl
你的服务可以使用mysql或任何你喜欢的东西来跟踪任务的队列,重点是:它全部包装在服务中,你的脚本只是使用URL。 如果需要,这可以让您将服务移动到另一台机器/服务器(即可轻松扩展)。
添加http授权或自定义授权方案(如亚马逊的网络服务)可让您打开任务供其他人员/服务使用(如果需要),并且可以进一步采取措施并在上方添加监控服务以跟踪队列和任务状态。
- HTTP://域名/队列任务= T1
- HTTP://域名/队列任务= T2
- HTTP://域/队列/ T1 / 100931
这需要一些设置工作,但有很多好处。
当您只是想执行一个或多个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连接关闭时继续运行而没有时间限制。
我已经使用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-script在重启之前只运行了50个循环,但是如果它想按计划重新启动的话,它会马上执行(通过bash脚本运行)。 如果出现问题,我exit(0)
( exit(0)
的默认值exit;
或die();
)它会暂停几秒钟。
如果只是提供昂贵任务的问题,为了支持php-fpm,为什么不使用fastcgi_finish_request()
函数呢?
这个函数将所有的响应数据刷新到客户端并完成请求。 这允许执行耗时的任务,而不会断开到客户端的连接。
你不用这种方式来实现异步性:
- 把所有的主要代码放在第一位。
- 执行
fastcgi_finish_request()
。 - 做所有重的东西。
再一次需要php-fpm。
这是我为我的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); } } }
这是我一直使用了几年的方法,我没有看到或发现任何更好的。 正如人们所说,PHP是单线程的,所以没有其他的东西可以做。
我实际上已经添加了一个额外的级别,这是获取和存储进程ID。 这允许我重定向到另一个页面,让用户坐在那个页面上,使用AJAX来检查进程是否完成(进程ID不再存在)。 这对于脚本的长度会导致浏览器超时的情况很有用,但是用户需要在下一步之前等待脚本完成。 (在我的情况下,它正在处理CSV文件的大型ZIP文件,加起来30 000条记录到数据库,然后用户需要确认一些信息。)
我也使用了类似的过程来生成报告。 我不确定我会使用诸如电子邮件之类的“后台处理”,除非慢速SMTP存在真正的问题。 相反,我可以使用一个表作为队列,然后有一个运行每分钟的进程来发送队列中的电子邮件。 你需要发送两次电子邮件或其他类似的问题。 我也会考虑类似的排队过程来处理其他任务。
PHP HAS多线程,它只是没有启用默认情况下,有一个扩展名为pthreads正是这样做。 你需要用ZTS编译的。 (线程安全)链接:
例子
另一个教程
pthreads PECL扩展
按照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); } ?>
不幸的是,PHP没有任何一种本地线程能力。 所以我认为在这种情况下,你别无选择,只能使用某种自定义代码来做你想做的事情。
如果你在网络上搜索PHP线程的东西,有人已经想出了在PHP上模拟线程的方法。
如果您在“谢谢您注册”响应中设置Content-Length HTTP标头,则在收到指定的字节数后,浏览器应关闭连接。 这将使服务器端进程运行(假设ignore_user_abort已设置),以便它可以在不使最终用户等待的情况下完成工作。
当然,在渲染头文件之前,你需要计算你的响应内容的大小,但是对于短的响应(把输出写入一个字符串,调用strlen(),调用头(),渲染字符串)来说,这是非常容易的。
这种方法的优点是不会强迫你管理一个“前端”队列,虽然你可能需要在后端做一些工作,以防止竞争的HTTP子进程彼此踩,这是你需要做的事情无论如何。
如果你不想要完整的ActiveMQ,我建议考虑RabbitMQ 。 RabbitMQ是使用AMQP标准的轻量级消息传递。
我建议也看看php-amqplib – 一个流行的AMQP客户端库来访问基于AMQP的消息代理。
我认为你应该尝试这种技术,它将有助于调用所有页面一样多的页面,而不用等待每个页面响应的异步。
cornjobpage.php // mainpage
<?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
PHP是一种单线程语言,因此除了使用exec
或popen
之外,没有官方的方式来启动异步进程。 这里有一篇博文。 你在MySQL中创建队列的想法也是一个好主意。
您的具体要求是发送电子邮件给用户。 我很好奇你为什么试图异步地做这件事,因为发送电子邮件是一个非常简单快速的任务。 我猜想如果你发了大量的电子邮件,而你的互联网服务供应商因为怀疑垃圾邮件而阻止了你,这可能是排队的原因之一,但除此之外,我想不出有任何理由这样做。