一个监控脚本带来的Python实践和学习
工作中经常要和服务器、数据库这些打交道,有些小功能例如:服务监控、数据监控等用java往往无处下手,即使能做也要花费很长的时间,身边好几个同事都会Python,面对这些需求他们往往优先选择Python来实现,又快又省事。所以我也计划着自学一下Python,寻思着买一本入门的书来参考,在豆瓣上挑来挑去最后挑了《Head first Python》这本。买来后断断续续看了好几章了,对Python也有了一些基本的了解,不过一直没真正上手。
废话太多,直接步入正题,昨天领导让我写个脚本来实时监控某个服务的日志,监控日志中报出的用户购买超时的现象,如果某个node累计超时次数达到10次则通过邮件和短信告警。
?
需求就是上面说的这样,虽然Python用的还不熟练,不过当下也没其它好的方式来实现,没办法,赶鸭子上架,现学现用,如果碰到不会的就借助于万能的互联网吧
?
一、Python调用shell
首先考虑实现的方式,由于领导要求实时监控,所以当前想到的方式就是使用linux shell的tail命令扫描服务日志,然后再使用关键字来grep,根据grep出来的日志内容进行分析。这时候碰到了第一个问题,如何在Python中执行linux shell命令呢?
?
没办法,上网求助,网上有人推荐使用os.system,不过大部分都推荐使用subprocess模块来实现,通过官方文档说明,我决定使用subprocess
官方给出的文档里有这么一段话:
import subprocess要调用shell命令,subprocess有很多方式,比如call、Popen等,这里我们使用Popen
首先看一个最简单的实例:
subprocess.Popen('ls -l',stdout=subprocess.PIPE,stderr=subprocess.PIPE,shell=True)
上面的代码构建了一个Popen对象,Popen的构造函数格式如下:
subprocess.Popen(args, bufsize=0, executable=None, stdin=None, stdout=None, stderr=None, preexec_fn=None, close_fds=False, shell=False, cwd=None, env=None, universal_newlines=False, startupinfo=None, creationflags=0)
参数args可以是字符串或者序列类型(如:list,元组),用于指定进程的可执行文件及其参数。如果是序列类型,第一个元素通常是可执行文件的路径。
参数stdin, stdout, stderr分别表示程序的标准输入、输出、错误句柄。他们可以是PIPE,文件描述符或文件对象,也可以设置为None,表示从父进程继承。
如果参数shell设为true,程序将通过shell来执行。
关于其它具体的参数的说明,参见官方文档:http://docs.python.org/2/library/subprocess.html#popen-constructor
不想看英文的可以看网上的另一篇博客:http://blog.csdn.net/wuwangyingzhong/article/details/6002055
?
我的代码逻辑如下:
#日志文件目录和名称filename = '/home/project/logs/xxx.log'#要执行的shell命令command='tail -f '+filename+'|grep "timeout"'popen=subprocess.Popen(command,stdout=subprocess.PIPE,stderr=subprocess.PIPE,shell=True)while True:#获取当前grep出来的日志文本行line=popen.stdout.readline().strip()#对日志文本进行分析(这里省略)
?
二、Python发送http请求
上面已经提到,告警的方式为邮件和短信,我们的短信推送接口是一个http接口,所以如果需要短信告警,则需要通过python来请求该http接口。python中一共有urllib、urllib2、httplib、httplib2支持发送http请求,由于服务器上没有安装python的httplib和httplib2模块,所以只能使用urllib,urllib2
使用GET请求来请求短信推送接口,可以把参数写在url中,如果使用POST方式来请求接口,则数据必须放在data或者body中,不能放在url中,放在url中将被忽略。
附上一个通过urllib,urllib2发送GET请求的示例:
import urllib,urllib2def main(): url="http://www.baidu.com/s?wd=douban" req = urllib2.Request(url) print req res_data = urllib2.urlopen(req) res = res_data.read() print resif __name__ == '__main__': main()
关于python对http请求的操作,可以参见博客:http://blog.163.com/xychenbaihu@yeah/blog/static/132229655201231085444250/
?
三、脚本进程和子进程的退出
发现了2个问题:
问题1:在上面的代码中,python调用的shell tail命令作用是实时监控当前的日志,而服务是24小时不间断的,该python脚本也一直同步执行,所以只能使用nohup方式后台启动。每次要关闭该脚本,手动通过进程pid来kill。带来了一个新的问题,每次kill掉python主进程时,主进程执行时启动的Popen子进程未关闭,由于调试时曾经连续启动->kill->启动->kill多次,导致系统中出现了一大堆tail日志的进程,这是我原来没有预料到的。
问题2:我们的日志是生成的dailyRolling日志,文件名称为:xxx_dailyRolling.log,每天的0点会自动重命名为xxx_dailyRolling.logyyyyMMdd,yyyyMMdd代表前一天的日期,例如xxx_dailyRolling.log20131023,如果tail -f xxx_dailyRolling.log命令一直执行,到0点时会中断,所以我还需要在每天0点前停止当前的python脚本,然后在第二天0点后定时启动此python脚本
?
综上,2个问题一起解决,在0点前停止python脚本前先kill掉Popen子进程,然后再停止就OK了。
我的思路时每天设置一个固定的时间点,例如就定为每天的23:30:00(因为我们的服务在凌晨时基本上没人使用,监控的目的主要是为了监控白天高峰期时的服务情况,所以23:30到0点这段时间即使没监控也不影响),然后在Popen子进程执行时判断当前时刻是否到了23:30:00,如果已经到了,则终止子进程。通过实践证明,如果Popen子进程终止了,如果python主进程里没有挂起的其余子进程在执行,则主进程也会终止。
?
Popen可以通过terminate()或者kill()函数来终止当前的子进程。在执行了terminate()后,如果不想立即终止,可以通过wait()或poll()函数来等待子进程执行结束,关于各函数的使用,具体参考官方文档。
各函数说明如下:
#程序执行终止时间为当前时刻延迟15秒stoptime = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time()+15))def main(): popen=subprocess.Popen(command,stdout=subprocess.PIPE,stderr=subprocess.PIPE,shell=True) pid=popen.pid print('Popen.pid:'+str(pid)) while True: line=popen.stdout.readline().strip() print(line) #当前时间 thistime = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time())) if thistime>=stoptime: #终止子进程 popen.terminate() #等待子进程终止后跳出while循环 if subprocess.Popen.poll(popen) is not None: break else: print('waiting for subprocess.Popen terminate()') print('DONE')if __name__ == '__main__': main()?
四、Python2.x和3.x版本的区别(部分)
Python在2.x和3.x之间有一些特性区别比较大,由于我本地是安装的最新的3.2.2版本,而服务器上还是2.6.x,所以在编写调试时遇到了一些,也拿出来说说说:
区别1.print函数
在python 2.x版本中,同时支持print(xxx)和print xxx,示例:
>>> name='chenzhou'>>> print namechenzhou>>> print(name) chenzhou>>>
在python 3.x版本中,只支持print(xxx)的方式,示例:
>>> name='chenzhou'>>> print(name)chenzhou>>> print nameSyntaxError: invalid syntax>>>
区别2.判断字典中的key是否存在
在python 2.x中,可以使用has_key(key)函数,也可以判断 key in xxxMap.keys(),示例:
>>> users={'name':'chenzhou','age':'24','address':'Beijing'}>>> print users.has_key('name')True>>> print 'name' in users.keys()True>>> print users.has_key('score')False>>> print 'score' not in users.keys()True>>>
在python 3.x中,只支持key in xxxMap.keys() 示例:
>>> users={'name':'chenzhou','age':'24','address':'Beijing'}>>> print('name' in users.keys())True>>> print('score' in users.keys())False>>> print(users.has_key('name'))Traceback (most recent call last): File "<pyshell#20>", line 1, in <module> print(users.has_key('name'))AttributeError: 'dict' object has no attribute 'has_key'>>>
?
五、Python?UnicodeDecodeError
脚本写完后在linux下执行时报错:
UnicodeDecodeError: 'ascii' codec can't decode byte 0xe8 in position 0: ordinal not in range(128)
网友给出的原因是windows系统下编辑的文件在linux下编码转换出现的问题,具体解决方法如下:
#引入sys模块import sys#加入下面两行代码reload(sys)sys.setdefaultencoding('utf-8')
其它:
在grep关键字时用到了正则表达式,推荐一篇关于Python正则表达式的好博客:http://www.cnblogs.com/huxi/archive/2010/07/04/1771073.html
Python官网:http://www.python.org/
?
以上就是我第一次使用Python的经历,开始体会到了Python的优势。另外,也应了那句俗话,学习编程语言,光看书是没有效果的,只有理论与实践结合起来才能达到学习的效果,我想这应该就是别人经常提到的应用驱动学习吧。
?
最后,说明一下,我只是一个Python小菜鸟,写的东西很简单,大家不喜勿喷哈。