[toc]html
Event 是在 Supervisor 3.0 引入的一個高級特性,若是隻簡單使用 Supervisor 管理進程,則不須要了解 Event。python
但若是但願監控 Supervisor 管理的進程的各類狀態(如: 啓動、退出、失敗、退出狀態碼 ...)並支持告警,才須要學習 Event。bash
Supervisor 提供了一種基於訂閱消息的通知機制,稱爲 event listener(事件監聽者)。 用於訂閱並處理事件通知消息。python2.7
Supervisor 在工做時,其下管理的進程發生任何的狀態變化都會產生事件通知消息,這些消息被分爲成了各類類型,在沒有配置 event listener 時,這些消息將不會獲得處理。當配置了event listener 能夠在配置中指定訂閱某一類型的事件通知消息,那麼當指定類型的消息產生時 event listener 就會收到,以便進行下一步處理。socket
事件通知協議基於子進程的 stdin 和 stdout,Supervisor 會發送指定格式的消息數據給 event listener 進程的 stdin ,並指望從 event listener 的 stdout 返回一個指定格式的輸出。 event listener 程序須要本身寫代碼實現,能夠使用任何語言,不過在 Python 中有提供一個庫 supervisor.childutils 專門用於快速開發 event listener ,所以用 python 開發是最簡便的。ide
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 |
一個 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
一個 Event Listener 有如下三種狀態:翻譯
Name | Description |
---|---|
ACKNOWLEDGED | 註冊,表示 Listener 被確認,即將能夠開始工做 |
READY | 就緒,表示隨時能夠接收事件通知 |
BUSY | 繁忙,表示此時沒法接收事件通知 |
一、Listener 處於 READY 時,當 Supervisor 產生了在 Listener 配置的 event 時,Supervisor 就會把該 event 發送給該 Listener ,並將狀態設置爲 BUSY
二、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
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
Name | Description |
---|---|
processname | 進程名 |
groupname | 進程組名 [program:bar] |
from_state | 進程在退出前是什麼狀態 |
expected | 默認狀況下 exitcodes=0,2,當退出碼爲 0 或 2 時,是 expected 的,此時該值爲 1;其它的退出碼,也就是 unexpected 了,該值爲 0 |
pid | 退出的進程的 pid |
三、處理完 payload 後,須要向本身的 stdout 寫一條消息以告訴 Supervisor 處理結果,例如 RESULT 2\nOK 或 RESULT 4\nFAIL。
四、Supervisor 收到的返回結果若是是 OK 表示 event 處理成功,若是是 FAIL 表示 event 處理失敗。
五、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 = "python@163.com" mail_pass = "123456" from_addr = 'python@163.com' to_addr = ['123@qq.com'] 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()