私はやや大規模なWebアプリケーションで作業しており、バックエンドはほとんどがPHPにあります。コードには、いくつかのタスクを完了する必要があるいくつかの場所がありますが、ユーザーに結果を待たせたくありません。たとえば、新しいアカウントを作成するときに、ウェルカムメールを送信する必要があります。しかし、「登録完了」ボタンを押すと、実際にメールが送信されるまで待たせたくありません。ただプロセスを開始して、すぐにユーザーにメッセージを返したいだけです。
これまで、いくつかの場所では、exec()でハックのようなものを使用していました。基本的に次のようなことを行います:
exec("doTask.php $arg1 $arg2 $arg3 >/dev/null 2>&1 &");
これは動作しているように見えますが、もっと良い方法があるかどうか疑問に思っています。 MySQLテーブルのタスクをキューに入れるシステムと、そのテーブルを1秒間に1回クエリし、見つかった新しいタスクを実行する別の長時間実行PHPスクリプトを書くことを検討しています。これには、必要に応じて将来的に複数のワーカーマシンにタスクを分割できるという利点もあります。
車輪を再発明していますか? exec()ハックやMySQLキューよりも良い解決策はありますか?
キューイングアプローチを使用しましたが、サーバーの負荷がアイドルになるまでその処理を延期できるため、「緊急ではないタスク」を簡単に分割できる場合、負荷を非常に効果的に管理できます。
独自のローリングはそれほど難しくありません。チェックアウトするためのその他のオプションがいくつかあります。
別の、おそらくより簡単なアプローチは、 ignore_user_abort を使用することです。ページをユーザーに送信すると、途中終了の恐れなく最終処理を実行できますが、ユーザーの観点からページのロードを延長します。
応答を待たずに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接続が閉じられると、スクリプトは時間制限なしで実行を継続します。
プロセスを分岐する別の方法は、curlを使用することです。内部タスクをWebサービスとして設定できます。例えば:
次に、ユーザーがアクセスしたスクリプトで、サービスを呼び出します。
$service->addTask('t1', $data); // post data to URL via curl
サービスは、mysqlを使用してタスクのキューを追跡できます。ポイントは、サービス内ですべてラップされ、スクリプトはURLを消費するだけです。これにより、必要に応じてサービスを別のマシン/サーバーに自由に移動できます(つまり、簡単に拡張できます)。
Http承認またはカスタム承認スキーム(AmazonのWebサービスなど)を追加すると、タスクを開いて(必要に応じて)他の人/サービスが消費できるようになります。キューとタスクのステータス。
セットアップには少し時間がかかりますが、多くの利点があります。
1つのプロジェクトで Beanstalkd を使用しましたが、再度計画しました。非同期プロセスを実行する優れた方法であることがわかりました。
私がそれでやったことのいくつかは次のとおりです。
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();
のデフォルト値)を実行した場合、最初に数秒間停止します。
Php-fpmの場合、高価なタスクを提供するだけの質問がサポートされている場合、 fastcgi_finish_request()
functionを使用しないのはなぜですか?
この関数は、すべての応答データをクライアントにフラッシュし、要求を終了します。これにより、クライアントへの接続を開いたままにせずに、時間のかかるタスクを実行できます。
この方法で非同期性を実際に使用することはありません。
fastcgi_finish_request()
を実行します。もう一度php-fpmが必要です。
これは、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);
}
}
}
これは、私が数年前から使用している方法と同じものであり、これ以上良いものを見たり発見したりしていません。人々が言ったように、PHPはシングルスレッドなので、他にできることはあまりありません。
私は実際にこれに1つの余分なレベルを追加しました、そしてそれはプロセスIDを取得して保存しています。これにより、別のページにリダイレクトし、AJAXを使用してプロセスが完了した(プロセスIDがもう存在しない)かどうかをユーザーに確認させることができます。これは、スクリプトの長さが原因でブラウザーがタイムアウトする場合に便利ですが、ユーザーは次のステップの前にそのスクリプトが完了するのを待つ必要があります。 (私の場合、データベースに最大30 000レコードを追加した後、ユーザーが何らかの情報を確認する必要があるCSVのような大きなZipファイルを処理していました。)
レポート生成にも同様のプロセスを使用しました。遅いSMTPに実際の問題がない限り、電子メールなどに「バックグラウンド処理」を使用するかどうかはわかりません。代わりに、テーブルをキューとして使用し、キュー内で電子メールを送信するために毎分実行されるプロセスを使用する場合があります。電子メールを2回送信するなどの同様の問題に注意する必要があります。他のタスクについても同様のキューイングプロセスを検討します。
PHPにはマルチスレッドがあり、デフォルトでは有効になっていないため、pthreadsまさにそれを行います。ただし、ZTSでコンパイルされたphpが必要です。 (スレッドセーフ)リンク:
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);
}
?>
残念ながらPHPにはネイティブスレッド機能はありません。ですから、この場合、あなたはやりたいことをするためにある種のカスタムコードを使う以外に選択肢はないと思います。
PHPスレッド処理をネット上で検索すると、PHPのスレッドをシミュレートする方法を考え出す人がいます。
「登録ありがとうございます」応答でContent-Length HTTPヘッダーを設定した場合、指定されたバイト数を受信した後、ブラウザーは接続を閉じる必要があります。これにより、サーバー側プロセスは実行されたままになり(ignore_user_abortが設定されていると仮定)、エンドユーザーを待たせることなく作業を終了できます。
もちろん、ヘッダーをレンダリングする前に応答コンテンツのサイズを計算する必要がありますが、短い応答の場合は非常に簡単です(出力を文字列に書き込む、strlen()を呼び出す、header()を呼び出す、文字列をレンダリングする)。
このアプローチにはnotという利点があり、「フロントエンド」キューを管理する必要があります。また、HTTP子プロセスの競合を防ぐためにバックエンドでいくつかの作業が必要になる場合がありますが、とにかく、それはあなたがすでにしなければならないことです。
ActiveMQを完全に使いたくない場合は、 RabbitMQ を検討することをお勧めします。 RabbitMQは、 AMQP標準 を使用する軽量メッセージングです。
php-amqplib -AMQPベースのメッセージブローカーにアクセスするための一般的なAMQPクライアントライブラリも検討することをお勧めします。
非同期として各ページの応答を待たずに、すべてのページが一度に独立して実行されるページを好きなだけ呼び出すのに役立つこのテクニックを試してみるべきだと思います。
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
exec()
を使用してサーバーで新しいプロセスを生成するか、curlを使用して別のサーバーで直接生成しても、それほどうまくスケーリングしません。execを実行すると、基本的に、非Web向きのサーバー、およびcurlを使用すると、何らかの負荷分散を構築しない限り、別のサーバーが拘束されます。
私はいくつかの状況でGearmanを使用しましたが、この種のユースケースにはGearmanが適していると思います。単一のジョブキューサーバーを使用して、基本的にサーバーで実行する必要のあるすべてのジョブのキューイングを処理し、ワーカーサーバーをスピンアップします。各ワーカーサーバーは、必要な数のワーカープロセスのインスタンスを実行し、必要に応じてワーカーサーバーを使用し、不要な場合はそれらを停止します。また、必要なときにワーカープロセスを完全にシャットダウンし、ワーカーがオンラインに戻るまでジョブをキューに入れます。