管理长时间运行的php脚本的最佳方法?


81

我有一个需要很长时间(5-30分钟)才能完成的PHP脚本。以防万一,脚本正在使用curl从另一台服务器上刮取数据。这就是为什么要花这么长时间的原因。它必须等待每个页面加载,然后再处理并移至下一页。

我希望能够启动脚本并放任其完成,这将在数据库表中设置一个标志。

我需要知道的是如何能够在脚本完成运行之前结束http请求。此外,php脚本是做到这一点的最佳方法吗?


1
尽管您没有在服务器支持的语言中提及它,但我会猜测您是否具有运行Ruby和Perl的能力,您可能可以添加Node.js,这对我来说听起来像是Javascript的完美用例:您的脚本将花费大部分时间等待请求完成,这是异步范式所体现出来的一个领域。没有线程意味着易于同步,并发意味着需要。
djfm 2015年

您可以使用PHP执行此操作。我将使用GoutteGuzzle实现并发线程。您也可以查看Gearman以并行形式启动并行请求。
安德烈·加西亚

Answers:


114

当然可以使用PHP来完成,但是您不应将此作为后台任务执行-必须将新进程从启动它的进程组中分离出来。

由于人们总是对此FAQ给出相同的错误答案,因此我在这里写了一个更完整的答案:

http://symcbean.blogspot.com/2010/02/php-and-long-running-processes.html

从评论:

简短的版本shell_exec('echo /usr/bin/php -q longThing.php | at now');只是其中包含一些内容的原因。


这篇博客文章是真正的答案。PHP的exec和系统具有太多潜在的陷阱。
incredimike

2
是否有可能将相关详细信息复制到答案中?有太多的旧答案链接到死博客。那个博客还没有死(还),但是将会是一天。
Murphy 2015年

5
简短的版本shell_exec('echo /usr/bin/php -q longThing.php | at now');只是其中包含一些内容的原因。
symcbean 2015年

1
高投票率问题的高票率答案,但是答案所包含的内容不只是指向博客文章的链接。请根据meta.stackexchange.com/questions/8231/…和/或帮助中心添加真实的答案
Nanne

1
我可以知道这个-q选项在做什么吗?
Kiren Siva

11

快速而肮脏的方法是ignore_user_abort在php中使用该函数。基本上是这样说的:不管用户做什么,运行脚本直到完成。如果它是一个面向公众的站点,这将有些危险(因为如果启动20次,最终有可能同时运行20 ++版本的脚本)。

“干净”的方式(至少是IMHO)是在您要启动进程时设置一个标志(例如,在数据库中),并每小时(或大约一个小时)运行一次cronjob来检查该标志是否已设置。如果已设置,则将运行长时间运行的脚本;如果未设置,则不会发生任何情况。


因此,“ ignore_user_abort”方法将允许用户关闭浏览器窗口,但是我可以做些什么让它在完成运行之前将HTTP响应返回给客户端?
kbanman 2010年

1
@kbanman是的。您需要关闭连接:header("Connection: close", true);。而且不要忘了冲洗()
Benubird

8

您可以使用execsystem来启动后台作业,然后在其中进行工作。

另外,还有更好的方法来抓取您正在使用的Web。您可以使用线程方法(多个线程一次执行一页),也可以使用事件循环(一个线程一次执行多个页面)。我个人使用Perl的方法是使用AnyEvent :: HTTP

ETA:symcbean解释了如何正确地分离后台进程在这里


5
几乎是正确的。仅仅使用exec或system就会再次吸引您。有关详细信息,请参见我的回复。
symcbean'2

5

不,PHP不是最佳解决方案。

我不确定Ruby或Perl,但使用Python可以将页面抓取器重写为多线程,并且它的运行速度至少快20倍。编写多线程应用程序可能有些困难,但是我编写的第一个Python应用程序是多线程页面刮板。而且,您可以使用外壳程序执行功能之一在PHP页面中简单地调用Python脚本。


刮削的实际处理部分非常有效。正如我上面提到的,杀死每个页面的原因是每个页面的加载。我想知道的是,PHP是否可以运行这么长时间。
kbanman 2010年

我有点有偏见,因为自从学习Python以来,我就完全不喜欢PHP。但是,如果您要抓取多个页面(连续),则几乎可以肯定,通过与多线程应用程序并行执行可以获得更好的性能。
jamieb 2010年

