我使用PHP,有大约10个任务需要运行。它们中的每一个都不应该超时,但所有10个任务加在一起可能会超时。
使用模块化方法处理新的http请求是一个好的解决方案吗?
像这样:
http://example.com/some/module/fetch
http://example.com/some/module/parse
http://example.com/some/module/save
也许这些url每个只做一个任务。如果成功了,从这个任务开始做下一个任务。一种连锁反应。一个路径调用下一个路径(带curl)。
利弊?这是一个好方法吗?如果不是,有什么更好的选择?
模块化的方法是一个好主意(如果一个"单元"失败了,工作就像你想的那样停止;此外,它更容易调试/测试每个单独的单元)。
它将工作,但你的方法链接有一些问题:
- 如果出现瓶颈(即一个"单元"比其他"单元"花费的时间更长),那么你可能最终会有100个瓶颈进程全部运行,并且你失去了对服务器资源的控制
- 缺乏控制;假设服务器需要重新启动:要重新启动作业,那么您需要从头启动它们。
- 类似地,如果有原因需要在运行时停止/启动/调试单个单元,则需要在第一个单元重新启动作业以重复。
- 通过发出web请求,您正在使用Apache/NGIX资源,内存,套接字连接等,只是为了运行PHP进程。你可以直接运行PHP进程,而不用使用这些开销。
- ,最后,如果在DMZ web服务器上,服务器实际上可能无法向自己发出请求。
为了获得更多的控制,您应该使用排队系统进行此类操作。
使用PHP(或任何语言),您的基本过程是:
-
每个"unit"是一个连续循环的php脚本,永远不会结束*
-
每个"单元"进程监听一个排队系统;当一个作业到达它可以处理的队列时,它就把它从队列中取出
-
当每个单元完成作业时,它确认已处理并推送到下一个队列。
-
如果单元决定该作业不应继续,则确认该作业已处理,但不将其推送到下一个队列。
优势:
- 如果一个"单元"停止了,那么该作业仍然留在队列中,可以在重新启动"单元"时收集。使它更容易重新启动单元/服务器或如果一个单元崩溃。
- 如果一个"单元"非常重,如果您有空间服务器容量,您可以启动第二个进程做完全相同的事情。如果没有服务器容量,则接受瓶颈;因此,您可以非常透明地查看您使用了多少资源。
- 如果你决定另一种语言可以更好地处理请求,你可以混合使用NodeJS, Python, Ruby和…它们都可以与相同的队列通信。
关于"持续循环PHP"的边注:这是通过设置max_execution_time为"0"来完成的。确保你没有造成"内存泄漏",并且保持干净。您可以在启动时自动启动该进程(取决于操作系统的systemd或任务调度程序),也可以手动运行以进行测试。如果您不想让它持续循环,请在5分钟后超时并重新启动cron/task scheduler。
关于队列的边注:对于简单的应用程序,你可以使用数据库的内存缓存来"滚动你自己的"(例如,使用数据库系统可以轻松地处理一个队列中每小时10万个项目),但避免冲突/管理状态/重试有点像艺术。一个更好的选择是RabbitMQ (https://www.rabbitmq.com/)。安装起来有点麻烦,但是一旦安装好了,按照PHP教程做,你就再也不会回头了!
假设你想使用HTTP请求,你有几个选项,设置一个超时,每次少:
function doTaskWithEnd($uri, $end, $ctx = null) {
if (!$ctx) { $ctx = stream_context_create(); }
stream_context_set_option($ctx, "http", "timeout", $end - time());
$ret = file_get_contents($uri, false, $ctx));
if ($ret === false) {
throw new Exception("Request failed or timed out!");
}
return $ret;
}
$end = time() + 100;
$fetched = doTaskWithEnd("http://example.com/some/module/fetch", $end);
$ctx = stream_context_create(["http" => ["method" => "POST", "content" => $fetched]]);
$parsed = doTaskWithEnd("http://example.com/some/module/parsed", $end, $ctx);
$ctx = stream_context_create(["http" => ["method" => "PUT", "content" => $parsed]]);
doTaskWithEnd("http://example.com/some/module/save", $end, $ctx);
或者,使用非阻塞解决方案(让我们使用amphp/amp + amphp/artax):
function doTaskWithTimeout($requestPromise, $timeout) {
$ret = yield Ampfirst($requestPromise, $timeout);
if ($ret === null) {
throw new Exception("Timed out!");
}
return $ret;
}
Ampexecute(function() {
$end = new AmpPause(100000); /* timeout in ms */
$client = new AmpArtaxClient;
$fetched = yield from doTaskWithTimeout($client->request("http://example.com/some/module/fetch"));
$req = (new AmpArtaxRequest)
->setUri("http://example.com/some/module/parsed")
->setMethod("POST")
->setBody($fetched)
;
$parsed = yield from doTaskWithTimeout($client->request($req), $end);
$req = (new AmpArtaxRequest)
->setUri("http://example.com/some/module/save")
->setMethod("PUT")
->setBody($parsed)
;
yield from doTaskWithTimeout($client->request($req), $end);
});
现在,我问,你真的想卸载到单独的请求吗?难道我们不能假设现在有函数fetch()
, parse($fetched)
和save($parsed)
吗?
在这种情况下很容易,我们可以设置一个警报:
declare(ticks=10); // this declare() line must happen before the first include/require
pcntl_signal(SIGALRM, function() {
throw new Exception("Timed out!");
});
pcntl_alarm(100);
$fetched = fetch();
$parsed = parse($fetched);
save($parsed);
pcntl_alarm(0); // we're done, reset the alarm
或者,非阻塞解决方案也可以工作(假设fetch()
, parse($fetched)
和save($parsed)
正确返回promise并且是非阻塞设计的):
Ampexecute(function() {
$end = new AmpPause(100000); /* timeout in ms */
$fetched = yield from doTaskWithTimeout(fetch(), $end);
$parsed = yield from doTaskWithTimeout(parse($fetched), $end);
yield from doTaskWithTimeout(save($parsed), $end);
});
如果你只是想要对不同的顺序任务有一个全局超时,我最好在一个脚本中使用pcntl_alarm()
,或者使用流上下文超时选项。
非阻塞解决方案主要适用于您碰巧需要同时做其他事情的情况。例如,如果您想多次执行fetch+parse+save循环,则需要独立于其他循环。
我认为"连锁反应"是一个线索,这种方法可能过于复杂…
切换到健壮的消息传递/工作队列系统(如RabbitMQ或SQS)可能有很好的理由,特别是当你处理大量负载时。消息传递队列在适当的上下文中是无价的,但是如果不必要地使用它们,它们会增加很多复杂性/开销/代码。
简单解决方案
…但如果你唯一关心的是防止超时,我不会让它比它需要更复杂;您可以使用以下命令轻松地完全扩展或禁用超时:
set_time_limit(0); //no time limit, not recommended
set_time_limit(300); //5 mins
你建议的"链接"模式在原则上是明智的,因为它允许你精确地识别任何错误发生的地方,但是你可以在相同的请求/函数中完成所有这些,而不是依赖于网络。
这将需要两层(或更多层)故障处理,而不是在一个整洁的位置处理故障:一层处理单个请求,另一层发出请求。
假设工作可以在单个请求中成功处理(甚至根本没有远程请求),那么 no ,这不是一个"使用模块化方法处理新http请求的好解决方案",因为你增加了不必要的工作&通过进行不必要的http调用/响应来增加复杂性",即这引入了额外的失败可能性,特别是网络连接/延迟,DNS,测试难度和;调试等。
分离成单独的远程调用甚至可能增加10倍的网络/服务器/身份验证延迟,并使做明智的事情(如数据库连接池)变得更加棘手。
还有其他简化问题的方法吗?
如果可能的话,可能值得研究一下为什么这个请求链需要这么长时间——如果您可以优化它们以运行得更快,您可能能够避免在系统的这一部分中增加不必要的复杂性。例如,像数据库延迟或不使用数据库连接池这样的事情可能会增加10个独立进程的严重开销。
这个答案假设您正在使用PHP并通过向问题中的每个url发出HTTP请求来运行任务。
您的解决方案取决于您的业务需求。如果你不关心HTTP请求的完成顺序,我建议你看看curl_multi_init(),开始学习cURL PHP扩展的curl_multi_*函数。
如果你确实关心完成的顺序(例如,一个特定的任务必须在下一个任务之前完成),看看curl_init()。
为了消除调用脚本超时的可能性,请阅读set_time_limit函数或考虑使用pcntl_fork fork进程。
或者,我会研究一个消息队列。具体来说,请查看Amazon的SQS,并阅读如何在PHP中与它进行接口。这里有一些关于SQS和PHP的链接:
- http://docs.aws.amazon.com/aws-sdk-php/v2/guide/service-sqs.html
- http://george.webb.uno/posts/aws-simple-queue-service-php-sdk
- 消息队列的有效架构PHP中的Worker系统?
有工人的后台工作是最好的方法,因为:
应用程序通常需要执行时间(或计算)密集的操作,但通常不希望在请求期间这样做,因为由此导致的缓慢会被应用程序的用户直接感知到。相反,任何耗时超过几十毫秒的任务,如图像处理、发送电子邮件或任何类型的后台同步,都应该作为后台任务执行。此外,工作队列还使执行计划作业变得容易,因为时钟进程可以利用相同的队列基础结构。
使用PHP请求来实现后台任务: