接触 Supervisor 还是学 Golang 的时候,用于把 Golang 应用的非守护进程转化为守护进程。不过这次到是要把 Supervisor 用到 Java 进程上,具体的操作和 Golang 应用的进程并无差别,毕竟 Supervisor 只要求被管理的进程是非守护进程即可。
Why 公司项目里的一个 Java 工程里全是 Main-Class,其中一些需要在后台一直运行(Thrift 服务、Timer 什么的),为了保证这些服务崩溃了能自动重启,crontab 里边每分钟会去尝试运行一次(从上个项目遗留下来的做法,当然,Java 程序里会判断是不是已启动)。前几天我们的一台阿里云服务器突然 CPU 100% 挂了(巧的是,我的一台阿里云 ECS 在 20 分钟前也是因为该问题挂了,和公司的还是同一个区的,怀疑是不是阿里的问题),排查半天没发现异常情况,之后就重点监控 CPU 占用率比较高的进程,发现每分钟都会有 Java 进程的 CPU 突然飙到 200% 并瞬间消失。 其实这些个进程就是我们在 crontab 中每分钟 check 的那些 Main-Class,启动后发现该服务已启动后直接 exit 了(除了 check 一下并没有执行任何方法)。那为什么 CPU 占用会瞬间 200%?这是因为 JVM 在启动的时候会并连接所有除反射以外的类,而 class 文件是二进制的文件,需要从磁盘加载到内存然后解析,这种解析是很耗费 CPU 的,class 文件越多,CPU 耗费就越高。 虽然可能并不是该问题导致的服务器挂掉,但还是得优化。然后我就想到了用 Supervisor 去管理这些 Java 进程。(不用这些进程管理工具话其实也可以用 shell 脚本去 check) 配置 Supervisor 去管理这几个 Java 进程添加了几个文件就搞定了。虽然 Supervisor 能在被监控程序异常中断时自动重启之,但是还是希望程序异常时能报警通知,这就需要用到 Supervisor 的 Event 和 Listener 了。
Mechanism Supervisor 官方对其 Event 机制的描述是:一个进程的监控/通知框架 。
该机制主要通过一个 event listener 订阅 event 通知实现。当被 Supervisor 管理的进程有特定行为的时候,supervisor 就会自动发出对应类型的 event。即使没有配置 listener,这些 event 也是会发的;如果配置了 listener 并监听该类型的 event,那么这个 listener 就会接收到该 event。 event listener 需要自己实现,并像 program 一样,作为 superviosr 的子进程运行。 其实被管理的进程自身也可以发 event,进而主动和 supervisor 通信,不过不想让程序依赖 supervisor,就不搞了。
Event Types Event Type 由 supervisor 官方定义,我们是没法自己添加的,不过官方已经定义了很多类型了,够我们使用的了。 全部的 Event Type 可以参考文档:http://supervisord.org/events.html#event-types
这里以其中一个类型为例PROCESS_STATE_EXITED
,顾名思义,当被管理的子进程退出的时候,就会产生该 event。(关于进程状态请参考 http://supervisord.org/subprocess.html#process-states ) 当 supervisord 发送一个 event 到 listener 时,会先发送一个“header”过去,类似这样
1 ver:3.0 server:supervisor serial:21 pool:listener poolserial:10 eventname:PROCESS_COMMUNICATION_STDOUT len:54
header 中每项的含义:
Key
Description
var
event 协议类型,目前 3.0
server
supervisor 的标识符,对应配置文件中[supervisord]块的 identifier
serial
event 的序列号
pool
listener 的 pool 的名字,如果 listener 只启动了一个进程,也就没有 pool 的概念了
poolserial
eventpool 给发送到我这个 pool 过来的 event 编的号,有点绕,只要知道与上边的 serial 不同就行了
eventname
event 类型名称
len
header 后面的 payload 部分的长度,又称PAYLOAD_LENGTH
对于不同的 event type,header 的结构都是一样的,而 payload 的数据结果与类型相关。PROCESS_STATE_EXITED
的 payload 结构如下:
1 processname:cat groupname:cat from_state:RUNNING expected:0 pid:2766
该 payload 中每项的含义:
Key
Description
processname
进程名 cat [program:cat]
groupname
进程组名
from_state
进程在退出前是什么状态
expected
默认情况下 exitcodes=0,2,当退出码为 0 或 2 时,是 expected 的,此时该值为 1;其它的退出码,也就是 unexpected 了,该值为 0
pid
退出的进程的 pid
Event Listener 编写 listener 之前,先了解一下 listener states 以及 listener 与 event 的通信协议。
Event Listener States 一个 listener 进程可能会处于以下三种状态:
Name
Description
ACKNOWLEDGED
确认,相当于注册上了这个 listener
READY
就绪,event 可以被发送到这个 listener
BUSY
忙碌,event 不能被发送到这个 listener
当一个 listener 启动后,会进入ACKNOWLEDGED
状态。然后它会向自己的 stdout 写一个READY\n
,进入READY
状态。supervisor 向READY
状态的 listener 发一个 event 后,listener 进入BUSY
状态。listener 返回OK
或FAIL
后,回到ACKNOWLEDGED
状态。
Event Listener Notification Protocol
listener 处于READY
时,当 supervisord 产生的 event 在 listener 的配置的可接受的 events 中时,supervisord 就会把该 event 发送给该 listener,并将其状态置为BUSY
。
listener 先处理header
获取len
并据其读取payload
,处理payload
中的数据,这时候如果有相同类型的 event 产生,supersivor 会将该 event 发给相同 listener pool 中的其他 listener。
listerner 处理完数据后,要向自己的 stdout 中写一条消息以告诉 supervisor 处理结果,例如RESULT 2\nOK
或RESULT 4\nFAIL
supervisor 收到 listener 返回的结果,若为OK
就认为处理成功;若为FAIL
,认为 event 处理失败,会把那个 event 再次放入缓存队列并稍后再次发送。不管收到OK
还是FAIL
,这个 listener 都会被置为ACKNOWLEDGED
状态。
listener 被置为ACKNOWLEDGED
状态后,这个 listener 进程可以退出并稍后自启(配置中autorestart=true
的话),也可以继续运行。如果要继续运行,则它必须立即向自己的 stdout 中发送READY
以让 supervisor 将其状态置为READY
。
Writing an Event Listener 先看一个官方的用 python 写的 demo 了解一下流程,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import sysdef write_stdout (s ): sys.stdout.write(s) sys.stdout.flush()def write_stderr (s ): sys.stderr.write(s) sys.stderr.flush()def main (): while 1 : write_stdout('READY\n' ) line = sys.stdin.readline() write_stderr(line) headers = dict ([ x.split(':' ) for x in line.split() ]) data = sys.stdin.read(int (headers['len' ])) write_stderr(data) write_stdout('RESULT 2\nOK' ) if __name__ == '__main__' : main() import sys
从理论上来说,supervisor 的 event listener 可以用任何语言实现,但是 python 是实现起来比较容易的,因为有一个名为supervisor.childutils
模块可以方便我们处理 event。另外 superlance 这个 package 中,有几个 event listener 的栗子,哦对了,这是个 python 的 package。既然轮子已经有了,我也就不重复造了,直接在其基础上修改了。
配置文件:
1 2 3 [eventlistener:crashmail] command =/home/crashmail.py -o hostname -a -m your@email.com -s '/usr/sbin/sendmail -t -i -f from@email.com' events =PROCESS_STATE_EXITED
关于 event listener 更多的配置参数就不赘述了,可以参考:《Supervisor 基础》
监控进程退出并发报警邮件的 listener:(改自 superlance 中的 crashmail.py)
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 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 import osimport socketimport sysfrom supervisor import childutilsdef usage (): print doc sys.exit(255 )class CrashMail : def __init__ (self, programs, any , email, sendmail, optionalheader ): self.programs = programs self.any = any self.email = email self.sendmail = sendmail self.optionalheader = optionalheader self.stdin = sys.stdin self.stdout = sys.stdout self.stderr = sys.stderr def runforever (self, test=False ): while 1 : headers, payload = childutils.listener.wait(self.stdin, self.stdout) if test: self.stderr.write(str (headers) + '\n' ) self.stderr.write(payload + '\n' ) self.stderr.flush() if not headers['eventname' ] == 'PROCESS_STATE_EXITED' : childutils.listener.ok(self.stdout) continue pheaders, pdata = childutils.eventdata(payload + '\n' ) if int (pheaders['expected' ]): childutils.listener.ok(self.stdout) continue hostname = socket.gethostname() ip = socket.gethostbyname(hostname) msg = "Host: %s(%s)\nProcess: %s\nPID: %s\nEXITED unexpectedly from state: %s" % \ (hostname, ip, pheaders['processname' ], pheaders['pid' ], pheaders['from_state' ]) subject = ' %s crashed at %s' % (pheaders['processname' ], childutils.get_asctime()) if self.optionalheader: subject = '[' + self.optionalheader + ']' + subject self.stderr.write('unexpected exit, mailing\n' ) self.stderr.flush() self.mail(self.email, subject, msg) childutils.listener.ok(self.stdout) def mail (self, email, subject, msg ): body = 'To: %s\n' % self.email body += 'Subject: %s\n' % subject body += '\n' body += msg m = os.popen(self.sendmail, 'w' ) m.write(body) m.close() self.stderr.write('Mailed:\n\n%s' % body) self.mailed = bodydef main (argv=sys.argv ): import getopt short_args = "hp:ao:s:m:" long_args = [ "help" , "program=" , "any" , "optionalheader=" "sendmail_program=" , "email=" , ] arguments = argv[1 :] try : opts, args = getopt.getopt(arguments, short_args, long_args) except : usage() programs = [] any = False sendmail = '/usr/sbin/sendmail -t -i' email = None optionalheader = None for option, value in opts: if option in ('-h' , '--help' ): usage() if option in ('-p' , '--program' ): programs.append(value) if option in ('-a' , '--any' ): any = True if option in ('-s' , '--sendmail_program' ): sendmail = value if option in ('-m' , '--email' ): email = value if option in ('-o' , '--optionalheader' ): optionalheader = value if not 'SUPERVISOR_SERVER_URL' in os.environ: sys.stderr.write('crashmail must be run as a supervisor event ' 'listener\n' ) sys.stderr.flush() return prog = CrashMail(programs, any , email, sendmail, optionalheader) prog.runforever(test=True )if __name__ == '__main__' : main() doc = """\ crashmail.py [-p processname] [-a] [-o string] [-m mail_address] [-s sendmail] URL Options: -p -- specify a supervisor process_name. Send mail when this process transitions to the EXITED state unexpectedly. If this process is part of a group, it can be specified using the 'process_name:group_name' syntax. -a -- Send mail when any child of the supervisord transitions unexpectedly to the EXITED state unexpectedly. Overrides any -p parameters passed in the same crashmail process invocation. -o -- Specify a parameter used as a prefix in the mail subject header. -s -- the sendmail command to use to send email (e.g. "/usr/sbin/sendmail -t -i"). Must be a command which accepts header and message data on stdin and sends mail. Default is "/usr/sbin/sendmail -t -i". -m -- specify an email address. The script will send mail to this address when crashmail detects a process crash. If no email address is specified, email will not be sent. The -p option may be specified more than once, allowing for specification of multiple processes. Specifying -a overrides any selection of -p. A sample invocation: crashmail.py -p program1 -p group1:program2 -m dev@example.com """
坑 & 爬坑 虽然 eventlistener 在配置文件中的地位上和 program 是相同的,但是待遇那可是不一样的。在使用的过程中我就遇到了两个坑,现记录如下。
1 号坑 1 号坑属性: 如果修改修改一个 program 的配置,使用supervisorctl update
即可载入最新的配置并按需重启对应的 program,在终端里的表现如下:
1 2 3 4 $ supervisorctl update TestCrashMail: stopped TestCrashMail: updated process group $
但是对于一个 eventlistener,当修改完对应的配置后,使用supervisorctl update
是毫无反应的:
restart
一下这个 listener?图样图森破,start
、restart
、stop
这三个命令都是不会载入最新的配置文件的!
瞅了全部的supervisorctl
的命令,发现只有一个能让 listener “满血复活”的——reload
。但是这个命令是重启 supervisord 的,被其管理的所有程序当然会全部重启,如果你只有一两个程序是用 supervisord 管理的,还能忍受;但是如果像我这样有几十个程序的,显然这么做不合适。那怎么办?
爬坑攻略: 首先该方法只适用于配置分离的情况(至于把 eventlistener 直接写到 supervisord 的配置文件中是否有这个坑还不确定)。
比如我的 supervisord.conf 文件中有如下配置:
1 2 [include] files = /opt/supervisor/conf.d/*.conf
/opt/supervisor/conf.d/
文件夹下有一个 listener 的配置crashmail.conf
,那么通过以下命令即可顺利爬出坑:
1 2 3 4 5 6 7 $ mv crashmail.conf crashmail.bak $ supervisorctl update crashmail: stopped crashmail: removed process group $ mv crashmail.bak crashmail.conf $ supervisorctl update crashmail: added process group
2 号坑 坑前提示:只有始终运行的 listener 会遇到该坑;运行一次后退出并自动重启的 listener 无视此坑。
2 号坑属性: 一般配置 program 的时候,我们会配置redirect_stderr=true
选项以使stderr
重定向到stdout
中,这样可以少一个 log 文件,方便查看。 但是如果 listener 中配置redirect_stderr=true
就可能会出问题——取决于你的 listener 在处理的过程中会不会向stderr
中有输出。 具体现象就是该 listener 只能处理一个 event,然后就卡在那里了,如果你重启了该 listener,它还会接收到上次的那个 event,进行处理后如此循环。
爬坑攻略: 对于 eventlistener,不要配置redirect_stderr=true
。supervisor 的 event 通信协议比较特殊,需要从stdout
中的输出来判断 listener 的状态(详见上文中的详解),所以stderr
重定向到stdout
的输出可能会干扰 supervisor 对 listener 状态的判断。
2015-12-24 更新 :3.2.0 版本中已经把[eventlistener:x]
中的redirect_stderr=true
设置禁用了,如果设置了的话会在启动 supervisord 时出错:
1 2 3 $ supervisord -c /etc/supervisord.conf Error: [eventlistener:listener-crashmail] section sets redirect_stderr=true but this is not allowed because it will interfere with the eventlistener protocol For help , use /usr/local/bin/supervisord -h
参考资料:Supervisor Docs supervisor(二)event superlance