Linuxデーモンメカニズムを利用して簡単なシステム監視demoを完成する


前編「Linuxデーモン設計仕様およびPython実装」によれば、Pythonベースのデーモンフレームワークを提供し、このフレームワークを使用して独自のデーモンを構築するには、Daemonクラスを継承しrunメソッドを実現するだけでよい.本稿では,この考え方に従ってlinuxシステム状況モニタリングプログラムを設計する.
現在、コミュニティにはGanglia、Zabbixなどのオープンソースのシステム監視ソフトウェアがたくさんあります.これらのソフトウェアは優れた性能と豊富な機能で多くの運営・メンテナンスエンジニアの愛顧を受けていますが、多くの場合、これらのソフトウェアの大部分の機能を利用することはできません.また、これらのソフトウェアをインストールして構成するには、多くの依存ソフトウェアをインストールする必要があります.信頼性が高く、使いやすく、軽量レベルのシステム状態のタイミングコレクターが必要かもしれません.このコレクターは、他のより複雑なモジュールと組み合わせて分散システム全体の監視を完了します.例えば筆者の現在の仕事はOpenStackの物理資源と仮想資源を統一的に監視することを望んでいるが、OpenStackが持っている監視モジュールCeilometerには合理的な機能フレームワークモデルがあるが、具体的な監視データの取得という階層に細分化するのはよくないので、この階層でCeilometerにいくつかの改善と補充を行うことを望んでいる.
 
このモニタリングプログラムには次のような特徴があることを望んでいます.
1.使いやすさ:複雑な依存関係がなく、複雑なインストール構成を必要としない
2.拡張性:監視機能が拡張しやすい
3.安定性:運行が安定し、過度な介入を必要とせず、システム資源を占有できない
4.制御性:サービスを制御可能(起動、停止、再起動、監視パラメータ設定)
 
実現構想:
モニタリングの基本的な機能要件に基づいて、モニタリングデータの取得とデーモン化の2つの問題を解決する必要があります.
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メソッドを実装し,ポーリングを監視する基本機能を実現した.この例では、CPU InforPollsterクラスをロードしてcpu情報の取得を実現し、監視時間間隔を10 sに設定し、ポーリングのたびに得られた情報を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()) + '
' for item in poll_info: content += '%s: %s
' %(item, poll_info[item]) content += '----------------------------

' 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). 動的ロード監視機能項目
前のブログで述べたように、Daemonによって構築されたデーモンプロセスは、プロセスグループ、セッショングループとの関係が切断されているため、プロセス内のパラメータを直接設定することは不可能であり、設定操作を実行するたびに実際に新しいプロセスが作成されるため、グローバル変数などを使用して現在の監視設定を伝達することはできません.この例では、モニタリングパラメータを1つのファイル「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()) + '
' for item in poll_info: content += '++++%s: %s
' %(item, str(poll_info[item])) content += '----------------------------

' 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は最も簡単なポーリングタスク、監視パラメータの設定などの機能しか完了していません.クラウドプラットフォームの仮想マシンに配置して他のモニタリングフレームワークと協力する場合は、モニタリング設定コマンドの転送とモニタリングデータの転送の問題を考慮する必要があります.また、1つのデーモン・プロセスのモードで異なるモニタリング・アイテムに対する異なるポーリングサイクルの設定をどのように完了するかも考慮する必要があります.