文章目录
  1. 1. 继续深入

服务器莫名宕机,没有检查到原因,怀疑某进程占用资源过高导致。

服务器莫名宕机,没有检查到原因,怀疑某进程占用资源过高导致。

于是使用top+crontab定时监视资源占用

TIPS:
1.crontab执行的shell脚本命令要用绝对路径
2.单独top命令不会自动退出,shell脚本必须要能够自动退出才会些数据到文件里面,所以一定要先单独测试会自动退出的带参数的top命令才行
3.内容写入文件是>,追加内容是>>

创建一个toptest.sh脚本,内容为:

1
2
3
4
5
6
#!/bin/sh
NAME="/root/top_"$(date +%Y-%m-%d-%H)
/usr/bin/top -b -d 1 -n 1 >> $NAME.txt
/bin/echo -e '----------------------------------------------------------------------------------------------------------------------------------------------------\n----------------------------------------------------------------------------------------------------------------------------------------------------\n----------------------------------------------------------------------------------------------------------------------------------------------------\n----------------------------------------------------------------------------------------------------------------------------------------------------\n----------------------------------------------------------------------------------------------------------------------------------------------------\n----------------------------------------------------------------------------------------------------------------------------------------------------\n' >> $NAME.txt
/usr/bin/top -b -d 1 -n 1 >> $NAME.txt
/bin/echo -e '----------------------------------------------------------------------------------------------------------------------------------------------------\n----------------------------------------------------------------------------------------------------------------------------------------------------\n----------------------------------------------------------------------------------------------------------------------------------------------------\n----------------------------------------------------------------------------------------------------------------------------------------------------\n----------------------------------------------------------------------------------------------------------------------------------------------------\n----------------------------------------------------------------------------------------------------------------------------------------------------\n' >> $NAME.txt

表示:每一秒更新一次,一共记录1次 将进程信息写入以日期命名的txt文件中
以echo -e写入换行,否则有误

1
chmod +x toptest.sh

接着

1
crontab -e

写入

1
*/10 * * * * /bin/bash /root/sh/toptest.sh

表示每十分钟记录两次top输出

如果系统再次崩溃或者是某个supervisor监控的进程异常退出,想要实现报警效果呢?使用superlance

superlance就是基于supervisor的事件机制实现的一系列命令行的工具集,它实现了许多supervisor本身没有实现的实用的进程监控和管理的特性,包括内存监控,http接口监控,邮件和短信通知机制等。同样的,superlance本身也是使用python编写的。

1
2
easy_install superlance
pip install superlance

superlance是一系列命令行工具的集合,其包括以下这些命令:

  • httpok
    通过定时对一个HTTP接口进行GET请求,根据请求是否成功来判定一个进程是否处于正常状态,如果不正常则对进程进行重启。
  • crashmail
    当一个进程意外退出时,发送邮件告警。
  • memmon
    当一个进程的内存占用超过了设定阈值时,发送邮件告警。
  • crashmailbatch
    类似于crashmail的告警,但是一段时间内的邮件将会被合成起来发送,以避免邮件轰炸。
  • fatalmailbatch
    当一个进程没有成功启动多次后会进入FATAL状态,此时发送邮件告警。与crashmailbatch一样会进行合成报警。
  • crashsms
    当一个进程意外退出时发送短信告警,这个短信也是通过email网关来发送的。

superlance是基于supervisor的,所以要利用Supervisord进行进程监控和报警需要利用Supervisord的Event特性,编写一个listener,监控进程状态的改变,然后执行指定的代码。event的发起方是supervisord进程,接收方是一个叫listener的东西。listener和program一样,都是supervisord的子进程。两者的在配置上,很多选项也都一样。

1.当supervisord启动的时候,如果我们的listener配置为autostart=true的话,listener就会作为supervisor的子进程被启动。
2.listener被启动之后,会向自己的stdout写一个"READY"的消息,此时父进程也就是supervisord读取到这条消息后,会认为listener处于就绪状态。
3.listener处于就绪状态后,当supervisord产生的event在listener的配置的可接受的events中时,supervisord就会把该event发送给该listener。  
4.listener接收到event后,我们就可以根据event的head,body里面的数据,做一系列的处理了。我们根据event的内容,判断,提取,报警等等操作。
5.该干的活都干完之后,listener需要向自己的stdout写一个消息"RESULT\nOK",supervisord接受到这条消息后。就知道listener处理event完毕了。

Supervisord支持的Event有:

PROCESS_STATE 进程状态发生改变
PROCESS_STATE_STARTING 进程状态从其他状态转换为正在启动(Supervisord的配置项中有startsecs配置项,
                          是指程序启动时需要程序至少稳定运行x秒才认为程序运行正常,在这x秒中程序状态为正在启动)
