常駐プロセス(デーモン)として動くアプリを自作する(PHPとPEAR:: System_Daemon編)
ほとんどのWebサイト(Webアプリ)は次の二つの動作でできている。
- 画面処理。つまり、webページ(URL)へのアクセスをきっかけ(トリガー)にして何らかの処理をすると同時に画面を表示して終わり。
- バッチ処理。つまり、手でコマンドを叩いたりcronで自動的に呼び出されたりすることをトリガーにして起動。数秒あるいは数時間をかけてなんらかの処理をして終わり。
小規模なサイトであれば、バッチ無しで画面アプリだけで済ませている場合も少なくはない。 しかし逆に、普通のつくりの画面やバッチ処理では不都合だったり実現できなかったりするような機能が必要になることがある。例えば誰かが不定期にファイルをアップロードしたらできるだけすぐになんらかのバッチ的な処理を走らせたい、とか。 cronで定時起動するバッチだと最短でも1分間隔でしか起動できないけど、もっと短い間隔で一定の処理をサイクルさせたい、とか。
常駐プログラムとか常駐プロセスとかデーモン(daemon)などと呼ばれるアプリはそういった用途のために開発される。別に珍しいものではなく、httpdだってhttp daemonなのでありmysqldだってやはりmysql daemonである。いつも世話になっているソフトの振る舞いをマネして自分で似たようなもの開発すると思えばそれほどハードルの高いことではない。JavaでもperlでもPHPでも、作り方は似たようなものだ。子プロセスをforkして自ら(親プロセス)は終了、あとは子プロセスで無限ループをつくり、中で目的の処理をする。外部からの制御はOSからのシグナル操作を待ち受けて...
むかーしの話、PHPでデーモンを一本つくるはめになってpnctl関数群をちまちま使ってこさえたときは、なんだかちょっと苦労した。Javaやperlのほうがまだ作りやすかったような。 しかし今は、PHPであってもPEARにSystem_Daemonという便利な拡張が用意されているので、これを使うとラクにデーモンスクリプトを自作できる。
なお、同じdaemonでもいわゆるネットワークサーバの類はNet_Serverのほうが向いているのだが、その話はまたの機会に。
また、ここから先の話はすべてPHPが--enable-pcntlオプション付きでコンパイルされているのが前提。供用レンタルサーバで提供されているPHPではたいてい無理。たくさんのユーザーに好き勝手に常駐プロセス走らされたらCPU負荷的にひどいことになりそうだから当然だ。できれば専用サーバを使いましょう。
さて、--enable-pcntlしたPHPの実行環境にSystem_Daemonと、ついでにLog(これってデフォルトで入ってるんだっけ?pear listコマンドで確認可能)をインストールしたら、さっそくコードを書いてみよう。
require_once("Log.php");
require_once("System/Daemon.php");
// ログ出力用のオブジェクト。syslogのlocal4に吐く。レベルは7(デバッグ)つまり全部。
$logger = &Log::singleton('syslog', LOG_LOCAL4, "testtest", null, 7);
$options = array(
"appName" => "testdaemon" // このデーモンの名前
,"appDescription" => "pear system daemon test" // 説明
,"appDir" => dirname(__FILE__) // デーモンが動作するときのカレントディレクトリ
,"authorName" => "neta.ywcafe.net"
,"authorEmail" => "root@localhost"
,"sysMaxExecutionTime" => "0"
,"sysMaxInputTime" => "0"
// ,"appRunAsUID" => 501 // このデーモンの実行ユーザーとグループ
// ,"appRunAsGID" => 501 // ここではコメントアウトにしてるが必ず指定すべき
// ,"appPidLocation" => "/tmp/foo/bar/testdaemon/testdaemon.pid"
// ↑指定しなければ/var/run/上でappNameというディレクトリと*.pidファイルをつくろうとする
,"usePEARLogInstance" => $logger // 下の説明参照。
);
System_Daemon::setOptions($options);
System_Daemon::start();
//DBに接続するならこのへんで。つまりstart()のあとで。
$loop = 0;
while ( ! System_Daemon::isDying()) {// デーモンが停止処理中でないかループのたびにチェック
$loop++;
//
//このへんでなんでも好きな処理をする
//
// なんかログを吐いてみる。(system_daemon正式版)
System_Daemon::debug("ほげほげ");
// なんかログを吐いてみる。
//(どうせPearLogのシングルトンなんだから$loggerを直接呼んでもいいじゃないか版)
$logger->debug("hoge! ".date("r"));
if ($loop % 30 == 0) {
$logger->info("現在のメモリ使用量: ".memory_get_usage());
}
System_Daemon::iterate(5);
}
System_Daemon::stop();
起動方法: php コード名.php
停止方法: ps コマンドでプロセス番号を探してそれをkill。
下で述べる起動スクリプトを使うと sudo /etc/init.d/hogehoge start|stop とかでやれるようになる。
以下はコードのコメントでは書ききれなかったメモ。
動作状況は必ずログに吐くべし。いわゆるprintfデバッグもなるたけ避ける。
デーモンというものはバックグラウンドのプロセスなので何か起きてもそれを叫ぶ場がない。たとえば普通の画面処理ではないのでブラウザに何か表示されるわけでももちろんない。したがって、通常の動作結果や何らかの予期せぬ障害発生の報告はすべてログに吐かせる必要がる。System_Daemonの作者のサイトでも、「メインの無限ループ内部でechoすんな。本当にバックグラウンドで走ってるときにSTDOUTに吐くような動作(echoとか)をうっかりさせたらfatalエラーで死ぬよ」と解説しているとおりである。
なお、SYSTEM_DAEMON自体に独自にファイルにログを吐く仕組みが用意されているのだが、筆者はsyslogに吐くほうが好みであり、かつ他の画面やバッチのアプリのログ出力機構と共通化を図るためにも、単純にPear/Logとsyslogを組み合わせて使っている。このほうが何かと簡単かつ単純だ。
実行ユーザー/グループを設定する
これはデーモンに限らず言えることだが、このデーモンスクリプトはどの実行ユーザー/グループで動く(べき)のか、ということをきちんと検討したうえでそれをしかるべくコード上で表現しておいたほうがいい。余計な混乱を避けれる。設定しなければそれを叩いたユーザーの権限で動くだけだが、実運用ではデーモンはinitスクリプト等で自動起動されるケースが多い。それでほっとくroot権限で動くということになり、その状況はまったくおすすめできない。
このスクリプトのファイル自体をchmod 755しておかないと起動スクリプトを作ってくれない
ここに書いてあるように System_Daemon::writeAutoRun();とやると、 /etc/init.d/の配下に起動スクリプトを自動的に作ってくれるという気の利いた機能がついている。 ただし、そのデーモンのスクリプトのファイル自体に実行属性がついてることを前提としたつくりになっている。これも好みの問題かもしれないが、 「php スクリプト名.php」というふうに指定して実行するほうがいいと思うので、起動スクリプトができあがったらその「/usr/local/bin/php /パス/スクリプト名.php」といった風に書き直しておいたほうがいい。
ループ制御
デーモンというものは内部で無限ループすることでいわゆる「常駐」状態となる。 System_Daemon::isDying() の返す値でループを続けるか否かを判断することで、スクリプトが停止のシグナルを受けたときに処理状態が中途半端な状態で停止してしまうことをある程度防ぐことができる(らしい、ってとこまでしかコード読んでないんだけど)。
また、ループの最初または最後で必ず System_Daemon::iterate(秒数) する。このメソッドは内部で指定秒数だけsleepし、かつ、clearstatcacheを呼んで余計な内部キャッシュを掃除してくれる。秒数をゼロにするのはあまりおすすめできない。CPUを食いすぎる可能性が高まるからだ(そのデーモンに何をやらせるかにもよるけど)。
メモリ使用量
ループの内部でうっかり同じオブジェクトや配列に要素を追加し続けたりすると、当然ながらそのデーモンのプロセスが抱えこむメモリ使用量は増え続けてしまう。一種のリーク状態だ。作る側が気をつけるしかないが、手っ取り早い方法は、memory_get_usage()の結果を時々ログに吐いて観察してみること。
シグナルハンドラ
できればいろんなシグナルを受け取った時の動作をSignalHandlerとしてちゃんと書くのがベターなんだけど、ここでははごく簡単なサンプルということであしからずご了承ください。
その他もろもろの諸注意は System_Daemonの作者のサイトへ。
see also
- DB更新とメール送信とをトランザクションにつつむ (2009/10)

コメントする
(初めてのコメントの時は、コメントが表示されるためにこのブログのオーナーの承認が必要になることがあります。承認されるまでコメントは表示されませんのでしばらくお待ちください)