當前位置: 妍妍網 > 碼農

程式碼背景執行,不止nohup,還有Python Supervisor!

2024-05-12碼農

作者丨Ais137

https://juejin.cn/post/73544069807843737 98

1. 概述

Supervisor 是一個 C/S 架構的行程監控與管理工具,本文主要介紹其基本用法和部份高級特性,用於解決部署持久化行程的穩定性問題。

2. 問題場景

在實際的工作中,往往會有部署持久化行程的需求,比如介面服務行程,又或者是消費者行程等。這類行程通常是作為後台行程持久化執行的。

一般的部署方法是透過 nohup cmd & 命令來部署。但是這種方式有個弊端是在某些情況下無法保證目標行程的穩定性執行,有的時候 nohup 執行的後台任務會因為未知原因中斷,從而導致服務或者消費中斷,進而影響計畫的正常執行。

為了解決上述問題,透過引入 Supervisor 來部署持久化行程,提高系統執行的穩定性。

3. Supervisor 簡介

Supervisor is a client/server system that allows its users to control a number of processes on UNIX-like operating systems.

Supervisor 是一個 C/S 架構的行程監控與管理工具,其最主要的特性是可以監控目標行程的執行狀態,並在其異常中斷時自動重新開機。同時支持對多個行程進行分組管理。

完整特性詳見官方文件 github 與 document。

4.部署流程

4.1. 安裝 Supervisor

透過 pip 命令安裝 Supervisor 工具:

pip install supervisor

PS : 根據官方文件的說明 Supervisor 不支持在 windows 環境下執行

4.2. 自訂服務配置檔

在安裝完成後,透過以下命令生成配置檔到指定路徑:

echo_supervisord_conf > /etc/supervisord.conf

配置檔的一些主要配置參數如下

