Аннотация: В этой статье давайте обсудим множество методов и мер предосторожности при создании базового механизма мониторинга на стороне сервера на основе языка PHP и предоставим полную реализацию исходного кода.
1. Проблема изменения рабочего каталога.
Когда вы пишете программу мониторинга, обычно лучше предоставить ей собственный рабочий каталог. Таким образом, если вы используете относительный путь для чтения и записи файлов, он автоматически определит место, где пользователь ожидает сохранить файл, в зависимости от ситуации. Хотя всегда полезно ограничивать пути, используемые в программе, это теряет ту гибкость, которую заслуживает; Поэтому самый безопасный способ изменить рабочий каталог — использовать chdir() и chroot().
chroot() можно использовать в версиях PHP CLI и CGI, но для этого требуется, чтобы программа запускалась с правами root. chroot() фактически изменяет путь текущего процесса из корневого каталога в указанный каталог. Это позволяет текущему процессу выполнять только файлы, существующие в этом каталоге. Часто chroot() используется серверами как «устройство безопасности», гарантирующее, что вредоносный код не изменит файлы за пределами определенного каталога. Имейте в виду, что хотя chroot() запрещает вам доступ к любым файлам за пределами вашего нового каталога, к любым открытым в данный момент файловым ресурсам все равно можно получить доступ. Например, следующий код может открыть файл журнала, вызвать chroot() и переключиться на каталог данных, при этом сохраняя возможность успешно войти в систему и открыть файловый ресурс:
<?php;
$logfile = fopen("/var/log/chroot.log", "w");
chroot("/Пользователи/Джордж");
fputs($logfile, "Привет изнутри Chrootn");
?>
Если приложение не может использовать chroot(), вы можете вызвать chdir(), чтобы установить рабочий каталог. Это полезно, например, когда коду необходимо загрузить определенный код, который может находиться в любом месте системы. Обратите внимание, что chdir() не предоставляет механизма безопасности для предотвращения несанкционированного открытия файлов.
2. Отказ от привилегий
При написании демонов Unix классической мерой безопасности является отказ от всех ненужных привилегий, в противном случае наличие ненужных привилегий может легко привести к ненужным проблемам. В случае уязвимостей в коде (или самом PHP) ущерб часто можно свести к минимуму, обеспечив запуск демона от имени пользователя с наименьшими привилегиями.
Один из способов добиться этого — запустить демон от имени непривилегированного пользователя. Однако этого обычно недостаточно, если программе необходимо изначально открыть ресурсы, на открытие которых у непривилегированных пользователей нет разрешения (например, файлы журналов, файлы данных, сокеты и т. д.).
Если вы работаете под root-правами, вы можете отказаться от своих привилегий с помощью функций posix_setuid() и posiz_setgid(). В следующем примере привилегии запущенной в данный момент программы изменяются на привилегии пользователя none:
$pw=posix_getpwnam('nobody');
posix_setuid($pw['uid']);
posix_setgid($pw['gid']);
Как и в случае с chroot(), любые привилегированные ресурсы, которые были открыты до отказа от привилегий, останутся открытыми, но новые ресурсы не могут быть созданы.
3. Гарантия эксклюзивности.
Часто вам может потребоваться добиться: в любой момент времени запускается только один экземпляр скрипта. Это особенно важно для защиты сценариев, поскольку их запуск в фоновом режиме может легко привести к случайному вызову нескольких экземпляров.
Стандартный метод обеспечения этой эксклюзивности состоит в том, чтобы сценарий заблокировал определенный файл (часто заблокированный файл и используется исключительно) с помощью flock(). Если блокировка не удалась, сценарий должен вывести ошибку и завершить работу. Вот пример:
$fp=fopen("/tmp/.lockfile","a");
if(!$fp || !flock($fp, LOCK_EX | LOCK_NB)) {
fputs(STDERR, «Не удалось получить блокировкуn»);
Выход;
}
/*Успешно заблокирован для безопасного выполнения работы*/
Обратите внимание, что обсуждение механизма блокировки требует большего содержания и не будет здесь объясняться.
4. Создание службы мониторинга
. В этом разделе мы будем использовать PHP для написания базового механизма мониторинга. Поскольку вы не будете знать заранее, как его изменить, вам следует сделать его реализацию гибкой и возможной.
Средство ведения журнала должно поддерживать проверку произвольных служб (например, служб HTTP и FTP) и регистрировать события любым способом (по электронной почте, вывод в файл журнала и т. д.). Конечно, вы хотите, чтобы он работал как демон, поэтому вам следует попросить его вывести полное текущее состояние;
Службе необходимо реализовать следующий абстрактный класс:
абстрактный класс ServiceCheck {
константный ОШИБКА = 0;
константный УСПЕХ = 1;
защищенный $таймаут = 30;
защищенный $next_attempt;
защищенный $current_status = ServiceCheck::SUCCESS;
защищенный $previous_status = ServiceCheck::SUCCESS;
защищенная частота $ = 30;
защищенное $описание;
защищенный $consecutive_failures = 0;
защищенный $status_time;
защищенный $failure_time;
защищенные $loggers = массив();
абстрактная публичная функция __construct($params);
публичная функция __call($name, $args)
{
if(isset($this->$name)) {
вернуть $this->$name;
}
}
публичная функция set_next_attempt()
{
$this->next_attempt = время() + $this->частота;
}
публичная абстрактная функция 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 = время();
}
}
еще {
$this->consecutive_failures = 0;
}
$this->status_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;
}
}
Перегруженный метод __call(), приведенный выше, обеспечивает доступ только для чтения к параметрам объекта ServiceCheck:
· таймаут — на какое время можно приостановить эту проверку, прежде чем механизм завершит проверку.
· next_attempt – следующая попытка подключения к серверу.
· current_status – Текущий статус услуги: УСПЕХ или НЕУДАЧА.
· previous_status – статус перед текущим статусом.
· частота - как часто проверять сервис.
· описание - описание услуги.
· последовательные_фаилуры — количество последовательных неудачных проверок службы с момента последнего успеха.
· status_time – время последней проверки службы.
· error_time – если статус FAILED, он представляет время, когда произошел сбой.
Этот класс также реализует шаблон Observer, позволяющий объектам типа ServiceLogger регистрироваться и затем вызывать его при вызове log_current_status() или log_service_event().
Ключевой функцией, реализованной здесь, является run(), которая отвечает за определение того, как должна выполняться проверка. Если проверка успешна, она должна вернуть SUCCESS, в противном случае она должна вернуть FAILURE;
Когда проверка службы, определенная в run(), возвращается, вызывается метод post_run(). Он отвечает за установку состояния объекта и реализацию логирования.
Интерфейс ServiceLogger: при указании класса журнала необходимо реализовать только два метода: log_service_event() и log_current_status(), которые вызываются, когда возвращается проверка run() и когда реализуется обычный запрос состояния.
Интерфейс выглядит следующим образом:
интерфейс ServiceLogger {
общедоступная функция log_service_event(ServiceCheck$service);
общедоступная функция log_current_status(ServiceCheck$service);
}
Наконец, вам нужно написать сам движок. Идея аналогична той, которая использовалась при написании простой программы в предыдущем разделе: сервер должен создать новый процесс для обработки каждой проверки и использовать обработчик SIGCHLD для обнаружения возвращаемого значения после завершения проверки. Максимальное количество, которое можно проверить одновременно, должно быть настраиваемым, что предотвращает чрезмерное использование системных ресурсов. Все службы и журналы будут определены в файле XML.
Вот класс ServiceCheckRunner, определяющий этот механизм:
class ServiceCheckRunner {
частный $num_children;
частные $services = массив();
частный $дети = массив();
публичная функция _ _construct($conf, $num_children)
{
$loggers = массив();
$this->num_children = $num_children;
$conf = simplexml_load_file($conf);
foreach($conf->loggers->logger как $logger) {
$class = новый Reflection_Class("$logger->class");
if($class->isInstantiable()) {
$loggers["$logger->id"] = $class->newInstance();
}
еще {
fputs(STDERR, "Невозможно создать экземпляр {$logger->class}.n");
Выход;
}
}
foreach($conf->services->service as $service) {
$class = новый Reflection_Class("$service->class");
if($class->isInstantiable()) {
$item = $class->newInstance($service->params);
foreach($service->loggers->logger как $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())? -1: 1;
}
частная функция next(){
usort($this->services, array($this, 'next_attempt_sort'));
вернуть $this->services[0];
}
публичный цикл функции(){
объявить (тикс = 1);
pcntl_signal(SIGCHLD, array($this, "sig_child"));
pcntl_signal(SIGUSR1, array($this, "sig_usr1"));
в то время как (1) {
$сейчас = время();
if(count($this->children)< $this->num_children) {
$service = $this->next();
if($now < $service->next_attempt()) {
спать(1);
продолжать;
}
$service->set_next_attempt();
если ($ pid = pcntl_fork ()) {
$this->дети[$pid] = $service;
}
еще {
pcntl_alarm($service->timeout());
выход ($ сервис-> запуск());
}
}
}
}
общественная функция 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-файл, создает все службы, подлежащие мониторингу, и создает средство ведения журнала для их записи.
Метод цикла() является основным методом в этом классе. Он устанавливает обработчик сигнала запроса и проверяет, можно ли создать новый дочерний процесс. Теперь, если следующее событие (упорядоченное по времени next_attempt CHUO) пройдет успешно, будет создан новый процесс. В этом новом дочернем процессе выдайте предупреждение, чтобы продолжительность теста не превысила его лимит, а затем выполните тест, определенный с помощью run().
Также имеется два обработчика сигналов: обработчик SIGCHLD sig_child(), который отвечает за сбор завершенных дочерних процессов и выполнение метода post_run() их службы; обработчик SIGUSR1 sig_usr1(), который просто вызывает метод log_current_status() всех зарегистрированных регистраторов, который может использоваться для получения текущего состояния всей системы.
Конечно, такая архитектура наблюдения не дает ничего практического. Но сначала вам нужно проверить услугу. Следующий класс проверяет, получаете ли вы ответ «200 Server OK» от HTTP-сервера:
class HTTP_ServiceCheck расширяет ServiceCheck{
публичный $url;
общественная функция _ _construct($params){
foreach($params as $k => $v) {
$к = "$к";
$this->$k = "$v";
}
}
публичная функция run(){
if(is_resource(@fopen($this->url, "r"))) {
вернуть ServiceCheck::SUCCESS;
}
еще {
вернуть ServiceCheck::FAILURE;
}
}
}
По сравнению с фреймворками, которые вы создавали ранее, этот сервис чрезвычайно прост и не будет здесь подробно описываться.
5. Пример процесса ServiceLogger
Ниже приведен пример процесса ServiceLogger. Когда служба не работает, она отвечает за отправку электронного письма дежурному:
класс EmailMe_ServiceLogger реализует 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){
возвращаться;
}
}
Если пять раз подряд происходит сбой, процесс также отправляет сообщение на резервный адрес. Обратите внимание, что он не реализует значимого метода log_current_status().
Всякий раз, когда вы меняете состояние службы следующим образом, вам следует реализовать процесс ServiceLogger, который записывает в журнал ошибок PHP:
класс ErrorLog_ServiceLogger реализует ServiceLogger {
общедоступная функция log_service_event(ServiceCheck$service)
{
if($service->current_status() !==$service->previous_status()) {
if($service->current_status() ===ServiceCheck::FAILURE) {
$status = 'ВНИЗ';
}
еще {
$статус = 'ВВЕРХ';
}
error_log("{$service->description()} изменил статус на $status");
}
}
общедоступная функция log_current_status(ServiceCheck$service)
{
error_log("{$service->description()}: $status");
}
}
Метод log_current_status() означает, что если процесс отправляет сигнал SIGUSR1, он копирует свой полный текущий статус в ваш журнал ошибок PHP.
Движок использует файл конфигурации следующим образом:
<config>
<логгеры>
<логгер>
<id>журнал ошибок</id>
<класс>ErrorLog_ServiceLogger</класс>
</логгер>
<логгер>
<id>электронная почта</id>
<класс>EmailMe_ServiceLogger</класс>
</логгер>
</логгеры>
<услуги>
<сервис>
<класс>HTTP_ServiceCheck</класс>
<параметры>
<описание>Проверка HTTP OmniTI</description>
<url> http://www.omniti.com </url>
<таймаут>30</таймаут>
<частота>900</частота>
</параметры>
<логгеры>
<логгер>журнал</логгер>
<logger>электронная почта</logger>
</логгеры>
</услуга>
<сервис>
<класс>HTTP_ServiceCheck</класс>
<параметры>
<описание>Проверка HTTP на домашней странице</description>
<url> http://www.schlossnagle.org/~george </url>
<таймаут>30</таймаут>
<частота>3600</частота>
</параметры>
<логгеры>
<логгер>журнал</логгер>
</логгеры>
</услуга>
</услуги>
</config>
При передаче этого XML-файла конструктор ServiceCheckRunner создает экземпляр программы ведения журнала для каждого указанного журнала. Затем он создает экземпляр объекта ServiceCheck, соответствующего каждой указанной службе.
Обратите внимание, что конструктор использует класс Reflection_Class для реализации внутренних проверок классов службы и журналирования — прежде чем вы попытаетесь создать их экземпляр. Хотя в этом нет необходимости, это прекрасно демонстрирует использование нового API Reflection в PHP 5. В дополнение к этим классам API Reflection предоставляет классы для реализации внутренней проверки практически любого внутреннего объекта (класса, метода или функции) в PHP.
Чтобы использовать созданный вами движок, вам все равно понадобится код-оболочка. Сторожевой таймер должен предотвратить попытку запуска его дважды — вам не нужно создавать два сообщения для каждого события. Конечно, монитор также должен получить некоторые опции, в том числе:
Описание опций.
[-f] Местоположение файла конфигурации механизма. По умолчанию используется файл Monitor.xml.
[-n] Размер пула дочерних процессов, разрешенный движком. По умолчанию — 5.
[-d] Флаг, отключающий функциональность демона этого движка. Это полезно при написании процесса отладки ServiceLogger, который выводит информацию на стандартный вывод или стандартный поток ошибок.
Вот окончательный сценарий наблюдения, который анализирует параметры, обеспечивает эксклюзивность и запускает проверки обслуживания:
require_once "Service.inc";
require_once "Консоль/Getopt.php";
$shorttoptions = "n:f:d";
$default_opts = array('n' => 5, 'f' =>'monitor.xml');
$args = getOptions($default_opts, $shorttoptions, null);
$fp = fopen("/tmp/.lockfile", "a");
if(!$fp || !flock($fp, LOCK_EX | LOCK_NB)) {
fputs($stderr, "Не удалось получить блокировкуn");
Выход;
}
if(!$args['d']) {
если (pcntl_fork()) {
Выход;
}
posix_setsid();
если (pcntl_fork()) {
Выход;
}
}
fwrite($fp, getmypid());
fflush($fp);
$engine = новый ServiceCheckRunner($args['f'], $args['n']);
$engine->loop();
Обратите внимание, что в этом примере используется настроенная функция getOptions().
После написания соответствующего файла конфигурации вы можете запустить сценарий следующим образом:
> ./monitor.php -f /etc/monitor.xml
Это защищает и продолжает мониторинг до тех пор, пока машина не будет выключена или сценарий не будет уничтожен.
Этот сценарий довольно сложен, но есть еще некоторые области, которые можно легко улучшить, и которые мы оставим читателю в качестве упражнения:
· Добавьте обработчик SIGHUP, который повторно анализирует файл конфигурации, чтобы вы могли изменить конфигурацию, не запуская сервер.
· Напишите ServiceLogger, который может входить в базу данных для хранения данных запроса.
· Написать программу веб-интерфейса, обеспечивающую хороший графический интерфейс для всей системы мониторинга.