PROCESS_STATE_RUNNING 进程状态由正在启动转换为正在运行
PROCESS_STATE_BACKOFF 进程状态由正在启动转换为失败
PROCESS_STATE_STOPPING 进程状态由正在运行转换为正在停止
PROCESS_STATE_EXITED 进程状态由正在运行转换为退出
PROCESS_STATE_STOPPED 进程状态由正在停止转换为已经停止(exited和stopped的区别是exited是程序自行退出,而stopped为人为控制其退出)
PROCESS_STATE_FATAL 进程状态由正在运行转换为失败
PROCESS_STATE_UNKNOWN 未知的进程状态
REMOTE_COMMUNICATION 使用Supervisord的RPC接口与Supervisord进行通信
PROCESS_LOG 进程产生日志输出,包括标准输出和标准错误输出
PROCESS_LOG_STDOUT 进程产生标准输出
PROCESS_LOG_STDERR 进程产生标准错误输出
PROCESS_COMMUNICATION 进程的日志输出包含 和
PROCESS_COMMUNICATION_STDOUT 进程的标准输出包含 和
PROCESS_COMMUNICATION_STDERR 进程的标准错误输出包含 和
SUPERVISOR_STATE_CHANGE_RUNNING Supervisord启动
SUPERVISOR_STATE_CHANGE_STOPPING Supervisord停止
TICK_5 每隔5秒触发
TICK_60 每隔60秒触发
TICK_3600 每隔3600触发
PROCESS_GROUP Supervisord的进程组发生变化
PROCESS_GROUP_ADDED 新增了Supervisord的进程组
PROCESS_GROUP_REMOVED 删除了Supervisord的进程组

详细的Event特性请参考:http://supervisord.org/events.html
我们可以利用Supervisord的特性监控进程并报警,如当进程异常退出时报警,或当进程产生错误输出是报警。
superlance使用sendmail来发送email,默认安装的话sendmail在ubuntu server已经安装好

1
sudo apt-get install sendmail

这个地方有一个大坑,默认的时候crashmail使用的是linux系统的sendmail,发送出去的邮件很容易隐藏自己的信息,所以一般邮件服务商针对这些邮件会报错,大概是DSN, service unavailable, 貌似是你的ip地址和dns反解析不一致导致,所以无法发送邮件。
所以本文采用的实际上是sendemail,一个非常强大的使用perl语言写的支持附件发送等诸多功能的脚本。


1
2
3
4
5
wget http://caspian.dotconf.net/menu/Software/SendEmail/sendEmail-v1.56.tar.gz //下载1.56版本
tar -xzvf sendEmail-v1.56.tar.gz //解压后就可以使用了
cd sendEmail-v1.56
mv sendEmail /usr/local/bin/
chmod +x /usr/local/bin/sendEmail

使用很简单

1
2
3
4
5
6
7
8
9
/usr/local/bin/sendEmail    命令主程序
-f yxylinux@163.com      发件人邮箱
-s smtp.163.com      发件人邮箱的smtp服务器
-u "我是邮件主题"       邮件的标题
-o message-content-type=html 邮件内容的格式,html表示它是html格式
-o message-charset=utf8 邮件内容编码
-xu yxylinux 发件人邮箱的用户名
-xp 123456   发件人邮箱密码
-m "我是邮件内容"   邮件的具体内容

-xp 是smtp服务器的授权码。不使用smtp发送的方式也可以,而且秒发,只是qq邮箱会把发送过来的邮件当做垃圾邮件
更多姿势参考这里

在开始编辑supervisor配置文件mail.conf 前,先补点知识,crashmail是一个主管“事件监听器”,用于订阅PROCESS_STATE_EXITED事件。当crashmail 收到该事件并且转换是“意外的”时,crashmail 会将电子邮件通知发送到已配置的地址。
尤其注意的是

crashmail不能监视不是supervisord子进程的进程的进程状态。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[program:top]
command=/usr/bin/top -b
user=root
directory=/opt/
stdout_logfile=/var/log/top/top.log
stderr_logfile=/var/log/top/top.err
stderr_logfile_maxbytes=1MB
stderr_logfile_backups=10
stdout_logfile_maxbytes=1MB
stdout_logfile_backups=10
autostart=true
autorestart=true
startsecs=10

