要約: この記事では、PHP 言語に基づいて基本的なサーバー側監視エンジンを構築するための多くのテクニックと注意事項について説明し、完全なソース コードの実装を示します。
1. 作業ディレクトリの変更の問題
監視プログラムを作成するときは、通常、独自の作業ディレクトリを設定させる方が良いでしょう。このように、ファイルの読み取りおよび書き込みに相対パスを使用すると、状況に基づいてユーザーがファイルの保存を期待する場所が自動的に処理されます。プログラムで使用されるパスを常に制限することは良い習慣ですが、それに値する柔軟性が失われます。したがって、作業ディレクトリを変更する最も安全な方法は、chdir() と chroot() の両方を使用することです。
chroot() は PHP の CLI および CGI バージョンで使用できますが、プログラムを root 権限で実行する必要があります。 chroot() は実際に、現在のプロセスのパスをルート ディレクトリから指定されたディレクトリに変更します。これにより、現在のプロセスはそのディレクトリに存在するファイルのみを実行できるようになります。多くの場合、chroot() は、悪意のあるコードが特定のディレクトリの外側にあるファイルを変更しないようにするための「セキュリティ デバイス」としてサーバーによって使用されます。 chroot() を使用すると、新しいディレクトリの外部にあるファイルにはアクセスできなくなりますが、現在開いているファイル リソースには引き続きアクセスできることに注意してください。たとえば、次のコードはログ ファイルを開いて chroot() を呼び出し、データ ディレクトリに切り替えても、正常にログインしてファイル リソースを開くことができます
。
$logfile = fopen("/var/log/chroot.log", "w");
chroot("/ユーザー/ジョージ");
fputs($logfile, "Chroot 内からこんにちはn");
?>
アプリケーションが chroot() を使用できない場合は、chdir() を呼び出して作業ディレクトリを設定できます。これは、たとえば、システム内のどこにでも配置できる特定のコードをコードで読み込む必要がある場合に便利です。 chdir() には、ファイルの不正なオープンを防止するセキュリティ メカニズムが提供されていないことに注意してください。
2. 特権を放棄する
Unix デーモンを作成する場合、古典的なセキュリティ予防策は、不必要な特権をすべて放棄させることです。そうしないと、不必要な特権を持つと不要な問題が発生しやすくなります。コード (または PHP 自体) に脆弱性がある場合、多くの場合、デーモンを最小特権ユーザーとして実行することによって被害を最小限に抑えることができます。
これを実現する 1 つの方法は、権限のないユーザーとしてデーモンを実行することです。ただし、特権のないユーザーが開く権限を持たないリソース (ログ ファイル、データ ファイル、ソケットなど) をプログラムが最初に開く必要がある場合、これでは通常十分ではありません。
root として実行している場合は、posix_setuid() 関数と posiz_setgid() 関数を使用して特権を放棄できます。次の例では、現在実行中のプログラムの権限を、ユーザーnobody が所有する権限に変更します。
$pw=posix_getpwnam('nobody');
posix_setuid($pw['uid']);
posix_setgid($pw['gid']);
chroot() と同様に、特権を放棄する前に開かれていた特権付きリソースは開いたままになりますが、新しいリソースは作成できません。
3. 排他性の保証
スクリプトのインスタンスを常に 1 つだけ実行することを実現したい場合があります。スクリプトをバックグラウンドで実行すると、誤って複数のインスタンスを呼び出してしまう可能性があるため、これはスクリプトを保護するために特に重要です。
この排他性を確保するための標準的な手法は、flock() を使用してスクリプトに特定のファイル (多くの場合、ロックされたファイルであり、排他的に使用されます) をロックさせることです。ロックが失敗した場合、スクリプトはエラーを出力して終了する必要があります。以下に例を示します。
$fp=fopen("/tmp/.lockfile","a");
if(!$fp || !flock($fp, LOCK_EX | LOCK_NB)) {
fputs(STDERR, "ロックの取得に失敗しましたn");
出口;
}
/*作業を安全に実行するために正常にロックされました*/
ロック メカニズムの説明にはさらに多くの内容が含まれるため、ここでは説明しないことに注意してください。
4. 監視サービスの構築
このセクションでは、PHP を使用して基本的な監視エンジンを作成します。変更方法が事前にわからないため、実装を柔軟かつ可能にする必要があります。
ロガーは、任意のサービス検査 (HTTP サービスや FTP サービスなど) をサポートでき、あらゆる方法 (電子メール、ログ ファイルへの出力など) でイベントを記録できる必要があります。もちろん、デーモンとして実行したいので、完全な現在の状態を出力するように要求する必要があります。
サービスは次の抽象クラスを実装する必要があります:
abstract class ServiceCheck {
const 失敗 = 0;
const 成功 = 1;
保護された $timeout = 30;
保護された$next_attempt;
protected $current_status = ServiceCheck::SUCCESS;
protected $previous_status = ServiceCheck::SUCCESS;
保護された $frequency = 30;
保護された $description;
protected $consecutive_failures = 0;
保護された $status_time;
保護された $failure_time;
$loggers = array();
抽象パブリック関数 __construct($params);
パブリック関数 __call($name, $args)
{
if(isset($this->$name)) {
$this->$name を返します。
}
}
パブリック関数 set_next_attempt()
{
$this->next_attempt = time() + $this->frequency;
}
パブリック抽象関数 run();
パブリック関数 post_run($status)
{
if($status !== $this->current_status) {
$this->previous_status = $this->current_status;
}
if($status === self::FAILURE) {
if( $this->current_status === self::FAILURE ) {
$this->consecutive_failures++;
}
それ以外 {
$this->failure_time = time();
}
}
それ以外 {
$this->consecutive_failures = 0;
}
$this->status_time = time();
$this->current_status = $status;
$this->log_service_event();
}
パブリック関数 log_current_status()
{
foreach($this->loggers as $logger) {
$logger->log_current_status($this);
}
}
プライベート関数 log_service_event()
{
foreach($this->loggers as $logger) {
$logger->log_service_event($this);
}
}
パブリック関数 register_logger(ServiceLogger $logger)
{
$this->loggers[] = $logger;
}
、
ServiceCheck オブジェクトのパラメーターへの読み取り専用アクセスを提供します。
· timeout - エンジンがチェックを終了するまで、このチェックを中断できる時間。
· next_attempt - 次回サーバーへの接続を試行する時間。
· current_status - サービスの現在のステータス: SUCCESS または FAILURE。
·Previous_status - 現在のステータスの前のステータス。
· 頻度 - サービスをチェックする頻度。
· 説明 - サービスの説明。
·Continuous_failures - 前回の成功以降の連続したサービス チェック失敗の数。
· status_time - サービスが最後にチェックされた時刻。
· Failure_time - ステータスが FAILED の場合、障害が発生した時間を表します。
このクラスは Observer パターンも実装しており、ServiceLogger 型のオブジェクトが自身を登録し、log_current_status() または log_service_event() が呼び出されたときにそれを呼び出すことができます。
ここで実装されている主要な関数は run() で、チェックの実行方法を定義します。チェックが成功した場合は SUCCESS を返し、それ以外の場合は FAILURE を返します。
run() で定義されたサービス チェックが返されると、post_run() メソッドが呼び出されます。オブジェクトの状態を設定し、ログを実装する役割を果たします。
ServiceLogger インターフェイス: ログ クラスを指定するには、log_service_event() と log_current_status() という 2 つのメソッドを実装するだけで済みます。これらのメソッドは、run() チェックが返されたときと、通常のステータス要求が実装されたときに呼び出されます。
インターフェイスは次のとおりです
。
パブリック関数 log_service_event(ServiceCheck$service);
パブリック関数 log_current_status(ServiceCheck$service);
最後
に、エンジン自体を作成する必要があります。この考え方は、前のセクションで単純なプログラムを作成するときに使用したものと似ています。サーバーは、各チェックを処理する新しいプロセスを作成し、SIGCHLD ハンドラを使用してチェックが完了したときの戻り値を検出する必要があります。同時にチェックできる最大数は構成可能である必要があり、これによりシステム リソースの過剰な使用が防止されます。すべてのサービスとログは XML ファイルで定義されます。
このエンジンを定義する ServiceCheckRunner クラスは次のとおりです。
class ServiceCheckRunner {
プライベート $num_children;
プライベート $services = array();
プライベート $children = array();
パブリック関数 _ _construct($conf, $num_children)
{
$ロガー = 配列();
$this->num_children = $num_children;
$conf = simplexml_load_file($conf);
foreach($conf->loggers->logger as $logger) {
$class = new Reflection_Class("$logger->class");
if($class->isInstantiable()) {
$loggers["$logger->id"] = $class->newInstance();
}
それ以外 {
fputs(STDERR, "{$logger->class} をインスタンス化できません。n");
出口;
}
}
foreach($conf->services->service as $service) {
$class = new Reflection_Class("$service->class");
if($class->isInstantiable()) {
$item = $class->newInstance($service->params);
foreach($service->loggers->logger as $logger) {
$item->register_logger($loggers["$logger"]);
}
$this->services[] = $item;
}
それ以外 {
fputs(STDERR, "{$service->class} はインスタンス化できません。n");
出口;
}
}
}
プライベート関数 next_attempt_sort($a, $b){
if($a->next_attempt() == $b->next_attempt()) {
0を返します。
}
return ($a->next_attempt() < $b->next_attempt())?
}
プライベート関数 next(){
usort($this->services, array($this, 'next_attempt_sort'));
$this->services[0] を返します。
}
パブリック関数loop(){
宣言(ティック=1);
pcntl_signal(SIGCHLD, array($this, "sig_child"));
pcntl_signal(SIGUSR1, array($this, "sig_usr1"));
while(1) {
$now = 時間();
if(count($this->children)< $this->num_children) {
$service = $this->next();
if($now < $service->next_attempt()) {
スリープ(1);
続く;
}
$service->set_next_attempt();
if($pid = pcntl_fork()) {
$this->children[$pid] = $service;
}
それ以外 {
pcntl_alarm($service->timeout());
exit($service->run());
}
}
}
}
パブリック関数 log_current_status(){
foreach($this->services as $service) {
$service->log_current_status();
}
}
プライベート関数 sig_child($signal){
$status = ServiceCheck::FAILURE;
pcntl_signal(SIGCHLD, array($this, "sig_child"));
while(($pid = pcntl_wait($status, WNOHANG)) > 0){
$service = $this->children[$pid];
unset($this->children[$pid]);
if(pcntl_wifexited($status) && pcntl_wexitstatus($status) ==ServiceCheck::SUCCESS)
{
$status = ServiceCheck::SUCCESS;
}
$service->post_run($status);
}
}
プライベート関数 sig_usr1($signal){
pcntl_signal(SIGUSR1, array($this, "sig_usr1"));
$this->log_current_status();
}
これ
は非常に複雑なクラスです。そのコンストラクターは XML ファイルを読み取って解析し、監視対象のすべてのサービスを作成し、それらを記録するロガーを作成します。
LOOP() メソッドは、このクラスのメイン メソッドです。リクエストのシグナルハンドラーを設定し、新しい子プロセスを作成できるかどうかを確認します。ここで、次のイベント (next_attempt time CHUO 順) が正常に実行されると、新しいプロセスが作成されます。この新しい子プロセス内で、テスト期間が制限時間を超えないように警告を発行し、run() で定義されたテストを実行します。
また、2 つのシグナル ハンドラーもあります。 SIGCHLD ハンドラー sig_child() は、終了した子プロセスを収集し、そのサービスの post_run() メソッドを実行します。 SIGUSR1 ハンドラー sig_usr1() は、単に登録されているすべてのロガーの log_current_status() メソッドを呼び出します。システム全体の現在のステータスを取得するために使用できます。
もちろん、この監視アーキテクチャは実際的なことは何も行いません。ただし、まずサービスを確認する必要があります。次のクラスは、HTTP サーバーから「200 Server OK」応答を取得しているかどうかをチェックします。
class HTTP_ServiceCheck extends ServiceCheck{
パブリック $url;
パブリック関数 _ _construct($params){
foreach($params as $k => $v) {
$k = "$k";
$this->$k = "$v";
}
}
パブリック関数 run(){
if(is_resource(@fopen($this->url, "r"))) {
ServiceCheck::SUCCESS を返します。
}
それ以外 {
ServiceCheck::FAILURE を返します。
}
}
比較
すると、このサービスは非常にシンプルなので、ここでは詳しく説明しません。
5. ServiceLogger プロセスのサンプル
次に、ServiceLogger プロセスのサンプルを示します。サービスがダウンすると、オンコール担当者に電子メールを送信する責任があります。
class EmailMe_ServiceLoggerimplements ServiceLogger {
パブリック関数 log_service_event(ServiceCheck$service)
{
if($service->current_status ==ServiceCheck::FAILURE) {
$message = "{$service->description()} に問題がありますrn";
mail( '[email protected]' , 'サービスイベント', $message);
if($service->consecutive_failures()> 5) {
mail( '[email protected]' , 'サービスイベント', $message);
}
}
}
パブリック関数 log_current_status(ServiceCheck$service){
戻る;
}
5 回連続で失敗した場合、プロセスはバックアップ アドレスにもメッセージを送信します
。
意味のある log_current_status() メソッドは実装されていないことに注意してください。
次のようにサービスの状態を変更するときは常に、PHP エラー ログに書き込む ServiceLogger プロセスを実装する必要があります
。
パブリック関数 log_service_event(ServiceCheck$service)
{
if($service->current_status() !==$service->previous_status()) {
if($service->current_status() ===ServiceCheck::FAILURE) {
$status = 'DOWN';
}
それ以外 {
$status = 'UP';
}
error_log("{$service->description()} のステータスが $status に変更されました");
}
}
パブリック関数 log_current_status(ServiceCheck$service)
{
error_log("{$service->description()}: $status");
}
、
プロセスが SIGUSR1 シグナルを送信すると、その完全な現在のステータスが PHP エラー ログにコピーされることを意味します。
エンジンは次のように構成ファイルを使用します:
<config>
<ロガー>
<ロガー>
<id>エラーログ</id>
<クラス>ErrorLog_ServiceLogger</クラス>
</ロガー>
<ロガー>
<id>メールアドレス</id>
<クラス>EmailMe_ServiceLogger</クラス>
</ロガー>
</ロガー>
<サービス>
<サービス>
<クラス>HTTP_ServiceCheck</クラス>
<パラメータ>
<説明>OmniTI HTTP チェック</説明>
<url> http://www.omniti.com </url>
<タイムアウト>30</タイムアウト>
<周波数>900</周波数>
</params>
<ロガー>
<ロガー>エラーログ</ロガー>
<ロガー>メールアドレス</ロガー>
</ロガー>
</サービス>
<サービス>
<クラス>HTTP_ServiceCheck</クラス>
<パラメータ>
<説明>ホームページHTTPチェック</説明>
<url> http://www.schlossnagle.org/~george </url>
<タイムアウト>30</タイムアウト>
<周波数>3600</周波数>
</params>
<ロガー>
<ロガー>エラーログ</ロガー>
</ロガー>
</サービス>
</サービス>
</config>
この XML ファイルが渡されると、ServiceCheckRunner のコンストラクターは、指定されたログごとにログ プログラムをインスタンス化します。次に、指定された各サービスに対応する ServiceCheck オブジェクトをインスタンス化します。
コンストラクターは、インスタンス化を試行する前に、Reflection_Class クラスを使用して、サービスおよびログ クラスの内部チェックを実装することに注意してください。これは不要ですが、PHP 5 の新しい Reflection API の使用法をうまく示しています。これらのクラスに加えて、Reflection API は、PHP のほぼすべての内部エンティティ (クラス、メソッド、または関数) の組み込み検査を実装するためのクラスを提供します。
構築したエンジンを使用するには、いくつかのラッパー コードが必要です。ウォッチドッグは、ウォッチドッグを 2 回開始しようとすることを防止する必要があります。イベントごとに 2 つのメッセージを作成する必要はありません。もちろん、モニターには次のようなオプションも表示される必要があります。
オプションの説明
[-f] エンジン構成ファイルの場所。デフォルトはmonitor.xmlです。
[-n] エンジンによって許可される子プロセス プールのサイズ。デフォルトは 5 です。
[-d] このエンジンのデーモン機能を無効にするフラグ。これは、情報を stdout または stderr に出力するデバッグ ServiceLogger プロセスを作成する場合に便利です。
これは、オプションを解析し、排他性を確保し、サービス チェックを実行する最終的なウォッチドッグ スクリプトです
。
require_once "コンソール/Getopt.php";
$shortoptions = "n:f:d";
$default_opts = array('n' => 5, 'f' =>'monitor.xml');
$args = getOptions($default_opts, $shortoptions, null);
$fp = fopen("/tmp/.lockfile", "a");
if(!$fp || !flock($fp, LOCK_EX | LOCK_NB)) {
fputs($stderr, "ロックの取得に失敗しましたn");
出口;
}
if(!$args['d']) {
if(pcntl_fork()) {
出口;
}
posix_setsid();
if(pcntl_fork()) {
出口;
}
}
fwrite($fp, getmypid());
fflush($fp);
$engine = new ServiceCheckRunner($args['f'], $args['n']);
$engine->loop();
この例では、カスタマイズされた getOptions() 関数が使用されていることに注意してください。
適切な設定ファイルを作成した後、次のようにスクリプトを開始できます。
> ./monitor.php -f /etc/monitor.xml
これにより、マシンがシャットダウンされるかスクリプトが強制終了されるまで、保護され、監視が継続されます。
このスクリプトは非常に複雑ですが、簡単に改善できる部分がいくつかありますので、読者の演習として残しておきます。
· サーバーを起動せずに構成を変更できるように、構成ファイルを再分析する SIGHUP ハンドラーを追加します。
· データベースにログインしてクエリ データを保存できる ServiceLogger を作成します。
· 監視システム全体に優れた GUI を提供する Web フロントエンド プログラムを作成します。