异步运行 PHP 任务
-
21-08-2019 - |
题
我正在开发一个较大的 Web 应用程序,后端主要是 PHP 。代码中有几个地方我需要完成某些任务,但我不想让用户等待结果。例如,当创建一个新帐户时,我需要向他们发送一封欢迎电子邮件。但是,当他们点击“完成注册”按钮时,我不想让他们等到电子邮件实际发送,我只想开始该过程,并立即向用户返回消息。
到目前为止,在某些地方我一直在使用 exec() 的感觉。基本上做这样的事情:
exec("doTask.php $arg1 $arg2 $arg3 >/dev/null 2>&1 &");
这似乎有效,但我想知道是否有更好的方法。我正在考虑编写一个在 MySQL 表中对任务进行排队的系统,以及一个单独的长时间运行的 PHP 脚本,该脚本每秒查询该表一次,并执行它找到的任何新任务。如果需要的话,这还有一个好处,让我将来可以将任务分配给多台工作机器。
我是在重新发明轮子吗?有没有比 exec() hack 或 MySQL 队列更好的解决方案?
解决方案
我使用了排队方法,它效果很好,因为您可以推迟处理,直到服务器负载空闲,如果您可以轻松地划分“不紧急的任务”,那么您可以非常有效地管理负载。
自己动手并不太棘手,这里还有一些其他选项可供查看:
- 齿轮人 - 这个答案写于 2009 年,从那时起 GearMan 看起来是一个流行的选择,请参阅下面的评论。
- ActiveMQ 如果您想要一个完整的开源消息队列。
- 零MQ - 这是一个非常酷的套接字库,它可以轻松编写分布式代码,而不必过多担心套接字编程本身。您可以将它用于单个主机上的消息队列 - 您只需让您的 Web 应用程序将某些内容推送到队列中,连续运行的控制台应用程序将在下一个合适的机会使用该队列
- 豆茎 - 在写这个答案时才发现这个,但看起来很有趣
- 滴注器 是一个基于 PHP 的消息队列项目,但自 2010 年 9 月以来一直没有积极维护
- php-enqueue 是最近(2017 年)维护的各种队列系统的包装器
- 最后,一篇关于使用的博客文章 memcached 用于消息队列
另一种也许更简单的方法是使用 忽略用户中止 - 将页面发送给用户后,您可以进行最终处理,而不必担心提前终止,尽管从用户角度来看,这确实会延长页面加载时间。
其他提示
当你只是要执行的一个或几个的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连接关闭脚本继续运行不受时间限制。
另一种分叉进程的方法是通过curl。您可以将内部任务设置为 Web 服务。例如:
然后在您的用户访问脚本中调用该服务:
$service->addTask('t1', $data); // post data to URL via curl
您的服务可以使用 mysql 或任何您喜欢的方式跟踪任务队列:它全部包含在服务中,您的脚本只是使用 URL。这使您可以在必要时将服务转移到另一台机器/服务器(即轻松扩展)。
添加http授权或自定义授权方案(如亚马逊的网络服务)可以让您开放您的任务以供其他人/服务使用(如果您愿意),您可以更进一步,在顶部添加一个监控服务来跟踪队列和任务状态。
它确实需要一些设置工作,但有很多好处。
我用过 豆茎 对于一个项目,并计划再次进行。我发现它是运行异步进程的绝佳方法。
我用它做了几件事:
- 图像调整大小 - 并且将轻负载队列传递给基于 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();
)它首先会暂停几秒钟。
如果只是提供昂贵任务的问题,在支持 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不再存在)。这是一个情况下,脚本的长度会导致浏览器超时是有用的,但用户需要等待脚本下一步之前完成。 (在我的情况下,它被加工大ZIP文件与CSV像高达30个000记录添加到用户之后,需要确认的一些信息的数据库文件。)
我还用于报告生成类似的过程。我不知道我会使用“后台处理”的东西,如电子邮件,除非有一个缓慢的SMTP一个真正的问题。相反,我可能会使用一个表作为队列,然后有运行每分钟发送队列中的电子邮件的过程。你将需要warry发送电子邮件两次或其他类似的问题。我会考虑其他的任务类似的排队过程也是如此。
这是使用卷曲一个伟大的想法通过rojoca的建议。
下面是一个例子。您可以在脚本运行在后台监视的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()调用header()函数,使字符串)。
该方法的优点是的不的迫使你管理一个“前端”队列,虽然你可能需要做后端的一些工作,防止赛车HTTP子进程从踩着对方,这东西你需要做的已经,反正。
如果你不想完全成熟的ActiveMQ的,我建议考虑的RabbitMQ 。 RabbitMQ的是使用 AMQP标准。
我建议也考虑 PHP-amqplib - 一个流行的AMQP客户端库访问AMQP基于消息代理。
我想你应该尝试这种技术将有助于打电话给你喜欢的所有页面将同时独立运行,而无需等待每一页响应异步多达页面。
<强> 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
在服务器上使用curl都没有规模那么好使用exec()
或者直接在另一个服务器上产卵新的流程,如果我们去EXEC你基本上是与可以通过其他非处理长时间运行的进程填满你的服务器面向网服务器,并使用curl联系了另一台服务器,除非你在某种负载均衡的建设。
我已经在少数情况下使用的Gearman,我对这种情况下使用更好的找到它。我可以用一个作业队列服务器基本上处理所有的工作需要由服务器来完成,旋转起来工作者服务器,每个服务器都可以运行需要的工作进程的多个实例排队,并扩大数量需要和自旋下来,不需要的时候工人服务器作为。这也让我的完全关闭工作进程在需要的时候和队列作业,直到工人回来在线。
PHP是单线程的语言,所以没有正式开始与它比使用exec
或popen
其它异步处理方式。有关于这里一博客文章>。您的MySQL中的队列想法是一个好主意为好。
在这里您的具体要求是发送电子邮件给用户。我很好奇,为什么你正在尝试做的异步自发送电子邮件是执行相当琐碎和快速的任务。我想,如果你比我想不出任何理由做这种方式发送电子邮件吨和您的ISP阻止你对垃圾邮件的嫌疑,这可能是一个原因要排队,但其他。