[unix_http_server]
file=/tmp/supervisor.sock ; the path to the socket file
;chmod=0700 ; socket file mode (default 0700)
;chown=nobody:nogroup ; socket file uid:gid owner
;username=user ; default is no username (open server)
;password=123 ; default is no password (open server)
[supervisord]
logfile=/tmp/supervisord.log ; main log file; default $CWD/supervisord.log
logfile_maxbytes=50MB ; max main logfile bytes b4 rotation; default 50MB
logfile_backups=10 ; # of main logfile backups; 0 means none, default 10
loglevel=info ; log level; default info; others: debug,warn,trace
pidfile=/tmp/supervisord.pid ; supervisord pidfile; default supervisord.pid
[supervisorctl]
serverurl=unix:///tmp/supervisor.sock ; use a unix:// URL for a unix socket
;serverurl=http://127.0.0.1:9001 ; use an http:// url to specify an inet socket
;username=chris ; should be same as in [*_http_server] ifset
;password=123 ; should be same as in [*_http_server] ifset
;prompt=mysupervisor ; cmd line prompt (default "supervisor")
;history_file=~/.sc_history ; use readline historyif available
;[program:theprogramname]
;command=/bin/cat ; the program (relative uses PATH, can take args)
;[group:thegroupname]
;programs=progname1,progname2 ; each refers to 'x'in [program:x] definitions
;priority=999 ; the relative start priority (default 999)
;[include]
;files = relative/directory/*.ini



對於上述配置參數,可以按照具體的需求進行自訂,大多數參數可以保持預設設定。但是為了方便多個計畫的統一管理,需要啟用 [include] 參數。該參數用於將指定檔包含到配置中,透過這種方式來 "擴充套件" 服務配置檔。

建立配置目錄,並修改 files 參數 :

mkdir /etc/supervisord.d

[include]
files = /etc/supervisord.d/*.ini

4.3. 自訂套用配置檔

假設現在有一個測試計畫 (test),裏面有個 test.py 指令碼需要持久化執行。現在切換到計畫目錄 (/root/test),並按照以下格式建立套用配置檔。

supervisor-{porject_name}.ini

配置計畫的行程啟動參數 :

; /root/test/supervisor-test.ini
[program:test]
command=python -u ./test.py ; 執行命令
directory=/root/test/ ; 執行目錄
redirect_stderr=true ; 將 stderr 重新導向到 stdout
stdout_logfile=/root/test/test.log ; 日誌檔輸出路徑

將上述配置檔連結到服務配置檔中 [include] 參數設定的目錄下 (或者復制):

ln ./supervisor-test.ini /etc/supervisord.d/supervisor-test.ini

需要註意的是,對於 supervisor 來說,上述 服務配置檔 套用配置檔 並沒有直接區別。之所以將其劃分成兩類配置檔的目的在於當添加新計畫時,不需要手動修改配置檔。

4.4. 啟動 supervisord 服務行程

supervisord supervisor 的核心服務行程,透過配置檔中的參數來建立具體的子行程,並對其進行監控與管理。透過以下命令來啟動:

supervisord

預設情況下,按照以下路徑順序尋找並載入配置檔

  1. ../etc/supervisord.conf (Relative to the executable)

  2. ../supervisord.conf (Relative to the executable)

  3. $CWD/supervisord.conf

  4. $CWD/etc/supervisord.conf

  5. /etc/supervisord.conf

  6. /etc/supervisor/supervisord.conf (since Supervisor 3.3.0)

也可以透過 -c 參數來指定配置檔路徑。

supervisord -c conf_file_path

4.5. 啟動 supervisorctl 客戶端行程

supervisorctl supervisor 的客戶端行程,透過與 supervisord 服務行程建立 socket 連線來進行互動。使用以下命令進行互動式連線:

supervisorctl

成功連線後會顯示當前執行的任務狀態,或者使用 status 命令檢視:

test RUNNING pid 2612, uptime 0:17:06

使用 tail -f test 來檢視指定套用的日誌輸出:

1712051907.8820918
1712051908.8822799
1712051909.8824165
1712051910.8826928
...

PS : 使用 help 命令可以檢視支持的所有操作。

4.6. 驗證 supervisor 的監控重新開機特性

文章開頭描述了引入 supervisor 的主要目的,即透過監控目標行程的執行狀態,並在其異常中斷後自動重新開機來提高執行的穩定性,接下來就驗證一下是否滿足這個需求。

在此透過手動 kill 目標行程的方式來模擬異常中斷。

(base) root:~/test# ps -ef | grep test
root 3359 2394 0 10:15 ? 00:00:00 python -u ./test.py
(base) root:~/test# kill -9 3359
(base) root:~/test# ps -ef | grep test
root 3472 2394 1 10:16 ? 00:00:00 python -u ./test.py

透過上述測試可以看到,當手動 kill 掉目標行程後, supervisor 又自動重新開機了目標行程 (pid 發生了變化)。

要主動結束目標行程,可以透過以下命令實作:

supervisorctl stop test

5. 高級特性

5.1. 行程組管理

對於大多數計畫,通常會包含多個行程, supervisor 支持將多個行程組成一個 行程組 來進行統一管理。

透過添加 [group:thegroupname] 參數並設定 programs 欄位來設定行程組。

[group:test]
programs=test-task_service, test-collector
[program:test-task_service]
command=python -u ./task_service.py
directory=/root/test/
[program:test-collector]
command=python -u ./collector.py
directory=/root/test/

進入 supervisor 並使用 update 命令後檢視執行狀態:

(base) root:~# supervisorctl 
test:test-collector RUNNING pid 1133, uptime 0:02:40
test:test-task_service RUNNING pid 1359, uptime 0:00:01

在使用 restart , start , stop 等命令時,可以透過指定行程組名稱來進行批次操作。

supervisor> stop test:
test:test-task_service: stopped
test:test-collector: stopped

PS: 進行行程組操作時需要加上 : 號,即 cmd groupname:

5.2. [program:x] 配置參數詳解

  • command : 用於指定待執行的命令。

  • [program:test]
    command=python -u /root/test/test.py

  • directory : 指定在執行 command 命令前切換的目錄,當 command 使用相對路徑時,可以與該參數配合使用。

  • [program:test]
    command=python -u ./test.py
    directory=/root/test

  • numprocs : 用於指定執行時的行程例項數量,需要與 process_name 參數配合使用。

  • [program:test]
    command=python -u /root/test/test.py
    process_name=%(program_name)s_%(process_num)s
    numprocs=3

    supervisor> status
    test:test_0 RUNNING pid 2463, uptime 0:00:02
    test:test_1 RUNNING pid 2464, uptime 0:00:02
    test:test_2 RUNNING pid 2465, uptime 0:00:02

  • autostart : 用於控制是否在 supervisord 行程啟動時同時啟動 (預設為 true)

  • [program:test1]
    command=python -u /root/test/test.py
    [program:test2]
    command=python -u /root/test/test.py
    autostart=false

    supervisor> reload
    Really restart the remote supervisord process y/N? y
    Restarted supervisord
    supervisor> status
    test1 RUNNING pid 3253, uptime 0:00:02
    test2 STOPPED Not started

  • stdout_logfile : 指定標準輸出流的日誌檔路徑。

  • stdout_logfile_maxbytes : 單個日誌檔的最大字節數,當超過該值時將對日誌進行切分。

  • stdout_logfile_backups : 切分日誌後保留的副本數,與 stdout_logfile_maxbytes 配合使用實作捲動日誌效果。

  • redirect_stderr : 將 stderr 重新導向到 stdout。

  • [program:test]
    command=python -u /root/test/test.py
    stdout_logfile=/root/test/test.log
    stdout_logfile_maxbytes=1KB
    stdout_logfile_backups=5
    redirect_stderr=true

    test.log
    test.log.1
    test.log.2
    test.log.3
    test.log.4
    test.log.5

    5.3. supervisorctl 命令詳解

    supervisorctl 支持的所有操作可以透過 help 命令來檢視:

    supervisor> help
    default commands (typehelp <topic>):
    =====================================
    add exit open reload restart start tail
    avail fg pid remove shutdown status update 
    clear maintail quit reread signal stop version

    透過 help cmd 可以檢視每個命令的意義和用法:

    supervisor> help restart
    restart <name> Restart a process
    restart <gname>:* Restart all processes in a group
    restart <name> <name> Restart multiple processes or groups
    restart all Restart all processes
    Note: restart does not reread config files. For that, see reread and update.

    其中與 supervisord 服務行程相關的命令有:

  • open : 連線到遠端 supervisord 行程。

  • reload : 重新開機 supervisord 行程。

  • shutdown : 關閉 supervisord 行程。

  • 而以下命令則用於進行具體的套用行程管理:

  • status : 檢視套用行程的執行狀態。

  • start : 啟動指定的套用行程。

  • restart : 重新開機指定的套用行程。

  • stop : 停止指定的套用行程。

  • signal : 向指定套用行程發送訊號。

  • update : 重新載入配置參數,並根據需要重新開機套用行程。

  • 5.4. 套用行程的訊號處理

    在某些套用場景,需要在行程結束前進行一些處理操作,比如清理緩存,上傳執行狀態等。對於這種需求可以透過引入 signal 模組並註冊相關處理邏輯,同時結合 supervisorctl signal 命令來實作。

    測試程式碼如下:

    import time
    import signal
    # 執行標誌
    RUN = True
    # 訊號處理邏輯
    def exit_handler(signum, frame):
    print(f'processing signal({signal.Signals(signum).name})')
    print("update task status")
    print("clear cache data")
    global RUN
    RUN = False
    # 註冊訊號
    signal.signal(signal.SIGTERM, exit_handler)
    # 模擬持久化行為
    while RUN:
    print(time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(int(time.time()))))
    time.sleep(1)
    print("exited")



    上述程式碼在 signal.SIGTERM 訊號上註冊了一個處理常式,用來在結束之前處理相關邏輯。

    透過 supervisorctl signal 向目標行程發送 signal.SIGTERM(15) 訊號。

    supervisor> status
    test RUNNING pid 2855, uptime 0:00:06
    supervisor> signal 15 test
    test: signalled
    supervisor> status
    test EXITED Apr 03 03:51 AM

    可以看到目標行程正常結束了,再檢視日誌驗證是否執行了 exit 函式的邏輯:

    2024-04-03 03:51:34
    2024-04-03 03:51:35
    2024-04-03 03:51:36
    2024-04-03 03:51:37
    2024-04-03 03:51:38
    processing signal(SIGTERM)
    update task status
    clear cache data
    exited

    日誌的輸出結果與程式碼的預期一致。

    PS : stop test signal 15 test 有相同的效果。

    5.5. 視覺化操作模式

    除了使用 supervisorctl 以互動式命令列終端的形式連線 supervisord 外,還支持以視覺化 web 頁面的方式來操作。修改 服務配置檔 (/etc/supervisord.conf) 並啟用以下配置:

    [inet_http_server]
    port=0.0.0.0:9001
    username=user
    password=123

    重新開機後存取 http://127.0.0.1:9001/ 輸入認證密碼後,可以看到以下頁面:

    PS : 根據配置文件中的警告,以這種模式啟動時,應考慮安全問題,不應該把服務介面暴露到公網上。

    6. 自動重新開機機制的簡單分析

    在上一節的 "[program:x] 配置參數詳解" 部份,有幾個與自動重新開機機制相關的關鍵配置參數沒有描述,在此透過具體的程式碼實驗來看看這些參數對自動重新開機機制的影響。

    控制自動重新開機機制的關鍵參數是:

    autorestart=unexpected

    autorestart 參數用來確定 supervisord 服務行程是否會自動重新開機目標行程,其有三個可選值,根據這三個值的不同有對應的處理邏輯。

  • autorestart=unexpected : 這是預設選項,當目標行程 「異常」 結束時,服務行程將自動重新開機目標行程,這裏的 「異常」 指的是目標行程的 exitcode exitcodes 配置的參數不一致時。_exitcodes_ 配置用於指定程式 「預期」 的結束程式碼列表,預設為 exitcodes=0

  • 用以下程式碼來進行測試行為:

    import time
    def worker(end_count=5, exit_mode=1):
    count = 0
    while True:
    print(time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(int(time.time()))))
    time.sleep(1)
    if count >= end_count:
    if exit_mode == 1:
    break
    elif exit_mode == 2:
    raise Exception("test")
    elif exit_mode == 3:
    exit(1)
    else:
    pass
    count += 1
    worker(exit_mode=1)
    # worker(exit_mode=2)
    # worker(exit_mode=3)
    # worker(exit_mode=4)

    分別對以上 4 種結束模式進行測試,觀察服務行程是否會自動重新開機目標行程。

    1. exit_mode == 1 : 透過 break 跳出迴圈正常結束

    supervisor> status
    test RUNNING pid 5965, uptime 0:00:05
    supervisor> status
    test EXITED Apr 03 08:59 AM

    可以看到目標行程在正常結束後,服務行程不會對其自動重新開機。

    1. exit_mode == 2 : 透過 Exception 丟擲異常,模擬內部異常導致的結束。

    supervisor> status
    test RUNNING pid 6056, uptime 0:00:05
    supervisor> status
    test STARTING
    supervisor> status
    test RUNNING pid 6103, uptime 0:00:02

    可以看到以這種方式結束後,服務行程會自動重新開機目標行程。

    1. exit_mode == 3 : 透過 exit(1) 方法返回與 exitcodes=0 不一致的結束程式碼來測試。

    supervisor> status
    test RUNNING pid 6209, uptime 0:00:05
    supervisor> status
    test STARTING
    supervisor> status
    test RUNNING pid 6240, uptime 0:00:01

    exit_mode == 2 的測試結果一致。

    1. exit_mode == 4 : 透過手動 kill 目標行程來測試,發現與上述結果一致。

    透過配置 exitcodes 參數,可以根據具體的場景來自訂自動重新開機的行為,比如為每一個關鍵異常賦予一個結束程式碼,當行程出現內部異常時,可以根據這些結束程式碼來控制自動重新開機行為。

    例如目標行程依賴於一個資料庫,如果資料庫連線失敗,那麽後續邏輯將無法執行,在這種情況下不需要再自動重新開機,因此可以在捕獲該異常時產生一個對應的結束程式碼,比如 exit(100) ,然後將其配置到 exitcodes=0,100 中。這樣當這個特定異常觸發時,產生特殊的結束程式碼,從而不再重新開機行程。

  • autorestart=true : 當使用這種模式時,就算程式正常結束也會自動重新開機。

  • autorestart=false : 當使用這種模式時,將停用自動重新開機機制。

  • 自動重新開機機制的相關源碼片段如下:

    # supervisor.process
    @functools.total_ordering
    class Subprocess(object):
    ...
    def transition(self):
    now = time.time()
    state = self.state
    self._check_and_adjust_for_system_clock_rollback(now)
    logger = self.config.options.logger
    if self.config.options.mood > SupervisorStates.RESTARTING:
    # dont start any processes if supervisor is shutting down
    if state == ProcessStates.EXITED:
    if self.config.autorestart:
    if self.config.autorestart is RestartUnconditionally:
    # EXITED -> STARTING
    self.spawn()
    else# autorestart is RestartWhenExitUnexpected
    if self.exitstatus not in self.config.exitcodes:
    # EXITED -> STARTING
    self.spawn()
    elif state == ProcessStates.STOPPED and not self.laststart:
    if self.config.autostart:
    # STOPPED -> STARTING
    self.spawn()
    elif state == ProcessStates.BACKOFF:
    if self.backoff <= self.config.startretries:
    if now > self.delay:
    # BACKOFF -> STARTING
    self.spawn()
    ...





    startsecs 是與自動重新開機相關的另一個配置參數。其作用是用於判斷行程是否啟動成功,只有當目標行程執行時間大於該配置時,才會判斷成成功。

    這意味著就算目標行程是正常結束的 ( exitcodes=0 ),如果其執行時間小於設定的參數,也會判斷成失敗。

    import time
    print(time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(int(time.time()))))

    supervisor> status
    test BACKOFF Exited too quickly (process log may have details)
    supervisor> status
    test STARTING
    supervisor> status
    test FATAL Exited too quickly (process log may have details)

    import time
    print(time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(int(time.time()))))
    time.sleep(3)

    supervisor> status
    test RUNNING pid 7049, uptime 0:00:02
    supervisor> status
    test EXITED Apr 03 09:41 AM

    startretries 參數需要與 startsecs 參數配合使用,用於控制目標行程的重新開機嘗試次數,並且每次重試花費的時間間隔越來越長。可以透過以下程式碼測試一下:

    startsecs=1
    startretries=5

    import time
    print(time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(int(time.time()))))

    測試程式碼的輸出結果如下,可以看到每次重試的間隔時間呈 1, 2, 3, ... 增長。

    2024-04-03 09:59:20
    2024-04-03 09:59:21
    2024-04-03 09:59:23
    2024-04-03 09:59:26
    2024-04-03 09:59:30
    2024-04-03 09:59:35

    7. 總結

    以上就是對 Supervisor 的簡單介紹與套用,除了上述介紹的基本用法和高級特性外,還支持以 RPC 的方式進行呼叫,但由於現階段還未遇到相關的套用場景,因此考慮後續深度使用後再研究相關程式碼。