PHPのプログラムをデーモンとして動かしてくれるPEAR::System_Daemon | A Day In The Boy's Life

A Day In The Boy's Life

とあるエンジニアのとある1日のつぶやき。

PEARパッケージのSystem_Daemon は、作ったPHPプログラムをデーモンとして常駐して動かしてくれます。


なんで、LL言語のPHPをデーモンとして動かす必要があるのか、またはcron使えばいいじゃんってところはあるのですが、作者のページ にも書かれているように、開発が容易で高速に開発できるPHPの特性を活かしてデーモンとして動かしたい場合や、cronで動作させる場合は単体で処理が繰り返されるために前後の処理の状況をチェックするのが面倒、または不具合の原因にもなるからってことで、このPEARパッケージを開発したようです。


LAMP主体のサービスを構築する企業なんかは、PHPで作られた豊富なライブラリを持ってたりもするでしょうから、PHPの資産を活かしつつデーモンとして動かしたいプログラムを開発するって場合はいいかもしれませんね。



System_Daemonのインストール


2010年04月29日現在、System_Daemonのバージョンは0.10.2でベータ版として提供されているため、pearコマンドでインストールする場合は、パッケージ名の最後に「-beta」を付けてインストールする必要があります。


# pear install System_Daemon-beta
WARNING: channel "pear.php.net" has updated its protocols, use "channel-update pear.php.net" to update
Did not download optional dependencies: pear/Log, use --alldeps to download automatically
pear/System_Daemon can optionally use package "pear/Log" (version >= 1.0)
downloading System_Daemon-0.10.2.tgz ...
Starting to download System_Daemon-0.10.2.tgz (31,154 bytes)
.........done: 31,154 bytes
install ok: channel://pear.php.net/System_Daemon-0.10.2


メッセージの途中で出ているPEARのLogパッケージは特に無くても動作します。

その場合、独自実装のログ出力になる模様。



System_Daemonを利用してPHPプログラムをデーモン化してみる


では、早速PHPのデーモンプログラムを書いてみます。


#!/usr/bin/php
<?php

require_once 'System/Daemon.php';

// デーモンの名前
$app_name = "date_deamon";

$options = array("appName"        => $app_name, // デーモンの名前
                 "authorEmail"    => "foo@example.com", // 作者のE-Mail
                 "appDir"         => dirname(__FILE__), // アプリを動かすディレクトリ
                 "logLocation"    => dirname(__FILE__) . "/" . $app_name . ".log", // アプリのログのパス(デフォルトだと/var/log/{アプリ名}.log)
                 "appPidLocation" => dirname(__FILE__) . "/" . $app_name . "/" . $app_name . ".pid", // プロセスIDのパス(デフォルトだと/var/run/{アプリ名}/{アプリ名}.pid)
                 "appRunAsUID"    => 500,
                 "appRunAsGID"    => 500);

// 上記オプションをまとめてセット
System_Daemon::setOptions($options);

// デーモンの起動
System_Daemon::start();

// これ以降の処理がデーモンとして動作させたい処理
// デーモンが生きてるかチェックしながらループ
while (!System_Daemon::isDying()) {
// アプリのログに日付をはき続ける
    System_Daemon::log(System_Daemon::LOG_INFO, date('Y/m/d H:i:s'));
// SLEEP処理(5秒待機)
    System_Daemon::iterate(5);
}

System_Daemon::stop();


デーモンとして動かすほどのプログラムでもないのですが、これを起動させてみてプロセスを表示してみると常駐して動作していることがわかります。


$ php date_deamon.php
[Apr 30 01:51:39]   notice: Starting date_deamon daemon, output in: '/home/work/workspace/date_deamon.log'

$ ps aux | grep date_deamon.php
work     21997  0.0  0.7  22172  3908 pts/0    S    01:51   0:00 php date_deamon.php


実行結果は、「System_Daemon::setOptions」の"logLocation"で指定したログファイルの中に日付が吐き出され続けます。


[Apr 30 01:55:34]     info: 2010/04/30 01:55:34
[Apr 30 01:55:39]     info: 2010/04/30 01:55:39
[Apr 30 01:55:44]     info: 2010/04/30 01:55:44
[Apr 30 01:55:49]     info: 2010/04/30 01:55:49

停止したい場合は、killコマンドを使い、その後にプロセス番号が書かれているpidファイルを削除しておきます。


$ kill -9 21997
$ rm date_deamon/date_deamon.pid


今回の例では、uidとgidが500のユーザーでデーモンを起動しています。

それぞれ、「System_Daemon::setOptions」の"appRunAsUID"および"appRunAsGID"で指定するわけですが、これを指定しなかった場合、System_Daemonははrootユーザーでデーモンを実行させようとします

この時、プロセスIDを格納するためのpidファイルの所有者およびグループをrootユーザーにしようとするため、エラーが出てデーモンが起動できません。


[Apr 30 00:42:46]      err: Unable to change group of file '/home/work/workspace/date_deamon' to 0 [l:1427]


rootユーザー以外でデーモンを起動したい場合は、必ず"appRunAsUID"および"appRunAsGID"オプションを指定しておく必要があります。
また、ログファイルやpidファイルのデフォルトの保存場所も「/var/log」や「/var/run」となるため、root以外のユーザーで実行する場合は、適宜権限のあるパスを指定しておきます。


ループ処理の中で、System_Daemon::iterateで5秒間待機させていますが、これはPHPのsleep関数で代替できそうですが、実際にはclearstatcache を呼び出すなど余分なキャッシュなどの掃除をしてくれるみたいです。


参考: 常駐プロセス(デーモン)として動くアプリを自作する(PHPとPEAR:: System_Daemon編) @ Web屋のネタ帳



