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

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