【问题标题】:PHP prevent timeout by using modules that uses http requestsPHP 通过使用使用 http 请求的模块来防止超时
【发布时间】:2016-12-31 06:13:46
【问题描述】:

我使用 PHP 并且有大约 10 个需要运行的任务。它们中的每一个都不应超时,但所有 10 个任务一起可能会超时。

对新的 http 请求使用模块化方法是一个好的解决方案吗?

类似这样的:

http://example.com/some/module/fetch
http://example.com/some/module/parse
http://example.com/some/module/save

也许这些网址每个都做一项任务。如果成功,则执行该任务的下一个任务。一种连锁反应。一条路径调用下一条路径(使用 curl)。

优点和缺点?这是一个好方法吗?如果没有,有什么更好的选择?

【问题讨论】:

  • 我觉得这很正常,你可以这样做,但是为什么你不在一个php上这样做呢?您想将它与 API 一起使用吗?还是你自己项目中的这个网址?
  • @TeymurMardaliyerLennon 我不想在超时的情况下同时运行所有东西。
  • 对于像在丰富的配置中运行工人这样的事情,我正在使用 RabbitMQ rabbitmq.com/tutorials/tutorial-two-php.html “工作队列(又名:任务队列)背后的主要思想是避免立即执行资源密集型任务而不得不等待它完成”

标签: php timeout httprequest


【解决方案1】:

此答案假定您正在使用 PHP 并通过向问题中的每个 URL 发出 HTTP 请求来运行任务。

您的解决方案取决于您的业务需求。如果您不关心 HTTP 请求的完成顺序,我建议您查看 curl_multi_init() 以开始了解 cURL PHP 扩展的 curl_multi_* 函数。

如果您确实关心完成顺序(例如,特定任务必须在下一个任务之前完成),请查看curl_init()

为消除您的调用脚本超时的可能性,请阅读set_time_limit 函数或考虑使用pcntl_fork 分叉您的进程。

或者,我会研究message queue。具体来说,查看Amazon's SQS 并阅读关于在 PHP 中与它交互的信息。以下是一些关于 SQS 和 PHP 的链接:

