1. 程式人生 > >利用Linux守護程序機制完成一個簡單系統監控demo

利用Linux守護程序機制完成一個簡單系統監控demo

根據前篇《Linux守護程序設計規範及Python實現》,我們給出了一個基於Python的守護程序框架,想要使用此框架構建自己的守護程序,只需要繼承Daemon類並實現run方法即可。在本文中,我們將按照此思路設計一個linux系統狀況監控程式。

目前,社群中有很多開源的系統監控軟體,例如Ganglia、Zabbix等,這些軟體以其優異的效能以及豐富的功能贏得了很多運維工程師的青睞,但是很多時候我們其實並不會利用到這些軟體的大部分功能,並且安裝配置這些軟體還需要安裝許多依賴軟體。我們需要的或許只是一個能夠可靠執行,簡單易用,輕量級的系統狀態定時收集器,通過這種收集器與其他更復雜的模組配合完成分散式系統的整體監控。例如筆者目前的工作是希望能夠統一監控OpenStack的物理資源及虛擬資源,而OpenStack自帶的監控模組Ceilometer雖然有一個合理的功能框架模型,但細化到對具體監控資料的獲取這一層次完成得並不是很好,於是我們希望能夠在這一層次對Ceilometer做一些改進和補充。

我們希望我們做的這個監控程式有如下特點:

1.    易用性:沒有複雜的依賴關係,不需要複雜的安裝配置

2.    擴充套件性:監控功能能夠方便擴充套件

3.    穩定性:執行穩定,不需要過多幹預,不能佔用太多系統資源

4.    可控性:可對服務進行控制(啟動、停止、重啟、設定監控引數)

實現思路:

根據監控的基本功能要求,必須解決監控資料獲取以及守護程序化兩個問題。

1.    監控資料獲取

首先根據《用Python指令碼實現對Linux伺服器的監控》一文所述,我們可以利用python指令碼讀取/proc虛擬檔案系統獲取當前系統中絕大多數的執行狀態資訊,一些開源的監控系統也是通過讀取/proc的方式獲取到監控資料的,並且Python下有一些工具包,諸如

psutils 封裝了這種方式能夠方便的獲取系統狀態資料,但在這個demo中我們不希望安裝其他的外部依賴,並且希望能夠自定義資料獲取後封裝的格式,所以我們自己簡單封裝了一下相關的功能。

首先是監控功能的基類PollsterClass.py,具體的監控類可以繼承此類並實現getSample方法實現具體的監控邏輯,新的監控功能的擴充套件也是通過這種方式完成。本例給出關於獲取CPU資訊的例子: 

#PollsterClass.py
'''
The base class of pollster
'''
class Pollster(object):
    def __init__(self, name):
        self.name = name

    '''
    Implement this method.
    '''
    def getSample(self):
        pass

#cpu.py
from collections import OrderedDict
import util
import time
from PollsterClass import Pollster

‘’’
Read cpu info from /proc/cpuinfo
‘’’
class CPUInfoPollster(Pollster):
    def __init__(self, name='cpu_info'):
        super(CPUInfoPollster, self).__init__(name=name)

    def getSample(self):
        cpu_info = OrderedDict()
        proc_info = OrderedDict()

        nprocs = 0

        try:
            if util.is_exist('/proc/cpuinfo'):
                with open('/proc/cpuinfo') as f:
                    for line in f:
                        if not line.strip():
                            cpu_info['proc%s' % nprocs] = proc_info
                            nprocs += 1
                            proc_info = OrderedDict()
                        else:
                            if len(line.split(':')) == 2:
                                proc_info[line.split(':')[0].strip()] = line.split(':')[1].strip()
                            else:
                                proc_info[line.split(':')[0].strip()] = ''
        except:
            print "Unexpected error:", sys.exc_info()[1]
        finally:
            return cpu_info

2.     守護程序化