[eventlistener:crashmail-exited]
command=crashmail -p top -s "/usr/local/bin/sendEmail -f xxx@163.com -t xxx@qq.com -s smtp.163.com -u '报警' -xu xxx -xp xxx -o message-charset=utf-8 -m" -m xxx@qq.com
#events=PROCESS_STATE_EXITED # 一般只是这一句就好了,测试中发现,下面一坨和这一行的效果是一样一样的,只有exited的时候才会触发
events=PROCESS_STATE_EXITED,PROCESS_STATE_STOPPED,PROCESS_STATE_FATAL,PROCESS_LOG_STDERR
redirect_stderr=false

首先配置了一个名为top的进程监控项,其内容就是很简单的重复执行top -b,持续地输出当前系统的进程信息。
随后配置了一个名为crashmail的事件监听器,它接受来自supervisor的PROCESS_STATE_EXITED事件,并且会触发crashmail的命令行调用。
而在command参数中-p参数配置了crashmail只会对名为top的监控项作出响应。
在上面的command中最后的-m后面没有东西了,因为crashmail会通过stdin输送给命令,也就是为啥邮件正文中有to 和 from这些本该是邮件header的内容。
具体参考官档:http://superlance.readthedocs.io/en/latest/crashmail.html

以上间接通过不断运行top并记录实现了服务器资源占用情况的监控,如果服务器崩溃会产生记录并报警。想要测试发送邮件的功能,可以通过shell的ps指令找到top -b 的进程id,使用kill -9的方式强行结束进程

继续深入

supervisor的事件机制是一个简单的 Listener/Notification模型, Listener通过标准输入来获取supervisor发来的事件通知, 然后通过标准输出来告诉supervisor事件处理结果。过程中传递的EventNotification 由head和body两部分组成,监听器的处理事件流程为: readline()读取head -> 读取固定长度的data -> 输出状态信息
head的结构如下:

1
ver:3.0 server:supervisor serial:35 pool:event_listener poolserial:35 eventname:PROCESS_STATE_RUNNING len:91

ver: 版本信息
serial: supervisor给事件的编号, 第一个事件为1, 之后事件编号递增
pool:这个是你的listener的pool的名字,一般你的listener只启动一个进程的的话,其实也就没有  
eventpool: 产生event的event_listener名字
poolserial: 与serial不同的是, 由于可以有多个eventpool,而且eventpool可以检测的范围事件范围可以不同, 这个poolserial是相对某个eventpool的编号
eventname: supervisor 标准定义的事件状态
len: data长度, 此长度十分重要,需要再通过标准输入读入len长度的数据, 某个event_notification才算读取完毕

然后按照head的信息, 读入长度为len的数据, 这个数据就是event的data部分:

1
processname:application_demo_03 groupname:application_demo_03 from_state:STARTING pid:81292
processname: 触发事件的applicaiton名称
groupname: 触发事件的application的组名称
from_state: 事件触发状态之前的那个状态
pid: 进程id

于是有了代码,可以编写自己的接口函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
#!/usr/bin/env python
#coding:utf-8

import sys
import os
import subprocess
#childutils这个模块是supervisor的一个模型,可以方便我们处理event消息。。。当然我们也可以自己按照协议,用任何语言来写listener,只不过用childutils更加简便罢了
from supervisor import childutils
from optparse import OptionParser
import socket
import fcntl
import struct

__doc__ = "\033[32m%s,捕获PROCESS_STATE_EXITED事件类型,当异常退出时触发报警\033[0m" % sys.argv[0]

def write_stdout(s):
sys.stdout.write(s)
sys.stdout.flush()
#定义异常,没啥大用其实
class CallError(Exception):
def __init__(self,value):
self.value = value
def __str__(self):
return repr(self.value)
#定义处理event的类
class ProcessesMonitor():
def __init__(self):
self.stdin = sys.stdin
self.stdout = sys.stdout

def runforever(self):
#定义一个无限循环,可以循环处理event,当然也可以不用循环,把listener的autorestart#配置为true,处理完一次event就让该listener退出,然后supervisord重启该listener,这样listen#er就可以处理新的event了
while 1:
#下面这个东西,是向stdout发送"READY",然后就阻塞在这里,一直等到有event发过来
#headers,payload分别是接收到的header和body的内容
headers, payload = childutils.listener.wait(self.stdin, self.stdout)
#判断event是否是咱们需要的,不是的话,向stdout写入"RESULT\NOK",并跳过当前
#循环的剩余部分
if not headers['eventname'] == 'PROCESS_STATE_EXITED':
childutils.listener.ok(self.stdout)
continue

pheaders,pdata = childutils.eventdata(payload+'\n')
#判读event是否是expected是否是expected的,expected的话为1,否则为0
#这里的判断是过滤掉expected的event
if int(pheaders['expected']):
childutils.listener.ok(self.stdout)
continue