【讨论】:

    【解决方案2】:

    我认为“连锁反应”是这种方法可能过于复杂的线索......

    切换到强大的消息传递/工作队列系统(例如 RabbitMQ 或 SQS)可能有充分的理由,尤其是在您处理大量负载的情况下。消息队列在适当的上下文中是无价的,但如果不必要地使用它们会增加很多复杂性/开销/代码。

    最简单的解决方案

    ...但是如果您唯一关心的是防止超时,我不会让它变得比需要的更复杂;您可以使用以下方法轻松延长或完全禁用超时:

    set_time_limit(0); //no time limit, not recommended
    set_time_limit(300); //5 mins
    

    您提出的“链接”模式原则上是明智的,因为它可以让您准确地识别任何故障发生的位置,但是您可以在同一个请求/函数中完成所有这些操作,而不是依赖于网络。

    这不是在一个整洁的位置处理故障,而是需要两(或更多)层故障处理:一层处理单个请求,另一层发出请求。

    假设可以在单个请求中成功处理工作(甚至根本没有远程请求)然后,这不是“对新的 http 请求使用模块化方法的好解决方案“因为您通过进行不必要的 http 调用/响应来增加不必要的工作和复杂性。”即这会引入额外的故障可能性,特别是网络连接/延迟、DNS、测试和调试困难等。

    分离成单独的远程调用甚至可能会增加 10 倍的网络/服务器/身份验证延迟,并使执行数据库连接池等明智的事情变得更加棘手。

    其他简化问题的方法?

    如果可能,可能值得调查一下为什么这个请求链需要这么长时间 - 如果您可以优化它们以更快地运行,您可能能够避免在系统的这部分增加不必要的复杂性。例如诸如数据库延迟或不使用数据库连接池之类的事情可能会增加 10 个独立进程的严重开销。

    【讨论】:

    • 我将下载一个 2-20MB 大小的 CSV 文件,将其转换为数组,解析并将其插入数据库,例如 20 000 次插入。
    【解决方案3】:

    有工人的背景工作是最好的方式,因为:

    应用程序通常需要执行时间(或计算量)密集型操作,但在请求期间通常不希望这样做,因为由此产生的缓慢会直接被应用程序的用户感知。相反,任何耗时超过几十毫秒的任务,例如图像处理、电子邮件发送或任何类型的后台同步,都应该作为后台任务执行。此外,工作队列还可以轻松执行计划作业,因为时钟进程可以使用相同的队列基础结构。

    使用 php resque 实现后台作业: php resque workers

    【讨论】:

      【解决方案4】:

      模块化方法是个好主意(如果一个“单元”出现故障,工作会按您的意愿停止;而且调试/测试每个单独的单元更简单)。

      它会起作用,但你的链接方法有一些问题:

      • 如果存在瓶颈(即一个“单元”比其他“单元”花费的时间更长),那么您最终可能会导致 100 个瓶颈进程全部运行,并且您将失去对服务器资源的控制
      • 缺乏控制;假设需要重新启动服务器:要重新启动作业,那么您需要从头开始全部启动它们。
      • 同样,如果在运行时需要停止/启动/调试单个单元,则需要在第一个单元处重新启动作业以重复。
      • 通过发出 Web 请求,您正在使用 Apache/NGIX 资源、内存、套接字连接等来运行 PHP 进程。您可以直接运行 PHP 进程而无需使用开销。
      • 最后,如果在 DMZ 的 Web 服务器上,服务器实际上可能无法向自身发出请求。

      为了获得更多控制权,您应该使用排队系统进行此类操作。

      使用 PHP(或任何语言,真的),您的基本流程是:

      1. 每个“单元”都是一个不断循环的 php 脚本,永无止境*

      2. 每个“单元”进程监听一个排队系统;当一个作业到达它可以处理的队列时,它就会把它从队列中取出

      3. 当每个单元完成作业时,它确认已处理并推送到下一个队列。

      4. 如果单元决定作业不应继续,请确认已处理的作业,但不要推送到下一个队列。

      优点:

      • 如果“单元”停止,则作业将保留在队列中,并且可以在您重新启动“单元”时收集。使重新启动单元/服务器或一个单元崩溃时更容易。
      • 如果一个“单元”非常重,如果您有空间服务器容量,您可以启动第二个进程,执行完全相同的操作。如果没有服务器容量,你接受瓶颈;因此,您可以非常清楚地了解自己使用了多少资源。
      • 如果您决定另一种语言可以更好地处理请求,您可以混合使用 NodeJS、Python、Ruby 和...它们都可以与相同的队列对话。

      关于“持续循环 PHP”的旁注:这是通过将 max_execution_time 设置为“0”来完成的。确保您不会导致“内存泄漏”并拥有 cleanm 。您可以在启动时自动启动进程(systemd 或任务调度程序,具体取决于操作系统)或手动运行以进行测试。如果您不想让它连续循环,请在 5 分钟后超时并重新启动 cron/task 调度程序。

      关于队列的旁注:对于简单的应用程序,您可以使用内存缓存数据库“自己动手”(例如,使用数据库系统可以轻松处理队列中每小时 100,000 个项目),但避免冲突/管理状态/重试有点艺术。更好的选择是 RabbitMQ (https://www.rabbitmq.com/)。安装起来有点麻烦,但是一旦你安装了它,按照 PHP 教程进行操作,你将永远不会回头!

      【讨论】:

        【解决方案5】:

        假设你要使用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 \Amp\first($requestPromise, $timeout);
            if ($ret === null) {
                throw new \Exception("Timed out!");
            }
            return $ret;
        }
        
        \Amp\execute(function() {
            $end = new \Amp\Pause(100000); /* timeout in ms */
        
            $client = new \Amp\Artax\Client;
            $fetched = yield from doTaskWithTimeout($client->request("http://example.com/some/module/fetch"));
            $req = (new \Amp\Artax\Request)
                ->setUri("http://example.com/some/module/parsed")
                ->setMethod("POST")
                ->setBody($fetched)
            ;
            $parsed = yield from doTaskWithTimeout($client->request($req), $end);
            $req = (new \Amp\Artax\Request)
                ->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) 正确返回 Promises 并且设计为非阻塞):

        \Amp\execute(function() {
            $end = new \Amp\Pause(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 循环,彼此独立。

        【讨论】:

          猜你喜欢
          • 2017-01-29
          • 1970-01-01
          • 1970-01-01
          • 2020-08-31
          • 2018-10-12
          • 2012-09-29
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多