根據前一篇部落格所述,我們通過一個TestMonitor類繼承了Daemon類並實現run方法,實現監控輪詢的基本功能。本例中載入了CPUInforPollster類實現對cpu資訊的獲取,設定監控時間間隔為10s,每次輪詢都將得到的資訊存入logfile檔案中。

import sys, time, datetime
import json

from DaemonClass import Daemon
from collections import OrderedDict
from cpu import CPUInfoPollster
import util
import re

class TestMonitor(Daemon):
    intvl = 10
    def __init__(self,
               pidfile='/tmp/test-monitor.pid',
               stdin='/dev/stdin',
               stdout='/dev/stdout',
               stderr='/dev/stderr',
               intvl=10,
               logfile='/opt/monitor.log'):
        Daemon.__init__(self, pidfile=pidfile, stdin=stdin, stdout=stdout, stderr=stderr)
        # Set poll interval
        TestMonitor.intvl = intvl
        # Set logfile
        self._logfile = logfile
    

    '''
    Basic poll task
    '''
    def _poll(self):
        # Get cpu info
        cpu_info = CPUInfoPollster().getSample()

        poll_info = OrderedDict()
    
        poll_info['cpu_info'] = cpu_info

        return cpu_info

    def run(self):
	c = 0
        while True:
            poll_info = self._poll()
            # Add timestamp
            content = time.asctime(time.localtime()) + '\n'
            for item in poll_info:
                content += '%s: %s\n' %(item, poll_info[item])
            content += '----------------------------\n\n'
            util.appendFile(content, self._logfile)
            time.sleep(TestMonitor.intvl)
            c = c + 1
            
if __name__ == "__main__":
    daemon = TestMonitor(pidfile='/tmp/test-monitor.pid', 
                           intvl=10)
   
    if len(sys.argv) == 2:
        if 'start' == sys.argv[1]:
            daemon.start()
        elif 'stop' == sys.argv[1]:
            daemon.stop()
        elif 'restart' == sys.argv[1]:
            daemon.restart()
        else:
            print 'Unknown command'
            sys.exit(2)
    else:
        print 'USAGE: %s start/stop/restart' % sys.argv[0]
        sys.exit(2)

至此,已經完成了監控的基本功能,可以通過以下命令完成對服務的啟動、停止以及重啟。

# Start daemon
python monitor.py start 

#Stop daemon
python monitor.py stop

#Restart daemon
python monitor.py restart

3. 監控引數的動態指定

如果現在就結束工作,那麼這個程式只能以固定的頻率輪詢固定的監控項,無法實現擴充套件性以及可控性。因此希望在此基礎上繼續修改,希望能夠在服務執行期間:

a). 動態設定輪詢時間

b). 動態載入監控功能項

這時候會遇到一個問題,有前一篇blog所述,通過Daemon構建的守護程序由於已經切斷了同進程組,會話組的關係,所以不可能直接去設定程序中的引數,並且每次執行設定操作其實都是建立了一個新的程序,所以無法使用全域性變數等方式傳遞當前的監控設定,本例採用了一個簡單的方法,即使將監控引數記錄到一個檔案"conf"中,每次在修改監控引數、重啟操作開始前先讀取儲存的監控引數,設定完畢後再將引數更新到檔案中,因此動態設定引數的過程在程式碼中稍顯複雜,如果大家有甚麼更好的方法,可以給我留言或發郵件,非常感謝。

#MonitorManager.py

import sys, time, datetime
import json, re
import util
from collections import OrderedDict
from DaemonClass import Daemon

