PHPタスクを非同期で実行する


144

私はやや大規模なWebアプリケーションで作業しており、バックエンドのほとんどはPHPです。コードのいくつかのタスクを完了する必要がある場所がいくつかありますが、ユーザーに結果を待たせたくありません。たとえば、新しいアカウントを作成するときに、ウェルカムメールを送信する必要があります。しかし、「登録の完了」ボタンを押したときに、実際にメールが送信されるまで待たせたくありません。プロセスを開始して、すぐにユーザーにメッセージを返したいだけです。

今までのところ、私はexec()を使ってハックのように感じるものを使用しています。基本的に次のようなことをしています:

exec("doTask.php $arg1 $arg2 $arg3 >/dev/null 2>&1 &");

うまくいくようですが、もっと良い方法があるかどうか疑問に思っています。MySQLテーブルのタスクをキューに入れるシステムと、そのテーブルに1秒に1回クエリを実行し、見つかった新しいタスクを実行する別の長時間実行PHPスクリプトを書くことを検討しています。これには、将来、必要に応じてタスクを複数のワーカーマシンに分割できるという利点もあります。

車輪を再発明していますか?exec()ハックやMySQLキューより良い解決策はありますか?

回答:


80

私はキューイングアプローチを使用しましたが、サーバーの負荷がアイドルになるまでその処理を延期できるのでうまく機能します。「緊急でないタスク」を簡単に分割できる場合は、負荷を非常に効果的に管理できます。

自分でロールするのは難しいことではありません。チェックアウトする他のいくつかのオプションを次に示します。

  • GearMan-この回答は2009年に書かれており、それ以来GearManは人気のあるオプションに見えます。以下のコメントを参照してください。
  • 完全なオープンソースメッセージキューが必要な場合は、ActiveMQ
  • ZeroMQ-これは非常にクールなソケットライブラリで、ソケットプログラミング自体をあまり気にすることなく、分散コードを簡単に作成できます。単一のホストでのメッセージキューイングに使用できます。継続的に実行しているコンソールアプリが次の適切な機会に消費するキューに何かをプッシュするだけです。
  • beanstalkd-この答えを書いているときにこれだけを見つけましたが、興味深いようです
  • droprはPHPベースのメッセージキュープロジェクトですが、2010年9月以降積極的にメンテナンスされていません
  • php-enqueueは、最近(2017年)に保守された、さまざまなキューシステムのラッパーです。
  • 最後に、メッセージのキューイングにmemcachedを使用することに関するブログ投稿

別の、おそらくより単純なアプローチは、ignore_user_abortを使用することです。ページをユーザーに送信すると、ユーザーからのページのロードが長くなるように見える効果がありますが、途中で終了することを恐れずに最終処理を実行できます。視点。


すべてのヒントをありがとう。ignore_user_abortに関する具体的な問題は、私の場合は実際には役に立ちません。私の全体的な目標は、ユーザーの不要な遅延を回避することです。
davr

2
「登録していただきありがとうございます」応答でContent-Length HTTPヘッダーを設定した場合、ブラウザは指定されたバイト数を受信した後に接続を閉じる必要があります。これにより、エンドユーザーを待たせることなく、サーバー側プロセスを実行したままにします(ignore_user_abortが設定されていると想定)。もちろん、ヘッダーをレンダリングする前に応答コンテンツのサイズを計算する必要がありますが、短い応答の場合は簡単です。
Peter

1
Gearman(gearman.org)は、クロスプラットフォームの優れたオープンソースメッセージキューです。ワーカーはC、PHP、Perl、または他のほぼすべての言語で記述できます。MySQLにはGearman UDFプラグインがあり、PHPまたはgearman pearクライアントからNet_Gearmanを使用することもできます。
ジャスティン・スワンハート

Gearmanは、カスタムワークキューイングシステムよりも今日(2015年に)推奨するものです。
ピーター