1
您是否有机会向我发送此类页面抓取器的示例?因为我还没有接触过Python,这将帮助我获得更多的体验。
kbanman 2010年

如果必须重写它,我将仅使用eventlet。这使我的代码简化了10倍:eventlet.net/doc
jamieb 2010年

5

是的,您可以在PHP中完成。但是除了PHP,使用队列管理器是明智的。这是策略:

  1. 将大型任务分解为较小的任务。在您的情况下,每个任务可能只加载一个页面。

  2. 将每个小任务发送到队列。

  3. 在队列中运行队列工作器。

使用此策略具有以下优点:

  1. 对于长时间运行的任务,如果在运行过​​程中出现致命问题,它具有恢复的能力-无需从头开始。

  2. 如果不必按顺序运行任务,则可以运行多个工作程序来同时运行任务。

您有多种选择(仅几个):

  1. RabbitMQ(https://www.rabbitmq.com/tutorials/tutorial-one-php.html
  2. ZeroMQ(http://zeromq.org/bindings:php
  3. 如果您使用的是Laravel框架,则队列是内置的(https://laravel.com/docs/5.4/queues),带有适用于AWS SES,Redis和Beanstalkd的驱动程序

3

PHP可能不是最好的工具,但您知道如何使用它,其余的应用程序都是使用PHP编写的。这两种特性,再加上PHP“足够好”的事实,为使用它而不是Perl,Ruby或Python奠定了坚实的基础。

如果您的目标是学习另一种语言,则选择一种语言并使用它。您提到的任何语言都可以胜任,没问题。我碰巧喜欢Perl,但是您喜欢的东西可能有所不同。

Symcbean在他的链接上对如何管理后台进程提供了一些很好的建议。

简而言之,编写一个CLI PHP脚本来处理较长的位。确保它以某种方式报告状态。使用AJAX或传统方法制作一个php页面来处理状态更新。您的启动脚本将启动进程在其自己的会话中运行,并返回确认进程正在运行。

祝好运。


1

我同意回答应该在后台进程中运行的答案。但是,报告状态也很重要,这样用户才能知道工作已经完成。

当收到启动该过程的PHP请求时,您可以将具有唯一标识符的任务表示形式存储在数据库中。然后,开始屏幕抓取过程,并向其传递唯一标识符。向iPhone应用程序报告该任务已启动,并且应检查包含新任务ID的指定URL以获得最新状态。iPhone应用程序现在可以轮询(甚至“长时间轮询”)此URL。同时,后台进程将在完成任务时使用完成百分比,当前步骤或您想要的任何其他状态指示器来更新任务的数据库表示。当完成时,它将设置完成标志。


1

您可以将其作为XHR(Ajax)请求发送。与普通的HTTP请求不同,客户端通常不会对XHR超时。


1

我意识到这是一个非常老的问题,但想尝试一下。该脚本尝试解决最初的启动呼叫以快速完成并将重负载切成较小的块的情况。我尚未测试此解决方案。

<?php
/**
 * crawler.php located at http://mysite.com/crawler.php
 */

// Make sure this script will keep on runing after we close the connection with
// it.
ignore_user_abort(TRUE);


function get_remote_sources_to_crawl() {
  // Do a database or a log file query here.

  $query_result = array (
    1 => 'http://exemple.com',
    2 => 'http://exemple1.com',
    3 => 'http://exemple2.com',
    4 => 'http://exemple3.com',
    // ... and so on.
  );

  // Returns the first one on the list.
  foreach ($query_result as $id => $url) {
    return $url;
  }
  return FALSE;
}

function update_remote_sources_to_crawl($id) {
  // Update my database or log file list so the $id record wont show up
  // on my next call to get_remote_sources_to_crawl()
}

$crawling_source = get_remote_sources_to_crawl();

if ($crawling_source) {


  // Run your scraping code on $crawling_source here.


  if ($your_scraping_has_finished) {
    // Update you database or log file.
    update_remote_sources_to_crawl($id);

    $ctx = stream_context_create(array(
      'http' => array(
        // I am not quite sure but I reckon the timeout set here actually
        // starts rolling after the connection to the remote server is made
        // limiting only how long the downloading of the remote content should take.
        // So as we are only interested to trigger this script again, 5 seconds 
        // should be plenty of time.
        'timeout' => 5,
      )
    ));

    // Open a new connection to this script and close it after 5 seconds in.
    file_get_contents('http://' . $_SERVER['HTTP_HOST'] . '/crawler.php', FALSE, $ctx);

    print 'The cronjob kick off has been initiated.';
  }
}
else {
  print 'Yay! The whole thing is done.';
}

@symcbean我阅读了您建议的帖子,并希望听听您对这种替代解决方案的想法。
弗朗西斯科·卢兹

首先,您为我的第一个机器人(teehee)提供了一个入门思路。其次,您如何找到解决方案的性能?您是否已与之进一步合作并学到了更多东西?我有兴趣通过26,000张图像(1,3GB)实现类似于挖泥的操作,执行各种操作等。这将需要一段时间。您的解决方案似乎不是唯一看起来不太可靠,使用exec()颤抖或需要Linux的解决方案(我们中有些失败者仍然必须使用Windows)。我更喜欢从抨击中学习,而不是我自己的:P
Just Plain High

@HighPriestessofTheTech嗨,队友,我再也没有做。在我写这篇文章的时候,我只是在进行思想实验。
Francisco Luz 2013年

1
哦,亲爱的...所以我将从自己的抨击中学习...我会让你知道它的进展;)
Just Plain High

1
我确实尝试过,但是我发现它非常有用。
Alex

1

我想提出一种与symcbean的解决方案略有不同的解决方案,主要是因为我还有其他要求,即长时间运行的进程需要以其他用户而不是apache / www-data用户的身份运行。

使用cron轮询后台任务表的第一个解决方案:

  • PHP Web页面插入到后台任务表中,状态为“ SUBMITTED”
  • cron使用另一个用户每3分钟运行一次PHP脚本,该脚本将检查后台任务表中是否有“ SUBMITTED”行
  • PHP CLI会将行中的状态列更新为“处理中”并开始处理,完成后它将更新为“已完成”

使用Linux inotify工具的第二个解决方案:

  • PHP网页使用用户设置的参数更新控制文件,并提供任务ID
  • 运行inotifywait的Shell脚本(作为非www用户)将等待写入控制文件
  • 写入控制文件后,将引发close_write事件,shell脚本将继续
  • shell脚本执行PHP CLI以执行长期运行的过程
  • PHP CLI将输出写入由任务ID标识的日志文件,或者更新状态表中的进度
  • PHP Web页面可以轮询日志文件(基于任务ID)以显示长时间运行的进程的进度,也可以查询状态表

一些其他信息可以在我的帖子中找到:http : //inventorsparadox.blogspot.co.id/2016/01/long-running-process-in-linux-using-php.html




0

我总是使用的是这些变体之一(因为Linux的不同版本在处理输出/某些程序输出方面有不同的规则):

变体I @exec('./ myscript.php \ 1> / dev / null \ 2> / dev / null&');

变体II @exec('php -f myscript.php \ 1> / dev / null \ 2> / dev / null&');

变体III @exec('nohup myscript.php \ 1> / dev / null \ 2> / dev / null&');

您可能尚未安装“ nohup”。但是例如,当我使FFMPEG视频会话自动化时,通过某种方式重定向输出流1和2并不能100%处理输出接口,因此我使用了nohup AND重定向了输出。


0

如果您有较长的脚本,则在每个任务的输入参数的帮助下划分页面工作。(然后,每个页面的行为都类似于线程),即,如果页面具有1个lac product_keywords较长的过程循环,则代替循环为一个关键字生成逻辑并传递此关键字来自magic或cornjobpage.php(在以下示例中)

对于后台工作人员,我认为您应该尝试使用此技术,它将有助于您一次调用尽可能多的页面,所有页面将立即独立运行,而不必等待每个页面的响应都是异步的。

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

正如这里许多人所说,这不是最好的方法,但这可能会有所帮助:

ignore_user_abort(1); // run script in background even if user closes browser
set_time_limit(1800); // run it for 30 minutes

// Long running script here

0

如果脚本的期望输出是某种处理,而不是网页,那么我认为期望的解决方案是从shell运行脚本,就像

php my_script.php

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.