PHP高階程式設計之守護程序,實現優雅重啟
影音先鋒電影http://www.iskdy.com/
1. 什麼是守護程序
守護程序是脫離於終端並且在後臺執行的程序。守護程序脫離於終端是為了避免程序在執行過程中的資訊在任何終端上顯示並且程序也不會被任何終端所產生的終端資訊所打斷。
例如 apache, nginx, mysql 都是守護程序
2. 為什麼開發守護程序
很多程式以服務形式存在,他沒有終端或UI互動,它可能採用其他方式與其他程式互動,如TCP/UDP Socket, UNIX Socket, fifo。程式一旦啟動便進入後臺,直到滿足條件他便開始處理任務。
3. 何時採用守護程序開發應用程式
以我當前的需求為例,我需要執行一個程式,然後監聽某埠,持續接受服務端發起的資料,然後對資料分析處理,再將結果寫入到資料庫中; 我採用ZeroMQ實現資料收發。
如果我不採用守護程序方式開發該程式,程式一旦執行就會佔用當前終端窗框,還有受到當前終端鍵盤輸入影響,有可能程式誤退出。
4. 守護程序的安全問題
我們希望程式在非超級使用者執行,這樣一旦由於程式出現漏洞被駭客控制,攻擊者只能繼承執行許可權,而無法獲得超級使用者許可權。
我們希望程式只能執行一個例項,不運行同事開啟兩個以上的程式,因為會出現埠衝突等等問題。
5. 怎樣開發守護程序
例 1. 多執行緒守護程序例示<?php class ExampleWorker extends Worker { #public function __construct(Logging $logger) { # $this->logger = $logger; #} #protected $logger; protected static $dbh; public function __construct() { } public function run(){ $dbhost = '192.168.2.1'; // 資料庫伺服器 $dbport = 3306; $dbuser = 'www'; // 資料庫使用者名稱 $dbpass = 'qwer123'; // 資料庫密碼 $dbname = 'example'; // 資料庫名 self::$dbh = new PDO("mysql:host=$dbhost;port=$dbport;dbname=$dbname", $dbuser, $dbpass, array( /* PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES \'UTF8\'', */ PDO::MYSQL_ATTR_COMPRESS => true, PDO::ATTR_PERSISTENT => true ) ); } protected function getInstance(){ return self::$dbh; } } /* the collectable class implements machinery for Pool::collect */ class Fee extends Stackable { public function __construct($msg) { $trades = explode(",", $msg); $this->data = $trades; print_r($trades); } public function run() { #$this->worker->logger->log("%s executing in Thread #%lu", __CLASS__, $this->worker->getThreadId() ); try { $dbh = $this->worker->getInstance(); $insert = "INSERT INTO fee(ticket, login, volume, `status`) VALUES(:ticket, :login, :volume,'N')"; $sth = $dbh->prepare($insert); $sth->bindValue(':ticket', $this->data[0]); $sth->bindValue(':login', $this->data[1]); $sth->bindValue(':volume', $this->data[2]); $sth->execute(); $sth = null; /* ...... */ $update = "UPDATE fee SET `status` = 'Y' WHERE ticket = :ticket and `status` = 'N'"; $sth = $dbh->prepare($update); $sth->bindValue(':ticket', $this->data[0]); $sth->execute(); //echo $sth->queryString; //$dbh = null; } catch(PDOException $e) { $error = sprintf("%s,%s\n", $mobile, $id ); file_put_contents("mobile_error.log", $error, FILE_APPEND); } } } class Example { /* config */ const LISTEN = "tcp://192.168.2.15:5555"; const MAXCONN = 100; const pidfile = __CLASS__; const uid = 80; const gid = 80; protected $pool = NULL; protected $zmq = NULL; public function __construct() { $this->pidfile = '/var/run/'.self::pidfile.'.pid'; } private function daemon(){ if (file_exists($this->pidfile)) { echo "The file $this->pidfile exists.\n"; exit(); } $pid = pcntl_fork(); if ($pid == -1) { die('could not fork'); } else if ($pid) { // we are the parent //pcntl_wait($status); //Protect against Zombie children exit($pid); } else { // we are the child file_put_contents($this->pidfile, getmypid()); posix_setuid(self::uid); posix_setgid(self::gid); return(getmypid()); } } private function start(){ $pid = $this->daemon(); $this->pool = new Pool(self::MAXCONN, \ExampleWorker::class, []); $this->zmq = new ZMQSocket(new ZMQContext(), ZMQ::SOCKET_REP); $this->zmq->bind(self::LISTEN); /* Loop receiving and echoing back */ while ($message = $this->zmq->recv()) { //print_r($message); //if($trades){ $this->pool->submit(new Fee($message)); $this->zmq->send('TRUE'); //}else{ // $this->zmq->send('FALSE'); //} } $pool->shutdown(); } private function stop(){ if (file_exists($this->pidfile)) { $pid = file_get_contents($this->pidfile); posix_kill($pid, 9); unlink($this->pidfile); } } private function help($proc){ printf("%s start | stop | help \n", $proc); } public function main($argv){ if(count($argv) < 2){ printf("please input help parameter\n"); exit(); } if($argv[1] === 'stop'){ $this->stop(); }else if($argv[1] === 'start'){ $this->start(); }else{ $this->help($argv[0]); } } } $cgse = new Example(); $cgse->main($argv);
<?php declare(ticks = 1); require_once( __DIR__.'/autoload.class.php' ); umask(077); class EDM { protected $queue; public function __construct() { global $argc, $argv; $this->argc = $argc; $this->argv = $argv; $this->pidfile = $this->argv[0].".pid"; $this->config = new Config('mq'); $this->logging = new Logging(__DIR__.'/log/'.$this->argv[0].'.'.date('Y-m-d').'.log'); //.H:i:s //print_r( $this->config->getArray('mq') ); //pcntl_signal(SIGHUP, array(&$this,"restart")); } protected function msgqueue(){ $exchangeName = 'email'; //交換機名 $queueName = 'email'; //佇列名 $routeKey = 'email'; //路由key //建立連線和channel $connection = new AMQPConnection($this->config->getArray('mq')); if (!$connection->connect()) { die("Cannot connect to the broker!\n"); } $this->channel = new AMQPChannel($connection); $this->exchange = new AMQPExchange($this->channel); $this->exchange->setName($exchangeName); $this->exchange->setType(AMQP_EX_TYPE_DIRECT); //direct型別 $this->exchange->setFlags(AMQP_DURABLE); //持久化 $this->exchange->declare(); //echo "Exchange Status:".$this->exchange->declare()."\n"; //建立佇列 $this->queue = new AMQPQueue($this->channel); $this->queue->setName($queueName); $this->queue->setFlags(AMQP_DURABLE); //持久化 $this->queue->declare(); //echo "Message Total:".$this->queue->declare()."\n"; //繫結交換機與佇列,並指定路由鍵 $bind = $this->queue->bind($exchangeName, $routeKey); //echo 'Queue Bind: '.$bind."\n"; //阻塞模式接收訊息 while(true){ //$this->queue->consume('processMessage', AMQP_AUTOACK); //自動ACK應答 $this->queue->consume(function($envelope, $queue) { $msg = $envelope->getBody(); $queue->ack($envelope->getDeliveryTag()); //手動傳送ACK應答 $this->logging->info('('.'+'.')'.$msg); //$this->logging->debug("Message Total:".$this->queue->declare()); }); $this->channel->qos(0,1); //echo "Message Total:".$this->queue->declare()."\n"; } $conn->disconnect(); } protected function start(){ if (file_exists($this->pidfile)) { printf("%s already running\n", $this->argv[0]); exit(0); } $this->logging->warning("start"); $pid = pcntl_fork(); if ($pid == -1) { die('could not fork'); } else if ($pid) { //pcntl_wait($status); //等待子程序中斷,防止子程序成為殭屍程序。 exit(0); } else { posix_setsid(); //printf("pid: %s\n", posix_getpid()); file_put_contents($this->pidfile, posix_getpid()); //posix_kill(posix_getpid(), SIGHUP); $this->msgqueue(); } } protected function stop(){ if (file_exists($this->pidfile)) { $pid = file_get_contents($this->pidfile); posix_kill($pid, SIGTERM); //posix_kill($pid, SIGKILL); unlink($this->pidfile); $this->logging->warning("stop"); }else{ printf("%s haven't running\n", $this->argv[0]); } } protected function restart(){ $this->stop(); $this->start(); } protected function status(){ if (file_exists($this->pidfile)) { $pid = file_get_contents($this->pidfile); printf("%s already running, pid = %s\n", $this->argv[0], $pid); }else{ printf("%s haven't running\n", $this->argv[0]); } } protected function usage(){ printf("Usage: %s {start | stop | restart | status}\n", $this->argv[0]); } public function main(){ //print_r($this->argv); if($this->argc != 2){ $this->usage(); }else{ if($this->argv[1] == 'start'){ $this->start(); }else if($this->argv[1] == 'stop'){ $this->stop(); }else if($this->argv[1] == 'restart'){ $this->restart(); }else if($this->argv[1] == 'status'){ $this->status(); }else{ $this->usage(); } } } } $edm = New EDM(); $edm->main();
倫理片http://www.dotdy.com/
5.1. 程式啟動
下面是程式啟動後進入後臺的程式碼
通過程序ID檔案來判斷,當前程序狀態,如果程序ID檔案存在表示程式在執行中,通過程式碼file_exists($this->pidfile)實現,但而後程序被kill需要手工刪除該檔案才能執行
private function daemon(){ if (file_exists($this->pidfile)) { echo "The file $this->pidfile exists.\n"; exit(); } $pid = pcntl_fork(); if ($pid == -1) { die('could not fork'); } else if ($pid) { // we are the parent //pcntl_wait($status); //Protect against Zombie children exit($pid); } else { // we are the child file_put_contents($this->pidfile, getmypid()); posix_setuid(self::uid); posix_setgid(self::gid); return(getmypid()); } }
程式啟動後,父程序會推出,子程序會在後臺執行,子程序許可權從root切換到指定使用者,同時將pid寫入程序ID檔案。
5.2. 程式停止
程式停止,只需讀取pid檔案,然後呼叫posix_kill($pid, 9); 最後將該檔案刪除。
private function stop(){ if (file_exists($this->pidfile)) { $pid = file_get_contents($this->pidfile); posix_kill($pid, 9); unlink($this->pidfile); } }
5.3. 單例模式
所有執行緒共用資料庫連線,在多執行緒中這個非常重要,如果每個執行緒建立以此資料庫連線在關閉,這對資料庫的開銷是巨大的。
protected function getInstance(){ return self::$dbh; }
5.4. 實現優雅重啟
所謂優雅重啟是指程序不退出的情況加實現重新載入包含重置變數,重新整理配置檔案,重置日誌等等
stop/start 或者 restart都會退出程序,重新啟動,導致程序ID改變,同時瞬間退出導致業務閃斷。所以很多守護程序都會提供一個reload功能,者就是所謂的優雅重啟。
reload 實現原理是給程序傳送SIGHUP訊號,可以通過kill命令傳送 kill -s SIGHUP 64881,也可以通過庫函式實現 posix_kill(posix_getpid(), SIGUSR1);
<?php pcntl_signal(SIGTERM, function($signo) { echo "\n This signal is called. [$signo] \n"; Status::$state = -1; }); pcntl_signal(SIGHUP, function($signo) { echo "\n This signal is called. [$signo] \n"; Status::$state = 1; Status::$ini = parse_ini_file('test.ini'); }); class Status{ public static $state = 0; public static $ini = null; } $pid = pcntl_fork(); if ($pid == -1) { die('could not fork'); } if($pid) { // parent } else { $loop = true; Status::$ini = parse_ini_file('test.ini'); while($loop) { print_r(Status::$ini); while(true) { // Dispatching... pcntl_signal_dispatch(); if(Status::$state == -1) { // Do something and end loop. $loop = false; break; } if(Status::$state == 1) { printf("This program is reload.\r\n"); Status::$state = 0; break; } echo '.'; sleep(1); } echo "\n"; } echo "Finish \n"; exit(); }
建立配置檔案
[[email protected] pcntl]# cat test.ini [db] host=192.168.0.1 port=3306
測試方法,首先執行該守護程序
# php signal.reload.php Array ( [host] => 192.168.0.1 [port] => 3306 )
現在修改配置檔案,增加user=test配置項
[[email protected] pcntl]# cat test.ini [db] host=192.168.0.1 port=3306 user=test
傳送訊號,在另一個終端視窗,通過ps命令找到該程序的PID,然後使用kill命令傳送SIGHUP訊號,然後再通過ps檢視程序,你會發現程序PID沒有改變
[[email protected] pcntl]# ps ax | grep reload 64881 pts/0 S 0:00 php -c /srv/php/etc/php-cli.ini signal.reload.php 65073 pts/1 S+ 0:00 grep --color=auto reload [[email protected] pcntl]# kill -s SIGHUP 64881 [[email protected] pcntl]# ps ax | grep reload 64881 pts/0 S 0:00 php -c /srv/php/etc/php-cli.ini signal.reload.php 65093 pts/1 S+ 0:00 grep --color=auto reload
配置檔案被重新載入
This signal is called. [1] This program is reload. Array ( [host] => 192.168.0.1 [port] => 3306 [user] => test )
優雅重啟完成。
6. 程序意外退出解決方案
如果是非常重要的程序,必須要保證程式正常執行,一旦出現任何異常退出,都需要做即時做處理。下面的程式可能檢查程序是否異常退出,如果退出便立即啟動。
#!/bin/sh LOGFILE=/var/log/$(basename $0 .sh).log PATTERN="my.php" RECOVERY="/path/to/my.php start" while true do TIMEPOINT=$(date -d "today" +"%Y-%m-%d_%H:%M:%S") PROC=$(pgrep -o -f ${PATTERN}) #echo ${PROC} if [ -z "${PROC}" ]; then ${RECOVERY} >> $LOGFILE echo "[${TIMEPOINT}] ${PATTERN} ${RECOVERY}" >> $LOGFILE #else #echo "[${TIMEPOINT}] ${PATTERN} ${PROC}" >> $LOGFILE fi sleep 5 done &