別のオプションは、ノードjsサーバーを設定してリクエストを処理し、その間にタスクを挟んで高速応答を返すことです。ノードのjsスクリプト内の多くのものは、httpリクエストなど、非同期で実行されます。
Zordon

22

応答を待たずに1つまたは複数のHTTPリクエストを実行したいだけの場合も、単純なPHPソリューションがあります。

呼び出しスクリプトで:

$socketcon = fsockopen($host, 80, $errno, $errstr, 10);
if($socketcon) {   
   $socketdata = "GET $remote_house/script.php?parameters=... HTTP 1.1\r\nHost: $host\r\nConnection: Close\r\n\r\n";      
   fwrite($socketcon, $socketdata); 
   fclose($socketcon);
}
// repeat this with different parameters as often as you like

呼び出されたscript.phpで、最初の行でこれらのPHP関数を呼び出すことができます:

ignore_user_abort(true);
set_time_limit(0);

これにより、HTTP接続が閉じられたときにスクリプトが時間制限なしで実行を継続します。


phpがセーフモードで実行されている場合、set_time_limitは効果がありません
Baptiste Pernet

17

プロセスをフォークする別の方法は、curlを使用することです。内部タスクをWebサービスとして設定できます。例えば:

次に、ユーザーがアクセスしたスクリプトでサービスを呼び出します。

$service->addTask('t1', $data); // post data to URL via curl

サービスはmysqlを使用してタスクのキューを追跡することができます。つまり、好きなように、サービス内にすべて含まれ、スクリプトはURLを消費するだけです。これにより、必要に応じてサービスを別のマシン/サーバーに移動できます(つまり、簡単にスケーラブルです)。

http認証またはカスタム認証スキーム(AmazonのWebサービスなど)を追加すると、タスクを開いて他の人/サービス(必要な場合)が使用できるようになり、さらにそれを追跡して追跡サービスを追跡することができますキューとタスクのステータス。

設定には少し時間がかかりますが、多くのメリットがあります。


1
Webサーバーに負荷がかかるため、私はこのアプローチが好きではありません
Oved Yavine

7

私は1つのプロジェクトでBeanstalkdを使用しており、今後も予定しています。非同期プロセスを実行するための優れた方法であることがわかりました。

私がそれでやったことのいくつかは:

  • 画像のサイズ変更-軽くロードされたキューがCLIベースのPHPスクリプトに渡されるため、大きな(2mb +)画像のサイズ変更は問題なく機能しましたが、mod_phpインスタンス内で同じ画像のサイズを変更しようとすると、メモリ領域の問題が定期的に発生しました(I PHPプロセスを32MBに制限し、サイズ変更にはそれ以上かかりました)
  • 近未来のチェック-Beanstalkdで利用可能な遅延があります(このジョブをX秒後にのみ実行できるようにします)-少し遅れてイベントの5または10のチェックを開始できます

私はZend-Frameworkベースのシステムを作成して、「素敵な」URLをデコードし、たとえば、呼び出す画像のサイズを変更しましたQueueTask('/image/resize/filename/example.jpg')。URLは最初に配列(モジュール、コントローラー、アクション、パラメーター)にデコードされ、次にキュー自体に挿入するためにJSONに変換されました。

次に、長時間実行されるcliスクリプトがキューからジョブを取得し、(Zend_Router_Simpleを介して)ジョブを実行し、必要に応じて、WebサイトPHPが実行時に必要に応じて取得できるように、情報をmemcachedに入れます。

私が加えたしわの1つは、cliスクリプトが再起動する前に50ループしか実行しなかったということですが、計画どおりに再起動したい場合は、すぐに再起動します(bashスクリプトを介して実行されます)。問題が発生して私が問題をexit(0)起こした場合(exit;またはのデフォルト値die();)、最初に数秒間一時停止します。


私は豆の木の見た目が好きです。持続性が加われば完璧だと思います。
davr 2009年