ip = self.get_ip('eth0')
#构造报警信息结构
msg = "[Host:%s][Process:%s][pid:%s][exited unexpectedly fromstate:%s]" % (ip,pheaders['processname'],pheaders['pid'],pheaders['from_state'])
#调用报警接口,换成自己的接口
subprocess.call("/usr/local/bin/alert.py -m '%s'" % msg,shell=True)
#stdout写入"RESULT\nOK",并进入下一次循环
childutils.listener.ok(self.stdout)


'''def check_user(self):
userName = os.environ['USER']
if userName != 'root':
try:
raise MyError('must be run by root!')
except MyError as e:
write_stderr( "Error occurred,value:%s\n" % e.value)
sys.exit(255)'''

def get_ip(self,ifname):
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
inet = fcntl.ioctl(s.fileno(), 0x8915, struct.pack('256s', ifname[:15]))
ret = socket.inet_ntoa(inet[20:24])
return ret


def main():
parser = OptionParser()
if len(sys.argv) == 2:
if sys.argv[1] == '-h' or sys.argv[1] == '--help':
print __doc__
sys.exit(0)
#(options, args) = parser.parse_args()
#下面这个,表示只有supervisord才能调用该listener,否则退出
if not 'SUPERVISOR_SERVER_URL' in os.environ:
try:
raise CallError("%s must be run as a supervisor event" % sys.argv[0])
except CallError as e:
write_stderr("Error occurred,value: %s\n" % e.value)

return

prog = ProcessesMonitor()
prog.runforever()

if __name__ == '__main__':
main()

或者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
#!/usr/bin/env python
#coding=utf-8
# Author zhyaof(mail@zhyaof.net)
'''
Suprevisord Listener example.
'''

import sys
import os

def write_stdout(s):
sys.stdout.write(s)
sys.stdout.flush()

def write_stderr(s):
sys.stderr.write(s)
sys.stderr.flush()

def baojing(msg=None, data=None):
if msg == None and data == None:
return
# alert

def parseData(data):
tmp = data.split('\n')
pheaders = dict([ x.split(':') for x in tmp[0].split() ])
pdata = None
if len(tmp) > 1:
pdata = tmp[1]
return pheaders, pdata


def main():
#Only supervisord can run this listener, otherwise exit.
if not 'SUPERVISOR_SERVER_URL' in os.environ:
print "%s must be run as a supervisor listener." % sys.argv[0]
return

while True:
#echo 'READY' and wait for event for stdin.
write_stdout('READY\n')
line = sys.stdin.readline() # read header line from stdin
headers = dict([ x.split(':') for x in line.split() ])
data = sys.stdin.read(int(headers['len'])) # read the event payload

if headers['eventname'] == 'PROCESS_STATE_EXITED' or\
headers['eventname'] == 'PROCESS_STATE_FATAL' or\
headers['eventname'] == 'PROCESS_STATE_STOPPED':
pheaders, pdata = parseData(data)
from_state = pheaders['from_state']
process_name = pheaders['processname']
if headers['eventname'] == 'PROCESS_STATE_EXITED' and\
not int(pheaders['expected']):
msg = '进程%s(PID: %s)异常退出,请检查进程状态.'\
% (process_name, pheaders['pid'])
baojing(msg=msg)
if headers['eventname'] == 'PROCESS_STATE_FATAL':
msg = '进程%s启动失败,请检查进程状态.'\
% (process_name)
baojing(msg=msg)
elif headers['eventname'] == 'PROCESS_LOG_STDERR':
pheaders, pdata = parseData(data)
process_name = pheaders['processname']
pid = pheaders['pid']
msg = '进程%s(PID: %s)错误输出,请检查进程状态,错误输出信息: %s.' \
% (process_name, pid, pdata)
baojing(msg=msg)
#echo RESULT
write_stdout('RESULT 2\nOK') # transition from READY to ACKNOWLEDGED

if __name__ == '__main__':
main()

其中报警函数可以自行拓展,如利用sendmail发邮件报警,利用微信接口实现微信报警或利用短信接口进行短信报警等。
本地测试,将报警函数写为

1
2
3
4
5
def baojing(msg=None, data=None):
# if msg == None and data == None:
# return
with open("/root/test.txt",'w')as f:
f.write(msg)

kill掉某个supervisor子进程,得到test.txt,内容为

1
进程top(PID: 19285)异常退出,请检查进程状态.

参考链接:

http://blog.csdn.net/baidu_zhongce/article/details/49151385
http://talk.withme.me/?p=318
http://lixcto.blog.51cto.com/4834175/1540169/

文章目录
  1. 1. 继续深入