개요: 이 기사에서는 PHP 언어를 기반으로 하는 기본 서버 측 모니터링 엔진을 구축하기 위한 많은 기술과 주의 사항에 대해 논의하고 완전한 소스 코드 구현을 제공합니다.
1. 작업 디렉터리 변경 문제
모니터링 프로그램을 작성할 때 일반적으로 자체 작업 디렉터리를 설정하도록 하는 것이 더 좋습니다. 이런 식으로 상대 경로를 사용하여 파일을 읽고 쓰는 경우 상황에 따라 사용자가 파일이 저장될 것으로 예상하는 위치를 자동으로 처리합니다. 그러나 프로그램에 사용되는 경로를 항상 제한하는 것이 좋지만 그렇게 하면 그에 따른 유연성이 손실됩니다. 따라서 작업 디렉토리를 변경하는 가장 안전한 방법은 chdir()과 chroot()를 모두 사용하는 것입니다.
chroot()는 PHP의 CLI 및 CGI 버전에서 사용할 수 있지만 프로그램을 루트 권한으로 실행해야 합니다. chroot()는 실제로 현재 프로세스의 경로를 루트 디렉터리에서 지정된 디렉터리로 변경합니다. 이를 통해 현재 프로세스는 해당 디렉터리에 존재하는 파일만 실행할 수 있습니다. chroot()는 악성 코드가 특정 디렉터리 외부의 파일을 수정하지 못하도록 하기 위해 서버에서 "보안 장치"로 사용되는 경우가 많습니다. chroot()를 사용하면 새 디렉터리 외부의 파일에 액세스할 수 없지만 현재 열려 있는 파일 리소스에는 계속 액세스할 수 있습니다. 예를 들어, 다음 코드는 로그 파일을 열고 chroot()를 호출한 후 데이터 디렉토리로 전환한 후에도 성공적으로 로그인하여 파일 리소스를 열 수 있습니다
.
$logfile = fopen("/var/log/chroot.log", "w");
chroot("/사용자/조지");
fputs($logfile, "Chroot 내부에서 안녕하세요n");
?>
응용 프로그램이 chroot()를 사용할 수 없는 경우 chdir()을 호출하여 작업 디렉터리를 설정할 수 있습니다. 예를 들어, 코드가 시스템의 어느 위치에나 있을 수 있는 특정 코드를 로드해야 하는 경우에 유용합니다. chdir()은 무단 파일 열기를 방지하는 보안 메커니즘을 제공하지 않습니다.
2. 권한 포기
Unix 데몬을 작성할 때 일반적인 보안 예방 조치는 불필요한 권한을 모두 포기하도록 하는 것입니다. 그렇지 않으면 불필요한 권한을 가지면 불필요한 문제가 쉽게 발생할 수 있습니다. 코드(또는 PHP 자체)에 취약점이 있는 경우 데몬이 최소 권한을 가진 사용자로 실행되도록 하면 피해를 최소화할 수 있는 경우가 많습니다.
이를 달성하는 한 가지 방법은 권한이 없는 사용자로 데몬을 실행하는 것입니다. 그러나 권한이 없는 사용자가 열 수 있는 권한이 없는 리소스(예: 로그 파일, 데이터 파일, 소켓 등)를 프로그램이 처음에 열어야 하는 경우 일반적으로 이 방법으로는 충분하지 않습니다.
루트로 실행 중인 경우 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 {
const 실패 = 0;
const 성공 = 1;
보호된 $timeout = 30;
보호된 $next_attempt;
protected $current_status = ServiceCheck::SUCCESS;
protected $previous_status = ServiceCheck::SUCCESS;
보호된 $주파수 = 30;
보호된 $설명;
보호된 $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 = 시간() + $this->주파수;
}
공개 추상 함수 run();
공개 함수 post_run($status)
{
if($status !== $this->current_status) {
$this->previous_status = $this->현재_상태;
}
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->로거를 $logger로 사용) {
$logger->log_current_status($this);
}
}
개인 함수 log_service_event()
{
foreach($this->로거를 $logger로 사용) {
$logger->log_service_event($this);
}
}
공용 함수 Register_logger(ServiceLogger $logger)
{
$this->loggers[] = $logger;
}
}
위의 __call() 오버로드된 메소드는 ServiceCheck 객체의 매개변수에 대한 읽기 전용 액세스를 제공합니다.
· 시간 제한 - 엔진이 검사를 종료하기 전에 이 검사를 일시 중단할 수 있는 기간입니다.
· next_attempt - 다음에 서버에 연결을 시도하는 시간입니다.
· current_status - 서비스의 현재 상태: SUCCESS 또는 FAILURE.
· 이전_상태 - 현재 상태 이전의 상태입니다.
· 빈도 - 서비스를 확인하는 빈도입니다.
· 설명 - 서비스 설명입니다.
· 연속_실패 - 마지막 성공 이후 연속적인 서비스 확인 실패 횟수입니다.
· status_time - 서비스가 마지막으로 확인된 시간입니다.
· failure_time - 상태가 FAILED인 경우 실패가 발생한 시간을 나타냅니다.
또한 이 클래스는 관찰자 패턴을 구현하여 ServiceLogger 유형의 객체가 스스로 등록한 다음 log_current_status() 또는 log_service_event()가 호출될 때 이를 호출할 수 있도록 합니다.
여기에 구현된 주요 함수는 run()이며, 검사 수행 방법을 정의하는 역할을 담당합니다. 검사가 성공하면 SUCCESS를 반환해야 하며, 그렇지 않으면 FAILURE를 반환해야 합니다.
run()에 정의된 서비스 검사가 반환되면 post_run() 메서드가 호출됩니다. 객체의 상태를 설정하고 로깅을 구현하는 일을 담당합니다.
ServiceLogger 인터페이스: 로그 클래스를 지정하려면 run() 검사가 반환될 때와 일반 상태 요청이 구현될 때 호출되는 log_service_event() 및 log_current_status()라는 두 가지 메서드만 구현하면 됩니다.
인터페이스는 다음과 같습니다:
인터페이스 ServiceLogger {
공개 함수 log_service_event(ServiceCheck$service);
공개 함수 log_current_status(ServiceCheck$service);
}
마지막으로 엔진 자체를 작성해야 합니다. 이 아이디어는 이전 섹션에서 간단한 프로그램을 작성할 때 사용한 것과 유사합니다. 서버는 각 검사를 처리하기 위한 새 프로세스를 생성하고 SIGCHLD 핸들러를 사용하여 검사가 완료되면 반환 값을 감지해야 합니다. 동시에 확인할 수 있는 최대 개수를 설정할 수 있어야 시스템 자원의 과도한 사용을 방지할 수 있다. 모든 서비스와 로그는 XML 파일로 정의됩니다.
다음은 이 엔진을 정의하는 ServiceCheckRunner 클래스입니다
.
비공개 $num_children;
개인 $services = 배열();
개인 $어린이 = 배열();
공개 함수 _ _construct($conf, $num_children)
{
$로거 = 배열();
$this->num_children = $num_children;
$conf = simplexml_load_file($conf);
foreach($conf->loggers->$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로 로거) {
$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을 반환합니다.
}
($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();
if($pid = pcntl_fork()) {
$this->children[$pid] = $service;
}
또 다른 {
pcntl_alarm($service->timeout());
종료($service->run());
}
}
}
}
공개 함수 log_current_status(){
foreach($this->서비스를 $service로) {
$service->log_current_status();
}
}
개인 함수 sig_child($signal){
$status = ServiceCheck::실패;
pcntl_signal(SIGCHLD, array($this, "sig_child"));
while(($pid = pcntl_wait($status, WNOHANG)) > 0){
$service = $this->어린이[$pid];
unset($this->children[$pid]);
if(pcntl_wifexited($status) && pcntl_wexitstatus($status) ==ServiceCheck::SUCCESS)
{
$status = ServiceCheck::성공;
}
$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()에 의해 정의된 테스트를 실행합니다.
두 개의 신호 핸들러도 있습니다. 종료된 하위 프로세스를 수집하고 서비스의 post_run() 메소드를 실행하는 SIGCHLD 핸들러 sig_child(); 등록된 모든 로거의 log_current_status() 메소드를 호출하는 SIGUSR1 핸들러 sig_usr1() 전체 시스템의 현재 상태를 가져오는 데 사용할 수 있습니다.
물론 이 감시 아키텍처는 실용적인 기능을 전혀 수행하지 않습니다. 하지만 먼저 서비스를 확인해야 합니다. 다음 클래스는 HTTP 서버로부터 "200 Server OK" 응답을 받고 있는지 확인합니다.
class HTTP_ServiceCheck는 ServiceCheck를 확장합니다.
공개 $url;
공개 함수 _ _construct($params){
foreach($params $k => $v) {
$k = "$k";
$this->$k = "$v";
}
}
공개 함수 실행(){
if(is_resource(@fopen($this->url, "r"))) {
ServiceCheck::SUCCESS를 반환합니다.
}
또 다른 {
ServiceCheck::FAILURE를 반환합니다.
}
}
}
이전에 구축한 프레임워크와 비교할 때 이 서비스는 매우 간단하므로 여기서는 자세히 설명하지 않습니다.
5. 샘플 ServiceLogger 프로세스
다음은 샘플 ServiceLogger 프로세스입니다. 서비스가 다운되면 통화 중인 사람에게 이메일을 보내는 일을 담당합니다.
Class 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){
반품;
}
}
연속해서 5번 실패하면 프로세스는 백업 주소에도 메시지를 보냅니다. 의미 있는 log_current_status() 메서드를 구현하지 않는다는 점에 유의하세요.
다음과 같이 서비스 상태를 변경할 때마다 PHP 오류 로그에 기록하는 ServiceLogger 프로세스를 구현해야 합니다.
class ErrorLog_ServiceLogger Implements ServiceLogger {
공개 함수 log_service_event(ServiceCheck$service)
{
if($service->current_status() !==$service->previous_status()) {
if($service->current_status() ===ServiceCheck::FAILURE) {
$status = '다운';
}
또 다른 {
$상태 = 'UP';
}
error_log("{$service->description()} 상태가 $status로 변경되었습니다.");
}
}
공개 함수 log_current_status(ServiceCheck$service)
{
error_log("{$service->description()}: $status");
}
}
log_current_status() 메소드는 프로세스가 SIGUSR1 신호를 보내는 경우 전체 현재 상태를 PHP 오류 로그에 복사한다는 것을 의미합니다.
엔진은 다음과 같은 구성 파일을 사용합니다
.
〈로거〉
<로거>
<id>오류로그</id>
<클래스>ErrorLog_ServiceLogger</class>
</로거>
<로거>
<id>emailme<//id>
<클래스>EmailMe_ServiceLogger</class>
</로거>
</로거>
<서비스>
<서비스>
<class>HTTP_ServiceCheck</class>
<매개변수>
<설명>OmniTI HTTP 확인</설명>
<URL> http://www.omniti.com </url>
<타임아웃>30</타임아웃>
<횟수>900</횟수>
</매개변수>
〈로거〉
<로거>오류로그</로거>
<로거>이메일을 보내주세요</로거>
</로거>
</서비스>
<서비스>
<class>HTTP_ServiceCheck</class>
<매개변수>
<설명>홈페이지 HTTP 확인</설명>
<url> http://www.schlossnagle.org/~george </url>
<타임아웃>30</타임아웃>
<주파수>3600</주파수>
</매개변수>
〈로거〉
<로거>오류로그</로거>
</로거>
</서비스>
</서비스>
</config>
이 XML 파일이 전달되면 ServiceCheckRunner의 생성자는 지정된 각 로그에 대한 로깅 프로그램을 인스턴스화합니다. 그런 다음 지정된 각 서비스에 해당하는 ServiceCheck 개체를 인스턴스화합니다.
생성자는 인스턴스화를 시도하기 전에 Reflection_Class 클래스를 사용하여 서비스 및 로깅 클래스의 내부 검사를 구현합니다. 이는 불필요하지만 PHP 5의 새로운 Reflection API 사용을 잘 보여줍니다. 이러한 클래스 외에도 Reflection API는 PHP의 거의 모든 내부 엔터티(클래스, 메서드 또는 함수)에 대한 내장 검사를 구현하는 클래스를 제공합니다.
구축한 엔진을 사용하려면 여전히 래퍼 코드가 필요합니다. 워치독은 두 번 시작하려고 시도하는 것을 방지해야 합니다. 모든 이벤트에 대해 두 개의 메시지를 생성할 필요가 없습니다. 물론 모니터에는 다음을 포함한 몇 가지 옵션도 제공되어야 합니다.
옵션 설명
[-f] 엔진 구성 파일의 위치입니다. 기본값은 monitor.xml입니다.
[-n] 엔진에서 허용하는 하위 프로세스 풀의 크기입니다. 기본값은 5입니다.
[-d] 이 엔진의 데몬 기능을 비활성화하는 플래그입니다. 이는 정보를 stdout 또는 stderr로 출력하는 디버그 ServiceLogger 프로세스를 작성할 때 유용합니다.
다음은 옵션을 구문 분석하고 독점성을 보장하며 서비스 검사를 실행하는 최종 감시 스크립트입니다.
require_once "Service.inc";
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());
플러시($fp);
$engine = new ServiceCheckRunner($args['f'], $args['n']);
$engine->loop();
이 예제에서는 사용자 정의된 getOptions() 함수를 사용합니다.
적절한 구성 파일을 작성한 후 다음과 같이 스크립트를 시작할 수 있습니다.
> ./monitor.php -f /etc/monitor.xml
이는 시스템이 종료되거나 스크립트가 종료될 때까지 모니터링을 보호하고 계속합니다.
이 스크립트는 매우 복잡하지만 여전히 쉽게 개선할 수 있는 영역이 있으므로 독자의 연습 과제로 남겨두십시오.
· 서버를 시작하지 않고도 구성을 변경할 수 있도록 구성 파일을 다시 분석하는 SIGHUP 처리기를 추가합니다.
· 쿼리 데이터를 저장하기 위해 데이터베이스에 로그인할 수 있는 ServiceLogger를 작성합니다.
· 전체 모니터링 시스템에 좋은 GUI를 제공하기 위해 웹 프런트엔드 프로그램을 작성합니다.