それはすでにコードベースにあり、安定しています。私は「名前付きジョブ」も楽しみです。そこに物を投げることができますが、すでにそこにある場合は追加されないことを知っています。定期的なイベントに最適です。
アリスターブルマン

@AlisterBulmanは、「長時間実行されているcliスクリプトがキューからジョブを取得する」ための詳細情報または例を提供できます。私は自分のアプリケーション用にそのようなcliスクリプトを作成しようとしています。
Sasi varna kumar 2016年

7

高価なタスクを提供するだけの問題で、php-fpmがサポートされている場合、fastcgi_finish_request()関数を使用しないのはなぜですか?

この関数は、すべての応答データをクライアントにフラッシュし、要求を完了します。これにより、クライアントへの接続を開いたままにせずに、時間のかかるタスクを実行できます。

このように非同期性を実際に使用することはありません。

  1. 最初にすべてのメインコードを作成します。
  2. を実行しfastcgi_finish_request()ます。
  3. すべての重いものを作ります。

もう一度php-fpmが必要です。


5

これが、Webアプリケーション用にコーディングした簡単なクラスです。PHPスクリプトやその他のスクリプトをフォークすることができます。UNIXおよびWindowsで動作します。

class BackgroundProcess {
    static function open($exec, $cwd = null) {
        if (!is_string($cwd)) {
            $cwd = @getcwd();
        }

        @chdir($cwd);

        if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN') {
            $WshShell = new COM("WScript.Shell");
            $WshShell->CurrentDirectory = str_replace('/', '\\', $cwd);
            $WshShell->Run($exec, 0, false);
        } else {
            exec($exec . " > /dev/null 2>&1 &");
        }
    }

    static function fork($phpScript, $phpExec = null) {
        $cwd = dirname($phpScript);

        @putenv("PHP_FORCECLI=true");

        if (!is_string($phpExec) || !file_exists($phpExec)) {
            if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN') {
                $phpExec = str_replace('/', '\\', dirname(ini_get('extension_dir'))) . '\php.exe';

                if (@file_exists($phpExec)) {
                    BackgroundProcess::open(escapeshellarg($phpExec) . " " . escapeshellarg($phpScript), $cwd);
                }
            } else {
                $phpExec = exec("which php-cli");

                if ($phpExec[0] != '/') {
                    $phpExec = exec("which php");
                }

                if ($phpExec[0] == '/') {
                    BackgroundProcess::open(escapeshellarg($phpExec) . " " . escapeshellarg($phpScript), $cwd);
                }
            }
        } else {
            if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN') {
                $phpExec = str_replace('/', '\\', $phpExec);
            }

            BackgroundProcess::open(escapeshellarg($phpExec) . " " . escapeshellarg($phpScript), $cwd);
        }
    }
}

4

これは、私がここ数年使用しているのと同じ方法であり、これ以上何も見たり見つけたりしていません。人々が言っ​​たように、PHPはシングルスレッドなので、他にできることはあまりありません。

これに実際にレベルを1つ追加しました。これにより、プロセスIDが取得および保存されます。これにより、別のページにリダイレクトし、ユーザーにそのページに座ってもらい、AJAXを使用してプロセスが完了したかどうかを確認できます(プロセスIDはもう存在しません)。これは、スクリプトの長さが原因でブラウザがタイムアウトになる場合に便利ですが、ユーザーはそのスクリプトが完了するのを待ってから次のステップに進む必要があります。(私の場合、CSVのような大きなZIPファイルを処理しており、最大30 000レコードがデータベースに追加された後、ユーザーがいくつかの情報を確認する必要があります。)

レポート作成にも同様のプロセスを使用しました。遅いSMTPで実際に問題がない限り、電子メールなどの「バックグラウンド処理」を使用するかどうかはわかりません。代わりに、テーブルをキューとして使用し、毎分実行されるプロセスを使用して、キュー内で電子メールを送信します。電子メールを2回送信するなどの同様の問題に注意する必要があります。他のタスクについても同様のキューイングプロセスを検討します。


