利用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只完成了最简单的轮询任务,设置监控参数等功能。如果放在云平台的虚拟机中同其他监控框架配合,必须要考虑监控设置命令的传入以及监控数据的传出问题;同时,如何在一个守护进程的模式下完成对不同监控项不同轮询周期的设置也是需要思考的问题。

相关内容