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


80

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

スクリプトを開始して、完了するまでそのままにしておくと、データベーステーブルにフラグが設定されます。

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


1
サーバーでサポートされている言語では言及していませんが、RubyとPerlを実行できるかどうか、おそらくNode.jsを追加できると思います。これは、Javascriptの完璧なユースケースのように思えます。 :スクリプトは、ほとんどの時間をリクエストの完了を待つことに費やします。これは、非同期パラダイムが優れている領域です。スレッドがないということは同期が簡単であることを意味し、並行性は高速であることを意味します。
djfm 2015年

これはPHPで行うことができます。並行性スレッドを使用GoutteGuzzleて実装します。またGearman、ワーカーの形式で並列リクエストを起動することもできます。
アンドレガルシア

回答:


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 2013年

2
関連する詳細を回答にコピーする可能性はありますか?死んだブログにリンクする古い答えが多すぎます。そのブログは(まだ)死んでいませんが、いつかはそうなるでしょう。
マーフィー

5
短いバージョンですshell_exec('echo /usr/bin/php -q longThing.php | at now');が、ここに含めるのが少し長い理由です。
symcbean 2015年

1
投票数の多い質問に対する投票数の多い回答ですが、回答にはブログ投稿へのリンク以上のものは含まれていません。meta.stackexchange.com/questions/8231/…および/またはヘルプセンターに従って、実際の回答を追加してください
Nanne

1
この-qオプションが何をしているのかわかりますか?
キレンシバ

11

手っ取り早い方法はignore_user_abort、phpで関数を使用することです。これは基本的に次のように述べています。ユーザーが何をするかは気にせず、完了するまでこのスクリプトを実行します。これは、公開サイトの場合はやや危険です(20回開始すると、スクリプトの20 ++バージョンが同時に実行される可能性があるため)。

「クリーンな」方法(少なくともIMHO)は、プロセスを開始し、1時間ごと(またはそれ以上)にcronジョブを実行して、そのフラグが設定されているかどうかを確認するときに、フラグ(たとえば、db)を設定することです。設定されている場合は長時間実行スクリプトが開始され、設定されていない場合は何も起こりません。


したがって、「ignore_user_abort」メソッドを使用すると、ユーザーはブラウザウィンドウを閉じることができますが、実行が終了する前にクライアントにHTTP応答を返すようにするためにできることはありますか?
kbanman 2010

1
@kbanmanうん。接続を閉じる必要があります:header("Connection: close", true);。そして、flush()を忘れないでください
Benubird 2013

8

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

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

ETA:symcbeanは、ここでバックグラウンドプロセスを適切に切り離す方法を説明しまし


5
ほぼ正しい。execまたはsystemを使用するだけで、お尻に噛み付くようになります。詳細については私の返信を参照してください。
symcbean 2010

5

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

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


私のスクレイピングの実際の処理部分は非常に効率的です。上で述べたように、私を殺すのは各ページの読み込みです。私が疑問に思っていたのは、PHPがこれほど長期間実行されることを意図しているかどうかです。
kbanman 2010

Pythonを学んで以来、私はPHPを完全に嫌っているので、少し偏見があります。ただし、複数のページを(連続して)スクレイピングしている場合は、マルチスレッドアプリと並行して実行することで、パフォーマンスが向上することはほぼ確実です。
jamieb 2010

1
そのようなページスクレイパーの例を送っていただけませんか。私はまだPythonに触れていないので、それは私がたくさん見るのを助けるでしょう。
kbanman 2010

書き直さなければならない場合は、eventletを使用します。これにより、コードが約10倍簡単になります:eventlet.net/doc
jamieb

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を使用して作成されています。これらの2つの品質は、PHPが「十分に優れている」という事実と相まって、Perl、Ruby、またはPythonの代わりにPHPを使用するための非常に強力なケースになります。

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

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

つまり、長いビットを処理するCLIPHPスクリプトを記述します。何らかの方法でステータスを報告していることを確認してください。AJAXまたは従来の方法を使用して、ステータスの更新を処理するphpページを作成します。キックオフスクリプトは、独自のセッションで実行されているプロセスを開始し、プロセスが進行中であることの確認を返します。

幸運を。


1

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

プロセスを開始するためのPHPリクエストを受信すると、一意の識別子を持つタスクの表現をデータベースに保存できます。次に、画面スクレイピングプロセスを開始し、一意の識別子を渡します。タスクが開始されたこと、および新しいタスクIDを含む指定されたURLをチェックして最新のステータスを取得する必要があることを、iPhoneアプリに報告します。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私はあなたが提案した投稿を読み、この代替ソリューションについてのあなたの考えを聞きたいと思います。
フランシスコルス2013年

まず、最初のボット(teehee)の最初のアイデアを教えてくれました。次に、ソリューションのパフォーマンスをどのように見つけましたか?あなたはそれをさらに使って、もっと何かを学びましたか?26,000枚(1.3GB)の浚渫に似たものを実装したり、いろいろな操作をしたりすることに興味があります。しばらく時間がかかります。あなたのものは、ハッキーに見えない、exec()シャダーを使用する、またはLinuxを必要とする唯一のソリューションです(私たちの敗者の中には、まだWindowsを使用しなければならない人もいます)。私自身ではなく、あなたのヘッドバッシングから学ぶことを好みます:P
Just Plain High

@HighPriestessofTheTechこんにちは、私はこれ以上先に進みません。これを書いたとき、私はただ思考実験をしていました。
Francisco Luz

1
ああ...だから私は自分のヘッドバッシングから学ぶでしょう...それがどうなるかをあなたに知らせます;)
Just Plain High

1
私はこれを試しましたが、非常に便利だと思います。
アレックス

1

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

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

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

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

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

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


0

私はPerl、double fork()、および親プロセスからのデタッチで同様のことを行いました。すべてのhttpフェッチ作業は、フォークされたプロセスで実行する必要があります。



0

私が常に使用しているのは、これらのバリアントの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

長いスクリプトがある場合は、各タスクの入力パラメーターを使用してページ作業を分割します(各ページはスレッドのように機能します)。つまり、ページに1つのlac product_keywordsの長いプロセスループがある場合は、ループの代わりに1つのキーワードのロジックを作成してこのキーワードを渡します。 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

スクリプトの目的の出力がWebページではなく何らかの処理である場合、目的の解決策は、単に次のように、シェルからスクリプトを実行することであると思います。

php my_script.php

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.