Linux資格認(rèn)證:使用Perl實(shí)現(xiàn)系統(tǒng)服務(wù)監(jiān)控和報(bào)警

字號:

一般的Web站點(diǎn)來說,都包括很多服務(wù)和應(yīng)用,我們沒法實(shí)時(shí)知道系統(tǒng)運(yùn)行是否正常,特別是晚上的時(shí)候,如果服務(wù)器宕機(jī)或應(yīng)用掛掉了,都會影響業(yè)務(wù)和用戶訪問,這時(shí)候一套對系統(tǒng)監(jiān)控的錯設(shè)就必須得當(dāng)。目前有很多軟件應(yīng)的監(jiān)控通知和報(bào)警服務(wù),有收費(fèi)的也有免費(fèi)的,大家都可以選擇。
    我們就嘗試自己來實(shí)現(xiàn)一個服務(wù)監(jiān)控和報(bào)警通知的程序,這樣能夠使用很小的代價(jià),同樣讓我們的服務(wù)高可用性和高可靠性。
    【監(jiān)控原理】
    遠(yuǎn)程服務(wù)
    對于遠(yuǎn)程機(jī)器來說,我們可以有一臺監(jiān)控服務(wù)器,或者隨便找一臺比較不容宕機(jī)的服務(wù)器來作為監(jiān)控服務(wù)器,那么就能夠監(jiān)控其他的服務(wù)機(jī)上的服務(wù)了,遠(yuǎn)程監(jiān)控是比較大家需要的方式了。一般遠(yuǎn)程監(jiān)控就監(jiān)控服務(wù)器和端口是否開放,比如說,我們的 Web 服務(wù) Apache 一般都會開放 80 端口,那么我們就可以通過訪問這臺服務(wù)器的 80 端口來確定 Apache 是否在正常工作,如果無法連接上,那么說明該服務(wù)就停止了。
    本地服務(wù)
    對于本機(jī)來說,監(jiān)控進(jìn)程和日志文件都是可行的,一般來說,長期頻繁工作的服務(wù),比如 Apache 都會在每次訪問后把訪問信息記錄到 access 訪問日志文件里,如果這個文件長時(shí)間沒有更新,就可以懷疑該服務(wù)已經(jīng)停止了(當(dāng)然了,不排除,這段時(shí)間內(nèi)都沒有人訪問的情況)。另外對于進(jìn)程來說,本機(jī)是很容易可以查看到進(jìn)程情況的,對于 MySQL 等服務(wù)器來說,守護(hù)進(jìn)程都是長期開放的,如果發(fā)現(xiàn)當(dāng)前系統(tǒng)中沒有了 MySQL 守護(hù)進(jìn)程,那么也可以確認(rèn) MySQL 服務(wù)已經(jīng)停止了。
    報(bào)警通知
    服務(wù)停止了,自然需要通知系統(tǒng)維護(hù)人員,那么一般就是通過郵件或者短信的方式,短信是的了,但是頻繁短信同樣讓維護(hù)人員很郁悶,這個叫做短信炸彈(Message Bomb),所以郵件也許是個簡單實(shí)在的方式,本地再配置上 Outlook/Foxmail 定期接收和通知方式,也比較快捷,但是晚上回家后,一般都無法收到郵件了,所以合理的方式是白天郵件通知,晚上和周末短信通知 的報(bào)警方式更合理。
    【代碼實(shí)現(xiàn)】
    具體代碼實(shí)現(xiàn)可以使用各種代碼了,C/C++、 Ruby、Python、PHP ,只要能夠訪問文件、Socket ,能夠定期執(zhí)行的語言都可以,我們下面的代碼采用 Perl 來構(gòu)建,因?yàn)?Perl 是很好的系統(tǒng)管理腳本語言,任何 Unix/Linux 都缺省安裝了 Perl 引擎,能夠很方便的在任何機(jī)器上面運(yùn)行,同時(shí) Perl 的靈活性強(qiáng),而且有強(qiáng)大的 CPAN 包庫,所以編寫代碼很方便,在系統(tǒng)管理中也是值得推薦大家使用的,當(dāng)然了,很多系統(tǒng)管理工作使用 shell 腳本也許更方便。
    下面的代碼實(shí)現(xiàn)對遠(yuǎn)程監(jiān)控、本地日志文件監(jiān)控、本地進(jìn)程監(jiān)控都進(jìn)行了實(shí)現(xiàn),但是只使用了遠(yuǎn)程端口監(jiān)控的方式,因?yàn)檫@樣就能夠監(jiān)控多臺機(jī)器和服務(wù)了,如果只是單臺機(jī)器或者只是想監(jiān)控本地進(jìn)程,可以很簡單的修改主函數(shù)來實(shí)現(xiàn)。同時(shí)通知方式主要是采用郵件通知的方式,并且函數(shù)實(shí)現(xiàn)了SMTP協(xié)議進(jìn)行郵件發(fā)送(因?yàn)槲野l(fā)現(xiàn)Perl內(nèi)置的 Net::SMTP 在進(jìn)行型驗(yàn)證的時(shí)候,并不是很靠譜),當(dāng)然了,在報(bào)警通知方面,完全可以改寫成發(fā)送短信或者按照時(shí)間來分別調(diào)用短信和郵件的方式。
    代碼中主要監(jiān)控了包括 Apache、MySQL、Memcache、Search(假如你有的話)等服務(wù),可以在這個基礎(chǔ)上進(jìn)行增刪不同的服務(wù)器監(jiān)控,只需要增加一個常量配置和修改 main 函數(shù)代碼。
    說明:以下Perl代碼在 RHEL 4 + Perl v5.8.6 環(huán)境下測試通過
    #!/usr/bin/perl
    use IO::Socket;
    use IO::File;
    use MIME::Base64;
    ##############################
    # Constant define (configure)
    ##############################
    # mail config
    use constant MAIL_ADDR => ('to'=>'webmaster@example.com', 'from'=>'webmaster@example.com');
    use constant SMTP_INFO => ('host'=>'smtp.example.com', 'user'=>'webmaster', 'password'=>'pass',
    'debug'=>1, 'bufsize'=>1024);
    # common config
    use constant MD5SUM_FILE => '/tmp/__monitor_md5sum_hash';
    use constant APACHE_LOG_PATH => '/usr/local/apache2/logs/access';
    # apache
    use constant APACHE_PORT => 80;
    use constant APACHE_SERVERS => ('web1.example.com', 'web2.example.com');
    # mysql
    use constant MYSQL_PORT => 3306;
    use constant MYSQL_SERVERS => ('db1.example.com', 'db2.example.com');
    # memcache
    use constant MEMCACHE_PORT => 11211;
    use constant MEMCACHE_SERVERS => ('cache1.example.com', 'cache2.example.com');
    # search
    use constant SEARCH_PORT => 8000;
    use constant SEARCH_SERVERS => ('search1.example.com');
    ##############################
    # Server port is alive check
    ##############################
    sub check_server_alive {
    my($server, $port) = @_;
    $sock = IO::Socket::INET->new(PeerAddr=>$server, PeerPort=>$port, Proto=>'tcp', Timeout=>3);
    if (!$sock){
    return 0;
    }
    $sock->close();
    return 1;
    }
    ##############################
    # Check process is exist
    ##############################
    sub check_process_exist {
    my $proc_name = shift;
    $line = `/bin/ps auxw | /bin/grep $proc_name | /bin/grep -v grep | /usr/bin/wc -l`;
    $line =~ s/^s+|s+$//g;
    if ($line == 0){
    return 0;
    }
    return 1;
    }
    ##############################
    # Check file md5 fingerprint
    ##############################
    sub check_file_md5sum {
    my $io, $line;
    $filename = shift;
    @arr = split(/s/, `/usr/bin/md5sum $filename`);
    $filehash = shift(@arr);
    $io = IO::File->new();
    $io->open(MD5SUM_FILE, O_RDWR);
    if (!($line = $io->getLine())){
    $io->syswrite($filehash);
    $io->close;
    return true;
    } if ($line != $filehash){
    $io->truncate(0);
    $io->syswrite($filehash);
    $io->close;
    return true;
    }
    return true;
    }
    ##############################
    # SMTP execute command
    ##############################
    sub smtp_cmd {
    my ($sock, $cmd, $blocking) = @_;
    my %smtpinfo = SMTP_INFO;
    my $buf, $bufsize = $smtpinfo{'bufsize'}, $debug=$smtpinfo{'debug'};
    $sock->syswrite($cmd);
    if ($debug == 1){
    print ">>> $cmd ";
    }
    if ($blocking == 1){
    $sock->sysread($buf, $bufsize);
    if ($debug){
    print "<<< $buf";
    }
    }
    }
    ##############################
    # Send notice mail
    ##############################
    sub send_mail {
    my ($subject, $content) = @_;
    my $sock;
    my %mailaddr = MAIL_ADDR;
    my %smtpinfo = SMTP_INFO;
    my $debug = $smtpinfo{'debug'};
    # Count date time
    ($sec, $min, $hour, $day, $mon, $year, $wday, $yday, $isdst) = localtime(time());
    $datetime = sprintf("%s-%s-%s %s:%s:%s", "20".substr($year,1,2), length($mon)==1?"0$mon":$mon, length($day)==1?"0$day":$day, length($hour)==1?"0$hour":$hour, length($min)==1?"0$min":$min, length($sec)==1?"0$sec":$sec);
    $subject .= "[$datetime]";
    # Connect to SMTP server
    $sock = IO::Socket::INET->new(PeerAddr=>$smtpinfo{'host'}, PeerPort=>25, Proto=>'tcp', Timeout=>10);
    $sock->blocking(1);
    # Send smtp command
    if ($debug == 1){
    print "<<< ". $sock->sysread($buf, $smtpinfo{'bufsize'});
    }
    smtp_cmd($sock, "HELO locahost ", 1);
    smtp_cmd($sock, "AUTH LOGIN ", 1);
    smtp_cmd($sock, encode_base64($smtpinfo{'user'}), 1);
    smtp_cmd($sock, encode_base64($smtpinfo{'password'}), 1);
    smtp_cmd($sock, "MAIL FROM: <". $mailaddr{'from'} ."> ", 1);
    smtp_cmd($sock, "RCPT TO: <". $mailaddr{'to'} ."> ", 1);
    smtp_cmd($sock, "DATA ", 1);
    smtp_cmd($sock, "From: ". $smtpinfo{'from'} ." ", 0);
    smtp_cmd($sock, "To: ". $smtpinfo{'to'} ." ", 0);
    smtp_cmd($sock, "Subject: $subject ", 0);
    smtp_cmd($sock, "$content ", 0);
    smtp_cmd($sock, " . ", 1);
    smtp_cmd($sock, "QUIT ", 0);
    $sock->close();
    return 1;
    }
    ##############################
    # Check server alive main
    ##############################
    sub monitor_main {
    # check apache
    foreach $item (APACHE_SERVERS) {
    if (!check_server_alive($item, APACHE_PORT)) {
    send_mail("$item apache server is down", "$item apache server is down. please timely restoration");
    }
    }
    # check mysql
    foreach $item (MYSQL_SERVERS) {
    if (!check_server_alive($item, MYSQL_PORT)) {
    send_mail("$item mysql server is down", "$item mysql server is down. please timely restoration");
    }
    }
    # check memcache
    foreach $item (MEMCACHE_SERVERS) {
    if (!check_server_alive($item, MEMCACHE_PORT)) {
    send_mail("$item memcache server is down", "$item memcache server is down. please timely restoration");
    }
    }
    # check search
    foreach $item (SEARCH_SERVERS) {
    if (!check_server_alive($item, SEARCH_PORT)) {
    send_mail("$item search server is down", "$item search server is down. please timely restoration");
    }
    }
    }
    ##############################
    # Main running
    ##############################
    monitor_main();