Supervisor Event Listener 任务监控与告警

[toc]

Event

Event 是在 Supervisor 3.0 引入的一个高级特性,如果只简单使用 Supervisor 管理进程,则不需要了解 Event。

但如果希望监控 Supervisor 管理的进程的各种状态(如: 启动、退出、失败、退出状态码 ...)并支持告警,才需要学习 Event。

事件监听与事件通知

  • Event Listeners and Event Notifications

Supervisor 提供了一种基于订阅消息的通知机制,称为 event listener(事件监听者)。 用于订阅并处理事件通知消息。

Supervisor 在工作时,其下管理的进程发生任何的状态变化都会产生事件通知消息,这些消息被分为成了各种类型,在没有配置 event listener 时,这些消息将不会得到处理。当配置了event listener 可以在配置中指定订阅某一类型的事件通知消息,那么当指定类型的消息产生时 event listener 就会收到,以便进行下一步处理。

事件通知协议基于子进程的 stdin 和 stdout,Supervisor 会发送指定格式的消息数据给 event listener 进程的 stdin ,并期望从 event listener 的 stdout 返回一个指定格式的输出。 event listener 程序需要自己写代码实现,可以使用任何语言,不过在 Python 中有提供一个库 supervisor.childutils 专门用于快速开发 event listener ,因此用 python 开发是最简便的。

Event Types

Event Types 由 Supervisor 官方定义,覆盖了进程运行生命周期的各种状态。

  • 下面翻译一些常用的类型
Event 解释
PROCESS_STATE 进程状态发生改变
PROCESS_STATE_STARTING 进程状态从其他状态转换为正在启动(Supervisord的配置项中有startsecs配置项,是指程序启动时需要程序至少稳定运行x秒才认为程序运行正常,在这x秒中程序状态为正在启动)
PROCESS_STATE_RUNNING 进程从正在启动状态转换为正在运行状态
PROCESS_STATE_BACKOFF 进程从正在启动状态转换为启动失败状态,Supervisor 正在重启该进程
PROCESS_STATE_STOPPING 进程从正在运行状态或正在启动状态转换为正在停止状态
PROCESS_STATE_EXITED 进程从正在运行状态转换为退出状态,expected 退出码,如果是 0 表示进程异常退出,1 表示进程正常退出。
PROCESS_STATE_STOPPED 进程从正在停止状态转换为已停止状态
PROCESS_STATE_FATAL 进程从启动失败状态(BACKOFF)转换为失败状态(FATAL). 意味着 startretries 尝试次数已达上限,Supervisor 已放弃重启该进程。
PROCESS_LOG 进程产生日志输出,被管理的进程需配置,stdout_events_enabled=true or stderr_events_enabled=true 这个事件通知才会生效。
PROCESS_LOG_STDOUT 进程产生标准输出,被管理的进程需配置,stdout_events_enabled=true
PROCESS_LOG_STDERR 进程产生错误输出,被管理的进程需配置,stderr_events_enabled=true

配置一个事件监听者

  • Configuring an Event Listener

一个 supervisor 事件监听者通过语句 [eventlistener:x] 进行定义,一旦定义就是一个监听者组,官方称为 pool,使用 numprocs 语句 配置监听者的数量,也就是进程数。

events 语句用于配置监听的事件类型,监听者程序只能收到这里配置的事件类型消息。 在自己实现监听者程序时,应注意当程序收到未知事件通知时,也能识别异常妥善处理,不至于让程序奔溃。另外,监听者也是配置在 /etc/supervisord.conf 文件中。

  • 配置示例
'''固定写法:程序名,可随意定'''
[eventlistener:crashmaiExit]

'''当需要开多个进程时,需这样写'''
process_name=%(process_num)02d

'''监听者程序,可以用任何语言编写,需自己写代码实现'''
command= /usr/local/python27/bin/crashmail_exit.py

'''监听的事件,可以指定多个用逗号分隔。 这里的 TICK_60 表示通知间隔 60s'''
events=PROCESS_STATE_EXITED,TICK_60

'''错误输出日志路径'''
stderr_logfile=/alidata/log/supervisord/crashmail_exit_err.log

'''标准输出日志路径'''
stdout_logfile=/alidata/log/supervisord/crashmail_exit.log

'''自动重启'''
autostart=true
autorestart=unexpected

'''进程数'''
numprocs=2

实现一个事件监听者程序

  • Writing an Event Listener

Event Listener States

一个 Event Listener 有以下三种状态:

Name Description
ACKNOWLEDGED 注册,表示 Listener 被确认,即将可以开始工作
READY 就绪,表示随时可以接收事件通知
BUSY 繁忙,表示此时无法接收事件通知

Event Listener Notification Protocol

1、Listener 处于 READY 时,当 Supervisor 产生了在 Listener 配置的 event 时,Supervisor 就会把该 event 发送给该 Listener ,并将状态设置为 BUSY

2、Supervisor 在发送 event 时,会先发送一个 header ,Listener 需先处理 header 中的信息并根据其中的 len 读取 payload 信息,这时如果有相同类型的 event 产生,Supervisor 会将 event 发送给该 Listener 下的其他进程。

  • header 数据示例

    ver:3.0 server:supervisor serial:21 pool:listener poolserial:10 eventname:PROCESS_COMMUNICATION_STDOUT len:54
  • header 中每项的含义
