本文目录导读:

- 目录导读
- 为什么需要自动重启进程?
- 核心思路:检测崩溃与拉起新进程
- 方法一:基于
subprocess与while循环的简单守护 - 方法二:利用
os.fork实现双进程互保 - 方法三:使用
supervisor等系统级工具 - 常见陷阱:避免无限重启与日志爆炸
- 问答精选:你可能关心的5个问题
- 选择适合你的重启策略
Python脚本守护指南:如何自动重启异常退出的进程(附完整代码)
目录导读
- 为什么需要自动重启进程?
- 核心思路:检测崩溃与拉起新进程
- 基于
subprocess与while循环的简单守护 - 利用
os.fork实现双进程互保 - 使用
supervisor等系统级工具 - 常见陷阱:避免无限重启与日志爆炸
- 问答精选:你可能关心的5个问题
- 选择适合你的重启策略
为什么需要自动重启进程?
任何长期运行的Python脚本,如Web服务、爬虫、任务队列消费者,都可能因以下原因意外退出:
- 未捕获的异常(如网络超时、内存泄漏)
- 操作系统信号(如OOM Killer)
- 第三方库崩溃
- 系统资源耗尽
手动重启不仅低效,还会造成服务中断。自动重启机制能显著提升服务的可用性,这也是运维自动化(AIOps)的基础能力之一。
核心思路:检测崩溃与拉起新进程
实现自动重启的通用逻辑如下:
while True:
启动目标脚本
等待脚本退出(或检测崩溃信号)
记录退出码
根据退出码判断是否重启(例如非0退出码表示异常)
等待短暂间隔后重新启动
关键点:
- 退出码:正常退出通常返回
0,异常退出返回非0。 - 重启延迟:防止快速崩溃导致CPU空转。
- 重启次数限制:避免陷入死循环。
方法一:基于subprocess与while循环的简单守护
这是最直接、最可控的方式,以下是一个完整的守护脚本示例:
#!/usr/bin/env python3
import subprocess
import time
import sys
import logging
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[logging.FileHandler("guardian.log"), logging.StreamHandler()]
)
logger = logging.getLogger("ProcessGuardian")
# 要守护的目标脚本(可执行文件名或完整路径)
TARGET_SCRIPT = ["python3", "/path/to/your_target_script.py"]
# 重启间隔(秒)
RESTART_DELAY = 2
# 最大重启次数(-1表示无限制)
MAX_RESTARTS = -1
def run_target():
"""启动目标进程并等待其退出"""
logger.info(f"启动目标进程: {' '.join(TARGET_SCRIPT)}")
process = subprocess.Popen(
TARGET_SCRIPT,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
universal_newlines=True
)
return process
def monitor_process(process):
"""实时打印目标进程的输出,并等待其结束"""
try:
for line in process.stdout:
print(line, end="") # 转发到守护进程的标准输出
except KeyboardInterrupt:
logger.warning("收到中断信号,正在关闭目标进程...")
process.terminate()
sys.exit(0)
# 等待进程结束,获取返回码
return_code = process.wait()
return return_code
def main():
restart_count = 0
while True:
process = run_target()
return_code = monitor_process(process)
if return_code == 0:
logger.info("目标进程正常退出,守护程序退出")
break
else:
logger.warning(f"目标进程异常退出,返回码: {return_code}")
# 检查重启次数限制
if MAX_RESTARTS != -1 and restart_count >= MAX_RESTARTS:
logger.critical(f"已达到最大重启次数 {MAX_RESTARTS},守护程序退出")
break
restart_count += 1
logger.info(f"将在 {RESTART_DELAY} 秒后重启 (第{restart_count}次重启)")
time.sleep(RESTART_DELAY)
if __name__ == "__main__":
main()
用法:
- 将
TARGET_SCRIPT的路径改为你的实际脚本。 - 直接运行本脚本即可开始守护。
优点:
- 纯Python实现,无外部依赖。
- 可以实时看到目标进程的输出。
缺点:
- 单点故障:守护进程本身也可能崩溃。
- 不适合高可用场景(守护进程挂了则无人接管)。
方法二:利用os.fork实现双进程互保
为了弥补单点故障,可以用os.fork()创建两个进程,它们互相监视对方的存活状态,以下是一个简化实现:
import os
import time
import signal
import sys
def child_process():
"""子进程执行实际工作"""
# 这里替换为你的业务逻辑
while True:
print("I am the child, pid:", os.getpid())
time.sleep(1)
def watch_parent(parent_pid):
"""子进程监视父进程,如果父进程死亡则自己退出"""
while True:
# 尝试发送空信号检查父进程是否存活
try:
os.kill(parent_pid, 0)
except OSError:
print("Parent died, I will exit.")
sys.exit(1)
time.sleep(5)
def main():
parent_pid = os.getpid()
pid = os.fork()
if pid == 0:
# 子进程:启动工作逻辑,同时监视父进程
# 启动一个线程或另一个子进程来监视父进程?更简单的做法是在子进程内定期检查
# 这里我们使用信号处理简化:子进程不断工作,父进程死亡时子进程会被init收养,
# 但更好的做法是子进程定期检查父进程是否还在
# 由于fork后子进程单独运行,我们直接在此启动watch函数
import threading
t = threading.Thread(target=watch_parent, args=(parent_pyid,), daemon=True)
t.start()
child_process()
else:
# 父进程:监视子进程,如果子进程死亡则重启
try:
while True:
# 等待子进程结束
wpid, status = os.waitpid(pid, 0)
if os.WIFEXITED(status) and os.WEXITSTATUS(status) == 0:
print("Child exited normally")
break
else:
print("Child crashed, restarting...")
pid = os.fork()
if pid == 0:
child_process()
else:
continue
except KeyboardInterrupt:
os.kill(pid, signal.SIGTERM)
sys.exit(0)
if __name__ == "__main__":
main()
注意:该方案较复杂,容易引入僵尸进程或竞态条件,生产环境建议优先使用成熟工具。
方法三:使用supervisor等系统级工具
对于生产环境,推荐使用专业进程管理工具:
- Supervisor:最流行的Python进程管理器,支持自动重启、日志管理、Web界面。
- Systemd:Linux原生服务管理器,通过服务单元文件实现。
- Docker + restart policy:容器化后使用
--restart=always。
Supervisor配置示例(/etc/supervisor/conf.d/myapp.conf):
[program:myapp] command=python3 /path/to/your_script.py directory=/path/to/working_dir autostart=true autorestart=true ; 关键:崩溃后自动重启 startretries=3 ; 启动重试次数 stderr_logfile=/var/log/myapp.err.log stdout_logfile=/var/log/myapp.out.log
启用后:
supervisorctl reread supervisorctl update supervisorctl start myapp
优点:
- 成熟、稳定、受企业验证。
- 支持进程组、事件监听、优先级别。
常见陷阱:避免无限重启与日志爆炸
陷阱1:快速崩溃导致CPU 100%
如果脚本启动后立即崩溃(如导入模块错误),守护进程会不断尝试重启。
解决方案:
- 设置重启间隔(最少1秒)。
- 记录连续崩溃次数,若超过阈值(如10次/分钟)则停止重启并报警。
陷阱2:日志无限增长
目标进程的输出如果被守护进程全部收集并打印,可能会填满磁盘。
解决方案:
- 使用Python的
logging.RotatingFileHandler滚动日志。 - 或者将输出重定向到文件,仅记录错误级别日志。
陷阱3:守护进程本身被杀死
如果使用subprocess方案,守护进程本身没有任何保护。
解决方案:
- 使用
supervisor同时守护守护进程。 - 或编写系统服务文件让
systemd托管。
问答精选:你可能关心的5个问题
Q1:如何区分进程是正常退出还是崩溃?
A:通过退出码,常规做法是:正常完成sys.exit(0),异常退出sys.exit(1)或更高数值,守护脚本根据非0退出码触发重启。
Q2:守护进程能否同时启动多个副本?
A:可以,使用os.fork()或multiprocessing创建多进程,但要注意端口冲突,推荐使用supervisor的numprocs指令控制副本数。
Q3:如何让重启时保留之前的状态?
A:在目标脚本中定期将状态持久化到文件或数据库(如pickle、sqlite),启动时检查是否存在状态文件并加载,对于爬虫类任务,可保存当前抓取位置的偏移量。
Q4:进程被kill -9强行杀死后还能被检测到吗?
A:可以,守护进程的process.wait()会返回-9,表示被信号终止,你可以据此判断是否为强制杀死,并决定重启策略。
Q5:是否可以在线更新目标脚本而不中断服务?
A:可以,使用蓝绿部署或热加载:守护进程先启动新版本脚本,新进程成功接收请求后再关闭旧进程。supervisor不支持热更新,但可以配合信号实现优雅重启。
选择适合你的重启策略
| 方案 | 适用场景 | 推荐度 |
|---|---|---|
subprocess + while循环 |
本地开发、简单任务 | |
os.fork双进程互保 |
学习验证、无第三方依赖 | |
| Supervisor | 生产环境、企业级服务 | |
| Systemd | Linux用户、需要与系统集成 |
行动建议:
- 如果你只是临时守护一个脚本,用方法一就够了。
- 如果你部署到生产服务器,直接使用Supervisor。
- 如果你的环境不允许安装额外工具,可以用Systemd编写一个服务文件。
无论选择哪种方式,核心原则不变:检测退出、分析原因、控制重启、记录日志,这样你就能从繁琐的手动重启中解放出来,专注于业务逻辑本身。