ディスク容量をチェックするデーモン


もう少し実用的なものにするため、指定のディレクトリの容量をチェックするデーモンを作ってみます。


#!/usr/bin/php
<?php

require_once 'System/Daemon.php';

// 監視対象ディレクトリ
$check_dir = "/home/work/workspace/";

// 閾値(MB)
$limit = 100;

System_Daemon::setOption("appName", "diskcheck");
System_Daemon::setOption("authorEmail", "foo@example.com");
// デーモンの説明
System_Daemon::setOption("appDescription", "Disk Check Daemon");
// 作者の名前
System_Daemon::setOption("authorName", "itboy");

System_Daemon::start();

System_Daemon::log(System_Daemon::LOG_INFO, "Disk-Check-Daemon started");

while (!System_Daemon::isDying()) {
    $disk_size = getDirSize($check_dir);

    if ($disk_size > ($limit * 1024 * 1024)) {
        System_Daemon::log(System_Daemon::LOG_WARNING, $check_dir . " over " . $limit . "MB !!" . " / size : " . number_format($disk_size) . "Byte");
    } else {
        System_Daemon::log(System_Daemon::LOG_INFO, $check_dir . " size : " . number_format($disk_size) . "Byte");
    }
    System_Daemon::iterate(5);
}

System_Daemon::stop();

// ディレクトリの容量をチェックする関数(参照: http://itpro.nikkeibp.co.jp/article/COLUMN/20070827/280408/
function getDirSize($path) {
    $total_size = 0;
    if (is_file($path)) {
        return filesize($path);
    } elseif (is_dir($path)) {
        $basename = basename($path);
        if ($basename == '.' || $basename == '..') {
            return 0;
        }
        $file_list = scandir($path);
        foreach ($file_list as $file) {
            $total_size += getDirSize($path .'/'. $file);
        }
        return $total_size;
    } else {
        return 0;
    }
}


今回は、rootユーザーで実行する前提にしたので、オプションの数が少なくなっています。


※ "appDescription"と"authorName"のオプションを追加してますが、自動起動スクリプトの作成(後述)のためです。


オプションは配列でまとめて指定も出来ますが、今回のように「System_Daemon::setOption」で一つずつオプションを指定することも可能です。

ディレクトリ内のファイル容量をチェックする関数は、「54. 再帰関数を使ってみよう @ ITpro 」のものを使わせていただきました。


動作原理もそれほど複雑ではなく、getDirSize関数で指定のディレクトリ内のファイルの容量をチェックし、閾値に設定している10MBを超えた場合は、LOG_WARNINGでアラートをログに出すようにしています。

実際にはメールを飛ばすなんかの手段の方がよいかもしれませんね。


# php disk_check.php
[May 01 01:02:27]   notice: Starting diskcheck daemon, output in: '/var/log/diskcheck.log'

# ps aux | grep disk_check.php
root     25752  0.0  0.7  22172  4020 pts/0    S    01:02   0:00 php disk_check.php


この時に、容量の大きいファイルを作ってみて閾値を超える状態を作り出してみます。


$ dd if=/dev/zero of=largefile.dmp bs=1M count=100
100+0 records in
100+0 records out
104857600 bytes (105 MB) copied, 0.197439 seconds, 531 MB/s

この一連の流れをログで確認してみると・・・


[May 01 01:06:38]     info: /home/work/workspace/ size : 55,772Byte
[May 01 01:06:43]     info: /home/work/workspace/ size : 55,772Byte
[May 01 01:06:48]     info: /home/work/workspace/ size : 55,772Byte
[May 01 01:06:53]  warning: /home/work/workspace/ over 100MB !! / size : 104,913,372Byte [l:23]
[May 01 01:06:58]  warning: /home/work/workspace/ over 100MB !! / size : 104,913,372Byte [l:23]
[May 01 01:07:03]     info: /home/work/workspace/ size : 55,772Byte
[May 01 01:07:08]     info: /home/work/workspace/ size : 55,772Byte


途中で閾値の100MBの容量を超えたため、warningがログに吐き出され、容量の大きいファイルを削除した後は単なる情報としてディスクのサイズをログに書き出すようになっています。



作成したSystem_DaemonのPHPプログラムを自動起動スクリプトに登録する


System_Daemonの面白い機能の一つに、自動起動スクリプト(/etc/init.d)へ登録してくれるものがあります。

自動起動スクリプトに登録するためには、幾つかの準備が必要です。


1. オプションに"appName"、"authorEmail"、"appDescription"、"authorName"を指定する


2. System_Daemon::start();の後に、System_Daemon::writeAutoRun();の記述を追加する


System_Daemon::start();
$path = System_Daemon::writeAutoRun();


3. 実行するPHPスクリプトに実行権限を付与しておく


# chmod 755 disk_check.php


これで、disk_check.phpを一度実行させれば「/etc/init.d」ディレクトリ以下に自動起動スクリプトが作成されます。

ここから、起動させたければ


# /etc/init.d/diskcheck start
[May 01 01:41:09]   notice: Starting diskcheck daemon, output in: '/var/log/diskcheck.log'
Starting diskcheck:                                        [  OK  ]


とすれば、起動してくれます。

こちらの方がプロセス番号を見てkillしたり、pidファイルを掃除する必要も無いので楽でしょうね。


サーバー起動時に自動起動させたければ、


# chkconfig --add diskcheck
# chkconfig --level 35 diskcheck on
# chkconfig --list | grep diskcheck
diskcheck       0:off   1:off   2:off   3:on    4:off   5:on    6:off


としておけば、幸せかもしれません。