1
最初の文でどの方法を参照していますか?
Simon East


2

rojocaによって提案されたcURLを使用することは素晴らしいアイデアです。

例を示します。スクリプトがバックグラウンドで実行されている間、text.txtを監視できます。

<?php

function doCurl($begin)
{
    echo "Do curl<br />\n";
    $url = 'http://'.$_SERVER['SERVER_NAME'].$_SERVER['REQUEST_URI'];
    $url = preg_replace('/\?.*/', '', $url);
    $url .= '?begin='.$begin;
    echo 'URL: '.$url.'<br>';
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    $result = curl_exec($ch);
    echo 'Result: '.$result.'<br>';
    curl_close($ch);
}


if (empty($_GET['begin'])) {
    doCurl(1);
}
else {
    while (ob_get_level())
        ob_end_clean();
    header('Connection: close');
    ignore_user_abort();
    ob_start();
    echo 'Connection Closed';
    $size = ob_get_length();
    header("Content-Length: $size");
    ob_end_flush();
    flush();

    $begin = $_GET['begin'];
    $fp = fopen("text.txt", "w");
    fprintf($fp, "begin: %d\n", $begin);
    for ($i = 0; $i < 15; $i++) {
        sleep(1);
        fprintf($fp, "i: %d\n", $i);
    }
    fclose($fp);
    if ($begin < 10)
        doCurl($begin + 1);
}

?>

2
ソースコードがコメントされると本当に助かります。私はそこで何が起こっているのか、どの部分が例であり、どの部分が自分の目的で再利用できるのかわかりません。
Thomas Tempelmann、2015年

1

残念ながら、PHPにはネイティブスレッド機能はありません。だから私はこの場合あなたがやりたいことをするためにある種のカスタムコードを使うしかないだろうと思います。

PHPのスレッディングに関するものをネット上で検索すると、PHPでスレッドをシミュレートする方法を思いつく人もいます。


1

「登録していただきありがとうございます」応答でContent-Length HTTPヘッダーを設定した場合、ブラウザは指定されたバイト数を受信した後に接続を閉じる必要があります。これにより、サーバー側プロセスが実行されたまま(ignore_user_abortが設定されていると想定)、エンドユーザーを待たせずに作業を終了できます。

もちろん、ヘッダーをレンダリングする前に応答コンテンツのサイズを計算する必要がありますが、短い応答(文字列への出力の書き込み、strlen()の呼び出し、header()の呼び出し、文字列のレンダリング)の場合は非常に簡単です。

このアプローチには、「フロントエンド」キューの管理を強制しないという利点があります。また、レースのHTTP子プロセスが相互に干渉しないようにするためにバックエンドでいくつかの作業を行う必要がある場合もありますが、これはすでに行う必要がありました。とにかく。


これは機能していないようです。私が使用するとheader('Content-Length: 3'); echo '1234'; sleep(5);、ブラウザは3文字しか使用しませんが、それでも応答を表示する前に5秒間待機します。何が欠けていますか?
Thomas Tempelmann、2015年

@ThomasTempelmann-出力を実際にすぐにレンダリングするには、おそらくflush()を呼び出す必要があります。そうしないと、スクリプトが終了するか、バッファをフラッシュするのに十分なデータがSTDOUTに送信されるまで、出力がバッファリングされます。
ピーター

私はすでにここでSOを見つけて、フラッシュする多くの方法を試しました。助けにはなりません。また、からわかるように、データもgzipされていない状態で送信されているようphpinfo()です。他に想像できることは、最初に最小バッファサイズ、たとえば256バイト程度に達する必要があることです。
Thomas Tempelmann

