web-dev-qa-db-ja.com

長時間実行されるphpスクリプトを管理する最良の方法は?

PHPスクリプトの完了には長い時間(5〜30分)かかります。重要な場合に備えて、スクリプトはcurlを使用して別のサーバーからデータをスクレイピングします。これが理由です。処理に時間がかかり、各ページが読み込まれてから次のページに移動する必要があります。

スクリプトを開始し、完了するまで待機させたいと考えています。これにより、データベーステーブルにフラグが設定されます。

私が知る必要があるのは、スクリプトの実行が終了する前にhttpリクエストを終了する方法です。また、これを行うにはPHPスクリプトが最善の方法ですか?

74
kbanman

確かに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');ですが、その理由をここに含めるには少し長いです。

104
symcbean

すばやく汚い方法は、ignore_user_abort PHPの関数。これは基本的に、ユーザーが何をするか気にせず、終了するまでこのスクリプトを実行します。公開サイトの場合、これはやや危険です(20回開始されると、同時に20 ++バージョンのスクリプトが実行される可能性があるため)。

「クリーン」な方法(少なくともIMHO)は、プロセスを開始し、そのフラグが設定されているかどうかを確認するために1時間ごとに(またはそのように)cronjobを実行するときにフラグを設定します(たとえば、dbで)。 IS setの場合、長時間実行されるスクリプトが開始されます。設定されていない場合、nothinが発生します。

11
FlorianH

exec または system を使用してバックグラウンドジョブを開始し、その中で作業を行うことができます。

また、使用しているWebをスクレイピングするためのより良いアプローチがあります。スレッドアプローチ(一度に1ページを実行する複数のスレッド)またはイベントループ(一度に複数ページを実行する1つのスレッド)を使用できます。 Perlを使用する私の個人的なアプローチは、 AnyEvent :: HTTP を使用することです。

ETA: symcbean バックグラウンドプロセスを適切にデタッチする方法を説明しました here

8
Leon Timmermans

いいえ、PHPは最善の解決策ではありません。

RubyまたはPerlについてはわかりませんが、Pythonを使用すると、ページスクレーパーをマルチスレッドに書き換えることができ、おそらく少なくとも20倍高速に実行できます。マルチスレッドのアプリを書くのはやや難しいかもしれませんが、最初に書いたPythonアプリは、マルチスレッドのページスクレーパーでした。また、シェル実行関数の1つを使用して、Pythonページ内からPHPスクリプトを呼び出すこともできます。

5
jamieb

はい、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
5
aljo f

PHPは最良のツールかもしれないし、そうでないかもしれないが、あなたはそれを使う方法を知っていて、あなたのアプリケーションの残りはそれを使って書かれている。これらの2つの性質と、PHPが「十分」であるという事実とを組み合わせることで、Perl、Ruby、またはPythonの代わりに、それを使用する非常に強力なケースが作成されます。

あなたの目標が別の言語を学ぶことであるなら、それを選んでそれを使ってください。あなたが言及したどの言語でも問題なく機能します。私はたまたまPerlが好きですが、あなたが好きなものは違うかもしれません。

Symcbeanには、リンクでバックグラウンドプロセスを管理する方法についての良いアドバイスがあります。

要するに、長いビットを処理するCLI PHPスクリプトを記述してください。何らかの方法でステータスを報告することを確認してください。AJAXまたは従来の方法:キックオフスクリプトは、独自のセッションで実行中のプロセスを開始し、プロセスが進行中であることの確認を返します。

幸運を。

3
daotoad

XHR(Ajax)リクエストとして送信できます。通常のクライアントは、通常のHTTPリクエストとは異なり、XHRのタイムアウトを持ちません。

1
JAL

これは非常に古い質問ですが、試してみたいと思います。このスクリプトは、最初のキックオフコールに対処して、迅速に終了し、重い負荷を小さなチャンクに切り詰めようとします。このソリューションはテストしていません。

<?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.';
}
1
Francisco Luz

これはバックグラウンドプロセスで実行する必要があるという回答に同意します。ただし、ユーザーが作業が完了したことを把握できるように、ステータスを報告することも重要です。

プロセスを開始するPHPリクエストを受信すると、一意の識別子を持つタスクの表現をデータベースに保存することができます。タスクが開始されたこと、および新しいタスクIDを含む指定されたURLをチェックして最新のステータスを取得することをiPhoneアプリに報告します。一方で、バックグラウンドプロセスは、完了率、現在のステップ、またはその他の任意のステータスインジケーターで動作するときにタスクのデータベース表現を更新します。そして、完了すると、完了フラグを設定します。

1
Jacob

主に、Apache/www-dataユーザーとしてではなく、別のユーザーとして長時間実行プロセスを実行する必要があるという追加要件があるため、symcbeanとは少し異なるソリューションを提案したいと思います。

Cronを使用してバックグラウンドタスクテーブルをポーリングする最初のソリューション:

  • PHP Webページがバックグラウンドタスクテーブルに挿入され、状態が「SUBMITTED」
  • cronは3分ごとに1回実行され、別のユーザーを使用して、PHP 'SUBMITTED'行のバックグラウンドタスクテーブルをチェックするCLIスクリプトを実行します
  • PHP CLIは、行の状態列を「PROCESSING」に更新し、処理を開始します。完了後、「COMPLETED」に更新されます。

Linux inotify機能を使用した2番目のソリューション:

  • PHP Webページは、ユーザーが設定したパラメーターで制御ファイルを更新し、タスクIDも提供します
  • Inotifywaitを実行するシェルスクリプト(非wwwユーザーとして)は、制御ファイルが書き込まれるのを待機します
  • 制御ファイルが書き込まれた後、close_writeイベントが発生し、シェルスクリプトが続行されます
  • シェルスクリプトはPHP CLIを実行して長時間実行プロセスを実行します
  • PHP CLIは、出力をタスクIDで識別されるログファイルに書き込むか、ステータステーブルの進行状況を更新します
  • PHP Webページは、ログファイルを(タスクIDに基づいて)ポーリングして、長時間実行されているプロセスの進行状況を表示するか、ステータステーブルを照会することもできます。

いくつかの追加情報は私の投稿で見つけることができます: http://inventorsparadox.blogspot.co.id/2016/01/long-running-process-in-linux-using-php.html

1
YudhiWidyatama

Perl、double fork()、および親プロセスからの切り離しで同様のことをしました。すべてのHTTPフェッチ作業は、分岐プロセスで実行する必要があります。

0

長いスクリプトがある場合、各タスクの入力パラメーターの助けを借りてページ作業を分割します(各ページはスレッドのように動作します)。つまり、ページに1つのlac product_keywords長いプロセスループがある場合、ループの代わりに1つのキーワードのロジックを作成し、このキーワードを渡しますmagicまたはcornjobpage.phpから(次の例)

バックグラウンドワーカーの場合は、この手法を試してみると、各ページの応答が非同期になるのを待たずに、すべてのページが独立して実行されるページをできるだけ多く呼び出すことができます。

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

0
Hassan Saeed

私が常に使用しているのは、これらのバリエーションの1つです(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を使用して出力をリダイレクトしました。

0
dr burns

多くの人がここで述べたように、最良のアプローチではありませんが、これは役立つかもしれません:

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

プロキシを使用してリクエストを委任します。

0
zerodin