web-dev-qa-db-ja.com

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

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

これまで、いくつかの場所では、exec()でハックのようなものを使用していました。基本的に次のようなことを行います:

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

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

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

136
davr

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

独自のローリングはそれほど難しくありません。チェックアウトするためのその他のオプションがいくつかあります。

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

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

77
Paul Dixon

応答を待たずに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接続が閉じられると、スクリプトは時間制限なしで実行を継続します。

20
Markus

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

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

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

サービスは、mysqlを使用してタスクのキューを追跡できます。ポイントは、サービス内ですべてラップされ、スクリプトはURLを消費するだけです。これにより、必要に応じてサービスを別のマシン/サーバーに自由に移動できます(つまり、簡単に拡張できます)。

Http承認またはカスタム承認スキーム(AmazonのWebサービスなど)を追加すると、タスクを開いて(必要に応じて)他の人/サービスが消費できるようになります。キューとタスクのステータス。

セットアップには少し時間がかかりますが、多くの利点があります。

17
rojoca

1つのプロジェクトで Beanstalkd を使用しましたが、再度計画しました。非同期プロセスを実行する優れた方法であることがわかりました。

私がそれでやったことのいくつかは次のとおりです。

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

Zend-Frameworkベースのシステムを作成して 'Nice' URLをデコードしました。たとえば、QueueTask('/image/resize/filename/example.jpg')を呼び出す画像のサイズを変更するためです。 URLは最初にarray(module、controller、action、parameters)にデコードされ、次にキュー自体に注入するためにJSONに変換されました。

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

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

7
Alister Bulman

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

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

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

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

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

7

これは、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);
        }
    }
}
5
Andrew Moore

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

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

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

4
Darryl Hein

PHPにはマルチスレッドがあり、デフォルトでは有効になっていないため、pthreadsまさにそれを行います。ただし、ZTSでコンパイルされたphpが必要です。 (スレッドセーフ)リンク:

別のチュートリアル

pthreads PECL拡張機能

3
Omar S.

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
Kjeld

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

PHPスレッド処理をネット上で検索すると、PHPのスレッドをシミュレートする方法を考え出す人がいます。

1
Peter D

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

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

このアプローチにはnotという利点があり、「フロントエンド」キューを管理する必要があります。また、HTTP子プロセスの競合を防ぐためにバックエンドでいくつかの作業が必要になる場合がありますが、とにかく、それはあなたがすでにしなければならないことです。

1
Peter

ActiveMQを完全に使いたくない場合は、 RabbitMQ を検討することをお勧めします。 RabbitMQは、 AMQP標準 を使用する軽量メッセージングです。

php-amqplib -AMQPベースのメッセージブローカーにアクセスするための一般的なAMQPクライアントライブラリも検討することをお勧めします。

1
phpPhil

非同期として各ページの応答を待たずに、すべてのページが一度に独立して実行されるページを好きなだけ呼び出すのに役立つこのテクニックを試してみるべきだと思います。

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

exec()を使用してサーバーで新しいプロセスを生成するか、curlを使用して別のサーバーで直接生成しても、それほどうまくスケーリングしません。execを実行すると、基本的に、非Web向きのサーバー、およびcurlを使用すると、何らかの負荷分散を構築しない限り、別のサーバーが拘束されます。

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

0