この記事では、PHPのデーモンシステムの実装の主なニュアンスを明らかにし、Yii2コンソールコマンドを悪魔化するように教えます。
過去3年間、私は1つのグループの企業向けに十分な規模の企業ポータルを開発および開発してきました。 私は、多くの人と同じように、ビジネスが必要とするタスクを解決するときにタイムアウトが収まらないという問題に遭遇しました。 30万行のExcelでレポートを作成し、1500文字のメーリングリストを送信します。 当然、そのようなタスクはバックグラウンドタスク、デーモン、およびcrontabで解決する必要があります。 記事の枠組みでは、王冠と悪魔の比較は行いません。このような問題を解決するために悪魔を選びました。 同時に、私たちにとって重要な要件は、それぞれバックエンド用にすでに記述されたすべてのものにアクセスできることでした。デーモンはYii2フレームワークの継続である必要があります。 同じ理由で、phpDaemonのような既製のソリューションは私たちに適合しませんでした。
カットの下で、私が得たYii2に悪魔を実装するための既製のソリューション。
PHPのデーモンのテーマは、うらやましい規則性で上昇します(1、2、3、
およびbadooの連中は、接続を失うことなくそれらを再起動します )。 おそらく私の
自転車は 、人気のあるフレームワークで悪魔を実行
する簡単な方法が役立つでしょう。
いくつかの基本
プロセスが悪魔になるためには、次のものが必要です。
- コンソールおよび標準入出力ストリームからスクリプトをアンバインドします。
- メインコードの実行を無限ループでラップします。
- プロセス制御メカニズムを実装します。
コンソールから取り外します
開始するには、標準ストリームSTDIN、STOUT、STDERRを閉じます。 しかし、PHPを使用しないと、最初のオープンストリーム標準になるため、/ dev / nullで開きます。
if (is_resource(STDIN)) { fclose(STDIN); $stdIn = fopen('/dev/null', 'r'); } if (is_resource(STDOUT)) { fclose(STDOUT); $stdOut = fopen('/dev/null', 'ab'); } if (is_resource(STDERR)) { fclose(STDERR); $stdErr = fopen('/dev/null', 'ab'); }
次に、プロセスをforkし、forkをメインプロセスにします。 ドナープロセス-完了。
$pid = pcntl_fork(); if ($pid == -1) { $this->halt(self::EXIT_CODE_ERROR, 'pcntl_fork() rise error'); } elseif ($pid) { $this->halt(self::EXIT_CODE_NORMAL); } else { posix_setsid(); }
無限ループと制御
私はすべてがサイクルで理解されていると思います。 ただし、必要な制御メカニズムをより詳細に検討する必要があります。
すでに実行中のプロセスを修正する
ここではすべてが簡単です-デーモンを起動すると、PIDがその名前のファイルに格納され、作業の最後にこのファイルが爆発します。
POSIX信号処理
デーモンは、オペレーティングシステムからの信号を正しく処理する必要があります。 シグナルを受信すると、SIGTERMはスムーズにシャットダウンします。 これはいくつかのことで実現されます。まず、受信した信号を処理する関数を定義します。
pcntl_signal(SIGTERM, ['MyClassName', 'mySignalHandlerFunction']);
次に、信号処理関数で、クラスの静的プロパティの割り当てをtrueに設定します。
static function signalHandler($signo, $pid = null, $status = null) { self::$stopFlag = true; }
そして第三に、無限ループはそれほど無限ではないはずです。
while (!self::$stopFlag) { pcntl_signal_dispatch(); }
PHPの異なるバージョンでの信号処理の機能PHP <5.3.0では、特別な
宣言ディレクティブ
(ticks = N)が信号の配信
に使用されました。 tickは、declareブロック内のパーサーによって実行される低レベルの操作ごとに発生するイベントです。 信号の分配は設定に従って行われました。 値が小さすぎるとパフォーマンスが低下し、値が大きすぎると信号処理がタイミングよく行われません。
PHP> = 5.3.0では、
pcntl_signal_dispatch()関数が登場しました。この関数は、手動でシグナルを配信するために呼び出すことができます。これは各反復後に行います。
そして最後に、PHP 7.1では
、シグナルの非同期配信が利用可能になり、オーバーヘッドや手動で関数を呼び出すことなく、ほぼ瞬時にシグナルを受信できるようになります。
これで、オペレーティングシステムからコマンドを受信すると、スクリプトは現在の反復を静かに終了し、ループを終了します。
メモリリーク監視
残念ながら、デーモンが再起動せずに長時間動作すると、メモリが流れ始めます。 リーク率は、使用する機能によって異なります。 私たちの実践から、最もひどく流れていたのは、標準のSoapClientクラスを介してリモートSOAPサービスと連携するデーモンでした。 したがって、これを監視し、定期的に再起動する必要があります。 サイクルを漏れ制御条件で補完します。
while (!self::$stopFlag) { if (memory_get_usage() > $this->memoryLimit) { break; } pcntl_signal_dispatch(); }
Yiiのコードはどこにありますか?
ソースはGithub-
yii2-daemonに投稿されており、パッケージはcomposerを介してインストールすることもできます。
パッケージは、基本クラスDaemonControllerとクラスWatcherDaemonControllerの2つの抽象クラスのみで構成されています。
デーモンコントローラー <?php namespace vyants\daemon; use yii\base\NotSupportedException; use yii\console\Controller; use yii\helpers\Console; abstract class DaemonController extends Controller { const EVENT_BEFORE_JOB = "beforeJob"; const EVENT_AFTER_JOB = "afterJob"; const EVENT_BEFORE_ITERATION = "beforeIteration"; const EVENT_AFTER_ITERATION = "afterIteration"; public $demonize = false; public $isMultiInstance = false; protected $parentPID; public $maxChildProcesses = 10; protected static $currentJobs = []; protected $memoryLimit = 268435456; private static $stopFlag = false; protected $sleep = 5; protected $pidDir = "@runtime/daemons/pids"; protected $logDir = "@runtime/daemons/logs"; private $stdIn; private $stdOut; private $stdErr; public function init() { parent::init();
WatcherDaemonController <?php namespace vyants\daemon\controllers; use vyants\daemon\DaemonController; abstract class WatcherDaemonController extends DaemonController { public $daemonFolder = 'daemons'; protected $firstIteration = true; public function init() { $pid_file = $this->getPidPath(); if (file_exists($pid_file) && ($pid = file_get_contents($pid_file)) && file_exists("/proc/$pid")) { $this->halt(self::EXIT_CODE_ERROR, 'Another Watcher is already running.'); } parent::init(); } protected function doJob($job) { $pid_file = $this->getPidPath($job['daemon']); \Yii::trace('Check daemon ' . $job['daemon']); if (file_exists($pid_file)) { $pid = file_get_contents($pid_file); if ($this->isProcessRunning($pid)) { if ($job['enabled']) { \Yii::trace('Daemon ' . $job['daemon'] . ' running and working fine'); return true; } else { \Yii::warning('Daemon ' . $job['daemon'] . ' running, but disabled in config. Send SIGTERM signal.'); if (isset($job['hardKill']) && $job['hardKill']) { posix_kill($pid, SIGKILL); } else { posix_kill($pid, SIGTERM); } return true; } } } \Yii::error('Daemon pid not found.'); if ($job['enabled']) { \Yii::trace('Try to run daemon ' . $job['daemon'] . '.'); $command_name = $job['daemon'] . DIRECTORY_SEPARATOR . 'index';
デーモンコントローラー
これは、すべてのデーモンの親クラスです。 最小限の悪魔の例を次に示します。
<?php namespace console\controllers\daemons; use vyants\daemon\DaemonController; class TestController extends DaemonController { protected function doJob($job) {
defineJobs()関数は、実行する一連のタスクを返す必要があります。 デフォルトでは、配列を返すことが期待されています。 たとえばMongoCursorを返したい場合は、defineJobExtractor()をオーバーライドする必要があります。 doJob()関数は、入力用の1つのタスクを受け取り、それを使用して必要な操作を実行し、ソース内の特定のタスクを2回目に落ちないように満たす必要があります。
可能なパラメーターと設定:
- demonize-このパラメーターは、スクリプトがデーモン化されるか、コンソールアプリケーションとして動作するかを決定します。 このパラメーターは、コンソールから設定できます。--demonize = 1
- isMultiInstanceおよびmaxChildProcesses-デーモンが独自のコピーを作成できるかどうか、および同時に動作できる最大数を決定します。 この機能により、複数のタスクを並行して実行できます。 doJobは子プロセスで実行され、親プロセスは子孫にタスクを委任するだけで、その数が許可されている最大数を超えないようにします。 非常に時間のかかるいくつかのタスクを実行するために十分なサーバーリソースがある場合に非常に便利です。 デフォルトでは、この動作はオフになっています。 パラメーターはコンソールからも利用できます:--isMultiInstance = 1 --maxChildProcesses = 2
- memoryLimit-デーモンがメモリを消費するためのしきい値。スタンバイモードのデーモンがこのしきい値を超えた場合、sippokuを気高くコミットします。 前に示したように、リークの結果としてデーモンによって消費されるメモリのサイズを削減します。
- sleep-タスクのチェック間でデーモンがスリープ状態になる秒単位の時間。 デーモンは、defineJobが空を返し、タスクがある間デーモンがスリープしない場合にのみスリープ状態になります。 したがって、defineJobsはタスクの静的リストを返すべきではありません。そうしないと、デーモンはタスクを際限なく打ち続けて休みます。
- pidDirおよびlogDir-ログとpid-sを保存する方法、Yiiエイリアスをサポートします。 デフォルトでは、「@ runtime / daemons / pids」および「@ runtime / daemons / logs」
接続損失の問題
fork()操作中に、親プロセスで確立された接続が子プロセスで機能しなくなります。 この問題を回避するために、すべての分岐の後、renewConnections()関数呼び出しが行われます。 デフォルトでは、この関数はYii :: $ app-> dbのみを再接続しますが、これをオーバーライドして、子プロセスで接続する必要がある他のソースを追加できます。
ロギング
デーモンは、標準のYiiロガーを再構成します。 デフォルトの動作に満足できない場合は、initLogger()関数をオーバーライドします。
WatcherDaemonController
これはほぼ完成した悪魔のオブザーバーです。 このデーモンのタスクは、他のデーモンを監視し、必要に応じてそれらを開始および停止することです。 2回起動することはできないため、crontabで安全に実行できます。 使用を開始するには、コンソール/コントローラーにデーモンフォルダーを作成し、次の形式のクラスを配置する必要があります。
<?php namespace console\controllers\daemons; use vyants\daemon\controllers\WatcherDaemonController; class WatcherController extends WatcherDaemonController { protected $sleep = 10; protected function getDaemonsList() { return [ ['daemon' => 'daemons/test', 'enabled' => true] ]; } }
定義する必要がある関数は、getDaemonsList()の1つだけです。この関数は、監視するデーモンのリストを返します。 最も単純な形式では、コードに配線された配列ですが、この場合、その場でリストを変更することはできません。 デーモンのリストをデータベースまたは別のファイルに入れて、そこから毎回取得します。 この場合、ウォッチャーは自身を再起動せずにデーモンをオンまたはオフにできます。
おわりに
現在、電子メールメッセージの送信からレポートの生成、異なるシステム間でのデータの更新まで、さまざまなタスクを実行する50以上のデーモンがあります。
デーモンは、MySQL、RabbitMQ、リモートWebサービスなど、さまざまなタスクのソースで動作します。 フライトは正常です。
もちろん、PHPのデーモンをGoの同じデーモンと比較することはできません。 しかし、開発速度が速いこと、すでに記述されたコードを再利用できること、および別の言語でチームを学ぶ必要がないことは、短所を上回ります。