@ThomasTempelmann-私はあなたの質問やgzipに関する私の答えに何も表示しません(通常、最も単純なシナリオを最初に機能させてから、複雑なレイヤーを追加することは理にかなっています)。サーバーが実際にデータを送信するタイミングを確立するために、ブラウザプラグインのパケットスニファ(fiddler、tamperdataなど)を使用できます。次に、フラッシュに関係なく、終了するまでWebサーバーが実際にすべてのスクリプト出力を保持していることがわかった場合は、Webサーバーの構成を変更する必要があります(この場合、PHPスクリプトでできることは何もありません)。
Peter

私は仮想Webサービスを使用しているため、その構成をほとんど制御できません。犯人となる可能性のあるものについて他の提案を見つけたいと思っていましたが、あなたの答えは、見た目ほど広く適用できるものではないようです。明らかに、多くのことがうまくいかない可能性があります。あなたの解決策は、ここで説明する他のすべての回答よりも実装がはるかに簡単です。残念ながら私にはうまくいきません。
Thomas Tempelmann、

1

本格的なActiveMQが必要ない場合は、RabbitMQを検討することをお勧めします。RabbitMQは、AMQP標準を使用する軽量メッセージングです。

AMQPベースのメッセージブローカーにアクセスするための人気のAMQPクライアントライブラリであるphp-amqplibも調べることをお勧めします。


0

私はあなたがこのテクニックを試すべきだと思います。非同期として各ページの応答を待つことなく、すべてのページが同時に独立して実行されるのと同じくらい多くのページを呼び出すのに役立つでしょう。

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

exec()curl を使用してサーバー上で、または直接curlを使用して別のサーバー上でスポーンしても、それだけでは十分にスケーリングされません。execを実行すると、基本的には、他のWeb以外のサーバーで処理できる実行時間の長いプロセスでサーバーがいっぱいになります。また、curlを使用すると、何らかの負荷分散を組み込まない限り、別のサーバーが拘束されます。

私はいくつかの状況でGearmanを使用しましたが、この種の使用例にはより適しています。単一のジョブキューサーバーを使用して、基本的にサーバーで実行する必要のあるすべてのジョブのキューを処理し、ワーカーサーバーを起動します。各ワーカーサーバーは、ワーカープロセスのインスタンスを必要なだけ実行でき、その数を増やすことができます。必要に応じてワーカーサーバーを使用し、不要な場合はスピンダウンします。また、必要に応じてワーカープロセスを完全にシャットダウンし、ワーカーがオンラインに戻るまでジョブをキューに入れます。


-4

PHPはシングルスレッド言語であるため、execまたはを使用する以外に、PHPを使用して非同期プロセスを開始する正式な方法はありませんpopen。これに関するブログ投稿があります。MySQLでのキューのアイデアも良いアイデアです。

ここでの特定の要件は、ユーザーにメールを送信することです。メールを送信するのは非常に簡単で迅速な作業なので、なぜ非同期にそれを実行しようとしているのかについて知りたいです。大量のメールを送信していて、ISPがスパムの疑いであなたをブロックしているとしたら、それはキューに入れる理由の1つかもしれませんが、それ以外に、この方法でそれを行う理由は考えられません。


他のタスクは説明するのがより複雑であり、それが実際に質問の要点ではないため、電子メールは単なる例でした。私たちが電子メールを送信するために使用した方法では、リモートサーバーがメールを受け入れるまで、電子メールコマンドは戻りません。一部のメールサーバーは、メールを受け入れる前に(おそらくスパムボットと戦うために)長い遅延(10〜20秒の遅延など)を追加するように構成されており、これらの遅延はユーザーに渡されます。現在、送信するメールをキューに入れるためにローカルメールサーバーを使用しているため、この特定のメールサーバーは適用されませんが、同様の性質を持つ他のタスクがあります。
davr 2009年

たとえば、sslとポート465を使用してGoogle Apps SMTP経由でメールを送信すると、通常より時間がかかります。
Gixty、2015
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.