class MonitorManager(Daemon):
    '''
    {'cpu_info':{'cls':CPUInfoPollster(), 'is_active':True},...}
    '''
    _intvl = None
    _pollsters = OrderedDict()
    _logfile = None

    def __init__(self,
               pidfile='/tmp/test-monitor.pid',
               stdin='/dev/stdin',
               stdout='/dev/stdout',
               stderr='/dev/stderr',
               intvl=10,
               logfile='/opt/monitor.log'):
        super(MonitorManager, self).__init__(pidfile=pidfile, stdin=stdin, stdout=stdout, stderr=stderr)

        paras = util.load_conf('conf')

        MonitorManager._logfile = logfile

        if not paras.has_key('intvl'):
            MonitorManager._intvl = intvl
            paras['intvl'] = intvl
        else:
            MonitorManager._intvl = int(paras['intvl'])

        if paras.has_key('pollsters'):
            tmp_list = eval(paras['pollsters'])
            for poll in tmp_list:
                p_name, cls = util.load_class(poll)
                if p_name and cls:
                    MonitorManager._pollsters[p_name] = cls()
        else:
            MonitorManager._pollsters = OrderedDict()

        util.update_conf('conf', paras)

    '''
    Set poll interval in running.
    '''
    def set_intvl(self, intvl):
        # load current settings, including interval and pollster list

        paras = util.load_conf('conf')
        if intvl >= 1:
            MonitorManager._intvl = intvl
            paras['intvl'] = intvl

            # update settings
            util.update_conf('conf', paras)
            self.restart()

    ‘’’
    Set pollster list in running.
    ‘’’
    def set_pollsters(self, poll_list):
        # load current settings, including interval and pollster list
        paras = util.load_conf('conf')
        MonitorManager._pollsters = OrderedDict()
        for poll in poll_list:
            p_name, cls = util.load_class(poll)
            if p_name and cls:
                MonitorManager._pollsters[p_name] = cls()
        if poll_list:
            paras['pollsters']='%s' %poll_list

            
            util.update_conf('conf', paras)
        self.restart()

    '''
    Execute poll task
    '''
    def _poll(self):
        poll_data = OrderedDict()
        if MonitorManager._pollsters:
            #poll all pollsters
            for pollster in MonitorManager._pollsters:
                poll_data[pollster] = {}
                poll_data[pollster]['timestamp'] = time.asctime(time.localtime())
                #Get monitor data from getSample method in each pollsters
                poll_data[pollster]['data'] = MonitorManager._pollsters[pollster].getSample()
        return poll_data

    def run(self):
        c = 0
        while True:
            poll_info = self._poll()
            content = time.asctime(time.localtime()) + '\n'
            for item in poll_info:
                content += '++++%s: %s\n' %(item, str(poll_info[item]))
            content += '----------------------------\n\n'
            util.append_file(content, MonitorManager._logfile)
            time.sleep(MonitorManager._intvl)
            c = c + 1

if __name__ == "__main__":
    daemon = MonitorManager(pidfile='/tmp/test-monitor.pid', 
                           intvl=10)
   
    if len(sys.argv) == 2:
        if 'start' == sys.argv[1]:
            daemon.start()
        elif 'stop' == sys.argv[1]:
            daemon.stop()
        elif 'restart' == sys.argv[1]:
            daemon.restart()
else:
            print 'Unknown command'
            sys.exit(2)
    elif len(sys.argv) == 3:
        if 'setintvl' == sys.argv[1]:
            if re.match(r'^-?\d+$', sys.argv[2]) or re.match(r'^-?(\.\d+|\d+(\.\d+)?)', sys.argv[2]):
                daemon.set_intvl(int(sys.argv[2]))
                print 'Set interval: %s' %sys.argv[2]
        elif 'setpoll' == sys.argv[1]:
            poll_list = None
            try:
                poll_list = eval(sys.argv[2])
            except:
                print '%s is not a list.' %sys.argv[2]
            if poll_list:
                daemon.set_pollsters(poll_list)
    else:
        print 'USAGE: %s start/stop/restart' % sys.argv[0]
        sys.exit(2)

該專案已經放在github上:https://github.com/kevinjs/procagent

擴充套件思考:

目前此demo只完成了最簡單的輪詢任務,設定監控引數等功能。如果放在雲平臺的虛擬機器中同其他監控框架配合,必須要考慮監控設定命令的傳入以及監控資料的傳出問題;同時,如何在一個守護程序的模式下完成對不同監控項不同輪詢週期的設定也是需要思考的問題。