Name 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
  • payload 数据示例

    processname:foo groupname:bar from_state:RUNNING expected:0 pid:276
  • payload 中每项含义
Name Description
processname 进程名
groupname 进程组名 [program:bar]
from_state 进程在退出前是什么状态
expected 默认情况下 exitcodes=0,2,当退出码为 0 或 2 时,是 expected 的,此时该值为 1;其它的退出码,也就是 unexpected 了,该值为 0
pid 退出的进程的 pid

3、处理完 payload 后,需要向自己的 stdout 写一条消息以告诉 Supervisor 处理结果,例如 RESULT 2\nOK 或 RESULT 4\nFAIL。

4、Supervisor 收到的返回结果如果是 OK 表示 event 处理成功,如果是 FAIL 表示 event 处理失败。

5、Supervisor 只要收到返回结果,无论 OK 还是 FAIL 都会将 Listener 转为 ACKNOWLEDGED 状态,一旦 Listener 进入 ACKNOWLEDGED 状态可以选择退出并自动重启(需配置 autorestart=true )或选择继续运行,如果选择继续运行,则需要向 stdout 写一条 READY 以告知 Supervisor 将自己状态转换为 READY

官方示例代码

import sys

def write_stdout(s):
    # only eventlistener protocol messages may be sent to stdout
    sys.stdout.write(s)
    sys.stdout.flush()

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

def main():
    while 1:
        # transition from ACKNOWLEDGED to READY
        write_stdout('READY\n')

        # read header line and print it to stderr
        line = sys.stdin.readline()
        write_stderr(line)

        # read event payload and print it to stderr
        headers = dict([ x.split(':') for x in line.split() ])
        data = sys.stdin.read(int(headers['len']))
        write_stderr(data)

        # transition from READY to ACKNOWLEDGED
        write_stdout('RESULT 2\nOK')

if __name__ == '__main__':
    main()

实战示例代码

#!/usr/local/python27/bin/python2.7
# -*- coding: utf-8 -*-

import os
import socket
import sys
from supervisor import childutils
import smtplib
from email.mime.text import MIMEText
from email.header import Header
from email.utils import parseaddr, formataddr

def _format_addr(s):
    name, addr = parseaddr(s)
    return formataddr(( \
        Header(name, 'utf-8').encode(), \
        addr.encode('utf-8') if isinstance(addr, unicode) else addr))

def send_mail(info):
    mail_host = "smtp.163.com"
    mail_user = "[email protected]"
    mail_pass = "123456"
    from_addr = '[email protected]'
    to_addr = ['[email protected]']

    msg = MIMEText(info, 'plain', 'utf-8')
    msg['From'] = _format_addr(u'supervisord 管理员 <%s>' % from_addr)
    msg['To'] = _format_addr(u'管理员 <%s>' % to_addr)
    msg['Subject'] = Header(u'测试 supervisord Exit', 'utf-8').encode()

    smtpObj = smtplib.SMTP_SSL(mail_host,465)
    smtpObj.login(mail_user,mail_pass)
    smtpObj.sendmail(from_addr, to_addr, msg.as_string())

class CrashMail:
    def __init__(self):

        self.stdin = sys.stdin
        self.stdout = sys.stdout
        self.stderr = sys.stderr

    def runforever(self, test=False):
        # 死循环, 处理完 event 不退出继续处理下一个
        while 1:
            # 使用 self.stdin, self.stdout, self.stderr 代替 sys.* 以便单元测试
            headers, payload = childutils.listener.wait(self.stdin, self.stdout)

            if test:
                # headers = {'ver': '3.0', 'poolserial': '4', 'len': '79', 'server': 'supervisor', 'eventname': 'PROCESS_STATE_EXITED', 'serial': '4', 'pool': 'crashmail'}
                self.stderr.write(str(headers) + '\n')

                # payload = 'processname:00 groupname:showImageWater from_state:RUNNING expected:1 pid:19499'
                self.stderr.write(payload + '\n')
                self.stderr.flush()

            if not headers['eventname'] == 'PROCESS_STATE_EXITED':
                # 如果不是 PROCESS_STATE_EXITED 类型的 event, 不处理, 直接向 stdout 写入"RESULT\nOK"
                childutils.listener.ok(self.stdout)
                continue

            # 解析 payload, 这里我们只用这个 pheaders.
            # pdata 在 PROCESS_LOG_STDERR 和 PROCESS_COMMUNICATION_STDOUT 等类型的 event 中才有
            # pheaders = {'from_state': 'RUNNING', 'processname': '00', 'pid': '19494', 'expected': '0', 'groupname': 'EvalueShow'}

            pheaders, pdata = childutils.eventdata(payload + '\n')

            # 过滤掉 expected 的 event, 仅处理 unexpected 的
            # 当 program 的退出码为对应配置中的 exitcodes 值时, expected=1; 否则为0
            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['groupname'], pheaders['pid'], pheaders['from_state'])

            self.stderr.write('unexpected exit, mailing\n')
            self.stderr.flush()

            send_mail(msg)

            # 向 stdout 写入"RESULT\nOK",并进入下一次循环
            childutils.listener.ok(self.stdout)

def main():

    # listener 必须交由 supervisor 管理, 自己运行是不行的
    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()
    prog.runforever(test=True)

if __name__ == '__main__':
    main()

参考资料:
http://supervisord.org/events.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章