进程监控神器Supervisor:让你的Python应用稳如老狗!
作者:Ais137
来源:https://juejin.cn/post/7354406980784373798
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 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.loglogfile_maxbytes=50MB ; max main logfile bytes b4 rotation; default 50MBlogfile_backups=10 ; # of main logfile backups; 0 means none, default 10loglevel=info ; log level; default info; others: debug,warn,tracepidfile=/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] 参数。该参数用于将指定文件包含到配置中,通过这种方式来 "扩展" 服务配置文件。
mkdir /etc/supervisord.d
[include]files = /etc/supervisord.d/*.ini
4.3. 自定义应用配置文件
supervisor-{porject_name}.ini
; /root/test/supervisor-test.ini[program:test]command=python -u ./test.py ; 运行命令directory=/root/test/ ; 运行目录redirect_stderr=true ; 将 stderr 重定向到 stdoutstdout_logfile=/root/test/test.log ; 日志文件输出路径
ln ./supervisor-test.ini /etc/supervisord.d/supervisor-test.ini
需要注意的是,对于 supervisor 来说,上述 服务配置文件 和 应用配置文件 并没有直接区别。之所以将其划分成两类配置文件的目的在于当添加新项目时,不需要手动修改配置文件。
4.4. 启动 supervisord 服务进程
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)
supervisord -c conf_file_path
4.5. 启动 supervisorctl 客户端进程
supervisorctl
test RUNNING pid 2612, uptime 0:17:06
1712051907.88209181712051908.88227991712051909.88241651712051910.8826928...
PS : 使用 help 命令可以查看支持的所有操作。
4.6. 验证 supervisor 的监控重启特性
文章开头描述了引入 supervisor 的主要目的,即通过监控目标进程的运行状态,并在其异常中断后自动重启来提高运行的稳定性,接下来就验证一下是否满足这个需求。
(base) root:~/test# ps -ef | grep testroot 3359 2394 0 10:15 ? 00:00:00 python -u ./test.py(base) root:~/test# kill -9 3359(base) root:~/test# ps -ef | grep testroot 3472 2394 1 10:16 ? 00:00:00 python -u ./test.py
通过上述测试可以看到,当手动 kill 掉目标进程后,supervisor 又自动重启了目标进程 (pid 发生了变化)。
supervisorctl stop test
5. 高级特性
5.1. 进程组管理
对于大多数项目,通常会包含多个进程,supervisor 支持将多个进程组成一个 进程组 来进行统一管理。
[group:test]programs=test-task_service, test-collector[program:test-task_service]command=python -u ./task_service.pydirectory=/root/test/[program:test-collector]command=python -u ./collector.pydirectory=/root/test/
(base) root:~# supervisorctltest:test-collector RUNNING pid 1133, uptime 0:02:40test:test-task_service RUNNING pid 1359, uptime 0:00:01
supervisor> stop test:test:test-task_service: stoppedtest: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.pydirectory=/root/test
numprocs : 用于指定运行时的进程实例数量,需要与 process_name 参数配合使用。
[program:test]command=python -u /root/test/test.pyprocess_name=%(program_name)s_%(process_num)snumprocs=3
supervisor> statustest:test_0 RUNNING pid 2463, uptime 0:00:02test:test_1 RUNNING pid 2464, uptime 0:00:02test: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.pyautostart=false
supervisor> reloadReally restart the remote supervisord process y/N? yRestarted supervisordsupervisor> statustest1 RUNNING pid 3253, uptime 0:00:02test2 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.pystdout_logfile=/root/test/test.logstdout_logfile_maxbytes=1KBstdout_logfile_backups=5redirect_stderr=true
test.logtest.log.1test.log.2test.log.3test.log.4test.log.5
5.3. supervisorctl 命令详解
supervisor> helpdefault commands (type help <topic>):=====================================add exit open reload restart start tailavail fg pid remove shutdown status updateclear maintail quit reread signal stop version
supervisor> help restartrestart <name> Restart a processrestart <gname>:* Restart all processes in a grouprestart <name> <name> Restart multiple processes or groupsrestart all Restart all processesNote: 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 timeimport 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 RUNRUN = 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 信号上注册了一个处理函数,用来在退出之前处理相关逻辑。
supervisor> statustest RUNNING pid 2855, uptime 0:00:06supervisor> signal 15 testtest: signalledsupervisor> statustest EXITED Apr 03 03:51 AM
2024-04-03 03:51:342024-04-03 03:51:352024-04-03 03:51:362024-04-03 03:51:372024-04-03 03:51:38processing signal(SIGTERM)update task statusclear cache dataexited
日志的输出结果与代码的预期一致。
PS : stop test 与 signal 15 test 有相同的效果。
5.5. 可视化操作模式
[inet_http_server]port=0.0.0.0:9001username=userpassword=123
重启后访问 http://127.0.0.1:9001/ 输入认证密码后,可以看到以下页面:
PS : 根据配置文档中的警告,以这种模式启动时,应考虑安全问题,不应该把服务接口暴露到公网上。
6. 自动重启机制的简单分析
在上一节的 "[program:x] 配置参数详解" 部分,有几个与自动重启机制相关的关键配置参数没有描述,在此通过具体的代码实验来看看这些参数对自动重启机制的影响。
autorestart=unexpected
autorestart 参数用来确定 supervisord 服务进程是否会自动重启目标进程,其有三个可选值,根据这三个值的不同有对应的处理逻辑。
autorestart=unexpected : 这是默认选项,当目标进程 “异常” 退出时,服务进程将自动重启目标进程,这里的 “异常” 指的是目标进程的 exitcode 与 exitcodes 配置的参数不一致时。_exitcodes_ 配置用于指定程序 “预期” 的退出代码列表,默认为 exitcodes=0。
import timedef worker(end_count=5, exit_mode=1):count = 0while 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:breakelif exit_mode == 2:raise Exception("test")elif exit_mode == 3:exit(1)else:passcount += 1worker(exit_mode=1)# worker(exit_mode=2)# worker(exit_mode=3)# worker(exit_mode=4)
分别对以上 4 种退出模式进行测试,观察服务进程是否会自动重启目标进程。
exit_mode == 1 : 通过 break 跳出循环正常结束
supervisor> statustest RUNNING pid 5965, uptime 0:00:05supervisor> statustest EXITED Apr 03 08:59 AM
可以看到目标进程在正常结束后,服务进程不会对其自动重启。
exit_mode == 2 : 通过 Exception 抛出异常,模拟内部异常导致的退出。
supervisor> statustest RUNNING pid 6056, uptime 0:00:05supervisor> statustest STARTINGsupervisor> statustest RUNNING pid 6103, uptime 0:00:02
可以看到以这种方式退出后,服务进程会自动重启目标进程。
exit_mode == 3 : 通过 exit(1) 方法返回与 exitcodes=0 不一致的退出代码来测试。
supervisor> statustest RUNNING pid 6209, uptime 0:00:05supervisor> statustest STARTINGsupervisor> statustest RUNNING pid 6240, uptime 0:00:01
与 exit_mode == 2 的测试结果一致。
exit_mode == 4 : 通过手动 kill 目标进程来测试,发现与上述结果一致。
通过配置 exitcodes 参数,可以根据具体的场景来自定义自动重启的行为,比如为每一个关键异常赋予一个退出代码,当进程出现内部异常时,可以根据这些退出代码来控制自动重启行为。
例如目标进程依赖于一个数据库,如果数据库连接失败,那么后续逻辑将无法执行,在这种情况下不需要再自动重启,因此可以在捕获该异常时产生一个对应的退出代码,比如 exit(100),然后将其配置到 exitcodes=0,100 中。这样当这个特定异常触发时,产生特殊的退出代码,从而不再重启进程。
autorestart=true : 当使用这种模式时,就算程序正常退出也会自动重启。
autorestart=false : 当使用这种模式时,将停用自动重启机制。
# supervisor.process@functools.total_orderingclass Subprocess(object):...def transition(self):now = time.time()state = self.stateself._check_and_adjust_for_system_clock_rollback(now)logger = self.config.options.loggerif self.config.options.mood > SupervisorStates.RESTARTING:# dont start any processes if supervisor is shutting downif state == ProcessStates.EXITED:if self.config.autorestart:if self.config.autorestart is RestartUnconditionally:# EXITED -> STARTINGself.spawn()else: # autorestart is RestartWhenExitUnexpectedif self.exitstatus not in self.config.exitcodes:# EXITED -> STARTINGself.spawn()elif state == ProcessStates.STOPPED and not self.laststart:if self.config.autostart:# STOPPED -> STARTINGself.spawn()elif state == ProcessStates.BACKOFF:if self.backoff <= self.config.startretries:if now > self.delay:# BACKOFF -> STARTINGself.spawn()...
startsecs 是与自动重启相关的另一个配置参数。其作用是用于判断进程是否启动成功,只有当目标进程运行时间大于该配置时,才会判断成成功。
import timeprint(time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(int(time.time()))))
supervisor> statustest BACKOFF Exited too quickly (process log may have details)supervisor> statustest STARTINGsupervisor> statustest FATAL Exited too quickly (process log may have details)
import timeprint(time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(int(time.time()))))time.sleep(3)
supervisor> statustest RUNNING pid 7049, uptime 0:00:02supervisor> statustest EXITED Apr 03 09:41 AM
startsecs=1startretries=5
import timeprint(time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(int(time.time()))))
2024-04-03 09:59:202024-04-03 09:59:212024-04-03 09:59:232024-04-03 09:59:262024-04-03 09:59:302024-04-03 09:59:35
7. 总结
以上就是对 Supervisor 的简单介绍与应用,除了上述介绍的基本用法和高级特性外,还支持以 RPC 的方式进行调用,但由于现阶段还未遇到相关的应用场景,因此考虑后续深度使用后再研究相关代码。
热门推荐