切換語言為:簡體
程式碼後臺執行,不止nohup,還有Python Supervisor!

程式碼後臺執行,不止nohup,還有Python Supervisor!

  • 爱糖宝
  • 2024-05-25
  • 2113
  • 0
  • 0

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] if set
;password=123                ; should be same as in [*_http_server] if set
;prompt=mysupervisor         ; cmd line prompt (default "supervisor")
;history_file=~/.sc_history  ; use readline history if 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

預設情況下,按照以下路徑順序查詢並載入配置檔案

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

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

  • $CWD/supervisord.conf

  • $CWD/etc/supervisord.conf

  • /etc/supervisord.conf

  • /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 (type help <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/ 輸入認證密碼後,可以看到以下頁面:

程式碼後臺執行,不止nohup,還有Python Supervisor!

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


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

2、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

可以看到以這種方式退出後,服務程序會自動重啟目標程序。

3、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 的測試結果一致。

4、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 的方式進行呼叫,但由於現階段還未遇到相關的應用場景,因此考慮後續深度使用後再研究相關程式碼。

0則評論

您的電子郵件等資訊不會被公開,以下所有項目均必填

OK! You can skip this field.