155

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

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

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

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

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

4

16 に答える 16

85

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

独自のロールを作成するのはそれほど難しいことではありません。チェックアウトする他のオプションがいくつかあります。

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

もう 1 つの、おそらくもっと簡単な方法は、ignore_user_abortを使用することです。ユーザーにページを送信したら、早期終了を恐れることなく最終処理を行うことができますが、これにはユーザーからのページの読み込みが長引いているように見える効果があります。視点。

于 2009-05-13T16:20:03.010 に答える
23

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

于 2011-04-14T16:02:08.543 に答える
18

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

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

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

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

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

設定には少し手間がかかりますが、メリットはたくさんあります。

于 2009-05-13T23:31:59.640 に答える
9

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

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

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

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

ここでも php-fpm が必要です。

于 2016-01-21T10:31:22.003 に答える
7

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

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

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

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

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

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

于 2009-05-18T10:16:22.263 に答える
6

これは、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);
        }
    }
}
于 2009-05-13T20:44:08.617 に答える
4

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

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

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

于 2009-05-13T16:40:00.013 に答える
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);
}

?>
于 2013-05-29T07:07:27.913 に答える
1

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

ネットで PHP のスレッド化に関する情報を検索すると、PHP でスレッドをシミュレートする方法を思いついた人がいます。

于 2009-05-13T16:20:27.800 に答える
1

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

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

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

于 2011-08-05T23:09:43.857 に答える
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

于 2016-12-19T15:18:25.220 に答える
-5

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

ここでの具体的な要件は、ユーザーにメールを送信することです。電子メールの送信は非常に簡単で簡単なタスクであるため、非同期でそれを実行しようとしている理由が知りたいです。大量の電子メールを送信していて、ISP がスパムの疑いでブロックしている場合、それがキューに入れる理由の 1 つかもしれませんが、それ以外にこの方法で送信する理由は思いつきません。

于 2009-05-13T16:21:03.960 に答える