http://tech.sina.com.cn/s/2006-10-27/08401206523.shtmlhttp://www.sina.com.cn 2006年10月27日 08:40 天極yesky
作者:朱先忠編譯
摘要:在本文中,讓我們共同探討基于PHP語(yǔ)言構建一個(gè)基本的服務(wù)器端監視引擎的諸多技巧及注意事項,并給出完整的源碼實(shí)現。
一. 更改工作目錄的問(wèn)題
當你編寫(xiě)一個(gè)監視程序時(shí),讓它設置自己的工作目錄通常更好些。這樣以來(lái),如果你使用一個(gè)相對路徑讀寫(xiě)文件,那么,它會(huì )根據情況自動(dòng)處理用戶(hù)期望存放文件的位置??偸窍拗瞥绦蛑惺褂玫穆窂奖M管是一種良好的實(shí)踐;但是,卻失去了應有的靈活性。因此,改變你的工作目錄的最安全的方法是,既使用chdir()也使用chroot()。
chroot()可用于PHP的CLI和CGI版本中,但是卻要求程序以根權限運行。chroot()實(shí)際上把當前進(jìn)程的路徑從根目錄改變到指定的目錄。這使得當前進(jìn)程只能執行存在于該目錄下的文件。經(jīng)常情況下,chroot()由服務(wù)器作為一個(gè)"安全設備"使用以確保惡意代碼不會(huì )修改一個(gè)特定的目錄之外的文件。請牢記,盡管chroot()能夠阻止你訪(fǎng)問(wèn)你的新目錄之外的任何文件,但是,任何當前打開(kāi)的文件資源仍然能夠被存取。例如,下列代碼能夠打開(kāi)一個(gè)日志文件,調用chroot()并切換到一個(gè)數據目錄;然后,仍然能夠成功地登錄并進(jìn)而打開(kāi)文件資源:
<?php
$logfile = fopen("/var/log/chroot.log", "w");
chroot("/Users/george");
fputs($logfile, "Hello From Inside The Chroot\n");
>
如果一個(gè)應用程序不能使用chroot(),那么你可以調用chdir()來(lái)設置工作目錄。例如,當代碼需要加載特定的代碼(這些代碼能夠在系統的任何地方被定位時(shí)),這是很有用的。注意,chdir()沒(méi)有提供安全機制來(lái)防止打開(kāi)未授權的文件。
二. 放棄特權
當編寫(xiě)Unix守護程序時(shí),一種經(jīng)典的安全預防措施是讓它們放棄所有不需要的特權;否則,擁有不需要的特權容易招致不必要的麻煩。在代碼(或PHP本身)中含有漏洞的情況下,通過(guò)確保一個(gè)守護程序以最小權限用戶(hù)身份運行,往往能夠使損失減到最小。
一種實(shí)現此目的的方法是,以非特權用戶(hù)身份執行該守護程序。然而,如果程序需要在一開(kāi)始就打開(kāi)非特權用戶(hù)無(wú)權打開(kāi)的資源(例如日志文件,數據文件,套接字,等等)的話(huà),這通常是不夠的。
如果你以根用戶(hù)身份運行,那么你能夠借助于posix_setuid()和posiz_setgid()函數來(lái)放棄你的特權。下面的示例把當前運行程序的特權改變?yōu)橛脩?hù)nobody所擁有的那些權限:
$pw=posix_getpwnam(‘nobody‘);
posix_setuid($pw[‘uid‘]);
posix_setgid($pw[‘gid‘]);
就象chroot()一樣,任何在放棄特權之前被打開(kāi)的特權資源都會(huì )保持為打開(kāi),但是不能創(chuàng )建新的資源。
三. 保證排它性
你可能經(jīng)常想實(shí)現:一個(gè)腳本在任何時(shí)刻僅運行一個(gè)實(shí)例。為了保護腳本,這是特別重要的,因為在后臺運行容易導致偶然情況下調用多個(gè)實(shí)例。
保證這種排它性的標準技術(shù)是,通過(guò)使用flock()來(lái)讓腳本鎖定一個(gè)特定的文件(經(jīng)常是一個(gè)加鎖文件,并且被排它式使用)。如果鎖定失敗,該腳本應該輸出一個(gè)錯誤并退出。下面是一個(gè)示例:
$fp=fopen("/tmp/.lockfile","a");
if(!$fp || !flock($fp, LOCK_EX | LOCK_NB)) {
fputs(STDERR, "Failed to acquire lock\n");
exit;
}
/*成功鎖定以安全地執行工作*/
注意,有關(guān)鎖機制的討論涉及較多內容,在此不多加解釋。
四. 構建監視服務(wù)
在這一節中,我們將使用PHP來(lái)編寫(xiě)一個(gè)基本的監視引擎。因為你不會(huì )事先知道怎樣改變,所以你應該使它的實(shí)現既靈活又具可能性。
該記錄程序應該能夠支持任意的服務(wù)檢查(例如,HTTP和FTP服務(wù))并且能夠以任意方式(通過(guò)電子郵件,輸出到一個(gè)日志文件,等等)記錄事件。你當然想讓它以一個(gè)守護程序方式運行;所以,你應該請求它輸出其完整的當前狀態(tài)。
一個(gè)服務(wù)需要實(shí)現下列抽象類(lèi):
abstract class ServiceCheck {
const FAILURE = 0;
const SUCCESS = 1;
protected $timeout = 30;
protected $next_attempt;
protected $current_status = ServiceCheck::SUCCESS;
protected $previous_status = ServiceCheck::SUCCESS;
protected $frequency = 30;
protected $description;
protected $consecutive_failures = 0;
protected $status_time;
protected $failure_time;
protected $loggers = array();
abstract public function __construct($params);
public function __call($name, $args)
{
if(isset($this->$name)) {
return $this->$name;
}
}
public function set_next_attempt()
{
$this->next_attempt = time() + $this->frequency;
}
public abstract function run();
public function 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++;
}
else {
$this->failure_time = time();
}
}
else {
$this->consecutive_failures = 0;
}
$this->status_time = time();
$this->current_status = $status;
$this->log_service_event();
}
public function log_current_status()
{
foreach($this->loggers as $logger) {
$logger->log_current_status($this);
}
}
private function log_service_event()
{
foreach($this->loggers as $logger) {
$logger->log_service_event($this);
}
}
public function register_logger(ServiceLogger $logger)
{
$this->loggers[] = $logger;
}
}
上面的__call()重載方法提供對一個(gè)ServiceCheck對象的參數的只讀存取操作:
· timeout-在引擎終止檢查之前,這一檢查能夠掛起多長(cháng)時(shí)間。
· next_attempt-下次嘗試連接到服務(wù)器的時(shí)間。
· current_status-服務(wù)的當前狀態(tài):SUCCESS或FAILURE。
· previous_status-當前狀態(tài)之前的狀態(tài)。
· frequency-每隔多長(cháng)時(shí)間檢查一次服務(wù)。
· description-服務(wù)描述。
· consecutive_failures-自從上次成功以來(lái),服務(wù)檢查連續失敗的次數。
· status_time-服務(wù)被檢查的最后時(shí)間。
· failure_time-如果狀態(tài)為FAILED,則它代表發(fā)生失敗的時(shí)間。
這個(gè)類(lèi)還實(shí)現了觀(guān)察者模式,允許ServiceLogger類(lèi)型的對象注冊自身,然后當調用log_current_status()或log_service_event()時(shí)調用它。
這里實(shí)現的關(guān)鍵函數是run(),它負責定義應該怎樣執行檢查。如果檢查成功,它應該返回SUCCESS;否則返回FAILURE。
當定義在run()中的服務(wù)檢查返回后,post_run()方法被調用。它負責設置對象的狀態(tài)并實(shí)現記入日志。
ServiceLogger接口:指定一個(gè)日志類(lèi)僅需要實(shí)現兩個(gè)方法:log_service_event()和log_current_status(),它們分別在當一個(gè)run()檢查返回時(shí)和當實(shí)現一個(gè)普通狀態(tài)請求時(shí)被調用。
該接口如下所示:
interface ServiceLogger {
public function log_service_event(ServiceCheck$service);
public function log_current_status(ServiceCheck$service);
}
最后,你需要編寫(xiě)引擎本身。該想法類(lèi)似于在前一節編寫(xiě)簡(jiǎn)單程序時(shí)使用的思想:服務(wù)器應該創(chuàng )建一個(gè)新的進(jìn)程來(lái)處理每一次檢查并使用一個(gè)SIGCHLD處理器來(lái)檢測當檢查完成時(shí)的返回值??梢酝瑫r(shí)檢查的最大數目應該是可配置的,從而可以防止對系統資源的過(guò)渡使用。所有的服務(wù)和日志都將在一個(gè)XML文件中定義。
下面是定義該引擎的ServiceCheckRunner類(lèi):
class ServiceCheckRunner {
private $num_children;
private $services = array();
private $children = array();
public function _ _construct($conf, $num_children)
{
$loggers = array();
$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();
}
else {
fputs(STDERR, "{$logger->class} cannot be instantiated.\n");
exit;
}
}
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;
}
else {
fputs(STDERR, "{$service->class} is not instantiable.\n");
exit;
}
}
}
private function next_attempt_sort($a, $b){
if($a->next_attempt() == $b->next_attempt()) {
return 0;
}
return ($a->next_attempt() < $b->next_attempt())? -1 : 1;
}
private function next(){
usort($this->services,array($this,‘next_attempt_sort‘));
return $this->services[0];
}
public function loop(){
declare(ticks=1);
pcntl_signal(SIGCHLD, array($this, "sig_child"));
pcntl_signal(SIGUSR1, array($this, "sig_usr1"));
while(1) {
$now = time();
if(count($this->children)< $this->num_children) {
$service = $this->next();
if($now < $service->next_attempt()) {
sleep(1);
continue;
}
$service->set_next_attempt();
if($pid = pcntl_fork()) {
$this->children[$pid] = $service;
}
else {
pcntl_alarm($service->timeout());
exit($service->run());
}
}
}
}
public function log_current_status(){
foreach($this->services as $service) {
$service->log_current_status();
}
}
private function 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);
}
}
private function sig_usr1($signal){
pcntl_signal(SIGUSR1, array($this, "sig_usr1"));
$this->log_current_status();
}
}
這是一個(gè)很復雜的類(lèi)。其構造器讀取并分析一個(gè)XML文件,創(chuàng )建所有的將被監視的服務(wù),并創(chuàng )建記錄它們的日志程序。
loop()方法是該類(lèi)中的主要方法。它設置請求的信號處理器并檢查是否能夠創(chuàng )建一個(gè)新的子進(jìn)程?,F在,如果下一個(gè)事件(以next_attempt時(shí)間CHUO排序)運行良好,那么一個(gè)新的進(jìn)程將被創(chuàng )建。在這個(gè)新的子進(jìn)程內,發(fā)出一個(gè)警告以防止測試持續時(shí)間超出它的時(shí)限,然后執行由run()定義的測試。
還存在兩個(gè)信號處理器:SIGCHLD處理器sig_child(),負責收集已終止的子進(jìn)程并執行它們的服務(wù)的post_run()方法;SIGUSR1處理器sig_usr1(),簡(jiǎn)單地調用所有已注冊的日志程序的log_current_status()方法,這可以用于得到整個(gè)系統的當前狀態(tài)。
當然,這個(gè)監視架構并不沒(méi)有做任何實(shí)際的事情。但是首先,你需要檢查一個(gè)服務(wù)。下列這個(gè)類(lèi)檢查是否你從一個(gè)HTTP服務(wù)器取回一個(gè)"200 Server OK"響應:
class HTTP_ServiceCheck extends ServiceCheck{
public $url;
public function _ _construct($params){
foreach($params as $k => $v) {
$k = "$k";
$this->$k = "$v";
}
}
public function run(){
if(is_resource(@fopen($this->url, "r"))) {
return ServiceCheck::SUCCESS;
}
else {
return ServiceCheck::FAILURE;
}
}
}
與你以前構建的框架相比,這個(gè)服務(wù)極其簡(jiǎn)單,在此恕不多描述。
五. 示例ServiceLogger進(jìn)程
下面是一個(gè)示例ServiceLogger進(jìn)程。當一個(gè)服務(wù)停用時(shí),它負責把一個(gè)電子郵件發(fā)送給一個(gè)待命人員:
class EmailMe_ServiceLogger implements ServiceLogger {
public function log_service_event(ServiceCheck$service)
{
if($service->current_status ==ServiceCheck::FAILURE) {
$message = "Problem with{$service->description()}\r\n";
mail(‘oncall@example.com‘, ‘Service Event‘,$message);
if($service->consecutive_failures() > 5) {
mail(‘oncall_backup@example.com‘, ‘Service Event‘, $message);
}
}
}
public function log_current_status(ServiceCheck$service){
return;
}
}
如果連續失敗五次,那么該進(jìn)程還把一個(gè)消息發(fā)送到一個(gè)備份地址。注意,它并沒(méi)有實(shí)現一個(gè)有意義的log_current_status()方法。
無(wú)論何時(shí)象如下這樣改變一個(gè)服務(wù)的狀態(tài),你都應該實(shí)現一個(gè)寫(xiě)向PHP錯誤日志的ServiceLogger進(jìn)程:
class ErrorLog_ServiceLogger implements ServiceLogger {
public function log_service_event(ServiceCheck$service)
{
if($service->current_status() !==$service->previous_status()) {
if($service->current_status() ===ServiceCheck::FAILURE) {
$status = ‘DOWN‘;
}
else {
$status = ‘UP‘;
}
error_log("{$service->description()} changed status to $status");
}
}
public function log_current_status(ServiceCheck$service)
{
error_log("{$service->description()}: $status");
}
}
該log_current_status()方法意味著(zhù),如果進(jìn)程發(fā)送一個(gè)SIGUSR1信號,它將把其完整的當前狀態(tài)復制到你的PHP錯誤日志中。
該引擎使用如下的一個(gè)配置文件:
<config>
<loggers>
<logger>
<id>errorlog</id>
<class>ErrorLog_ServiceLogger</class>
</logger>
<logger>
<id>emailme</id>
<class>EmailMe_ServiceLogger</class>
</logger>
</loggers>
<services>
<service>
<class>HTTP_ServiceCheck</class>
<params>
<description>OmniTI HTTP Check</description>
<url>http://www.omniti.com</url>
<timeout>30</timeout>
<frequency>900</frequency>
</params>
<loggers>
<logger>errorlog</logger>
<logger>emailme</logger>
</loggers>
</service>
<service>
<class>HTTP_ServiceCheck</class>
<params>
<description>Home Page HTTP Check</description>
<url>http://www.schlossnagle.org/~george</url>
<timeout>30</timeout>
<frequency>3600</frequency>
</params>
<loggers>
<logger>errorlog</logger>
</loggers>
</service>
</services>
</config>
當傳遞這個(gè)XML文件時(shí),ServiceCheckRunner的構造器對于每一個(gè)指定的日志實(shí)例化一個(gè)日志記錄程序。然后,它相應于每一個(gè)指定的服務(wù)實(shí)例化一個(gè)ServiceCheck對象。
注意 該構造器使用Reflection_Class類(lèi)來(lái)實(shí)現該服務(wù)和日志類(lèi)的內在檢查-在你試圖實(shí)例化它們之前。盡管這是不必要的,但是它很好地演示了PHP 5中新的反射(Reflection)API的使用。除了這些類(lèi)以外,反射API還提供一些類(lèi)來(lái)實(shí)現對PHP中幾乎任何內部實(shí)體(類(lèi),方法或函數)的內在檢查。
為了使用你構建的引擎,你仍然需要一些包裝代碼。監視程序應該會(huì )禁止你試圖兩次啟動(dòng)它-你不需要對每一個(gè)事件建立兩份消息。當然,該監視程序還應該接收包括下列選項在內的一些選項:
選項 描述
[-f] 引擎的配置文件的一個(gè)位置,默認是monitor.xml。
[-n] 引擎允許的子進(jìn)程池的大小,默認是5。
[-d] 一個(gè)停用該引擎的守護功能的標志。在你編寫(xiě)一個(gè)把信息輸出到stdout或stderr的調試ServiceLogger進(jìn)程時(shí),這是很有用的。
下面是最終的監視程序腳本,它分析選項,保證排它性并且運行服務(wù)檢查:
require_once "Service.inc";
require_once "Console/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, "Failed to acquire lock\n");
exit;
}
if(!$args[‘d‘]) {
if(pcntl_fork()) {
exit;
}
posix_setsid();
if(pcntl_fork()) {
exit;
}
}
fwrite($fp, getmypid());
fflush($fp);
$engine = new ServiceCheckRunner($args[‘f‘],$args[‘n‘]);
$engine->loop();
注意,這個(gè)示例使用了定制的getOptions()函數。
在編寫(xiě)一個(gè)適當的配置文件后,你可以按如下方式啟動(dòng)該腳本:
> ./monitor.php -f /etc/monitor.xml
這可以保護并繼續監視直到機器被關(guān)掉或該腳本被殺死。
這個(gè)腳本相當復雜,但是仍然存在一些容易改進(jìn)的地方,這些只好留給讀者作為練習之用:
· 添加一個(gè)重新分析配置文件的SIGHUP處理器以便你能夠在不啟動(dòng)服務(wù)器的情況下改變配置。
· 編寫(xiě)一個(gè)能夠登錄到一個(gè)數據庫的ServiceLogger以用于存儲查詢(xún)數據。
· 編寫(xiě)一個(gè)Web前端程序以為整個(gè)監視系統提供一種良好的GUI。