Python脚本如何重启异常退出进程

wen python案例 34

本文目录导读:

Python脚本如何重启异常退出进程

  1. 目录导读
  2. 为什么需要自动重启进程?
  3. 核心思路:检测崩溃与拉起新进程
  4. 方法一:基于subprocesswhile循环的简单守护
  5. 方法二:利用os.fork实现双进程互保
  6. 方法三:使用supervisor等系统级工具
  7. 常见陷阱:避免无限重启与日志爆炸
  8. 问答精选:你可能关心的5个问题
  9. 选择适合你的重启策略

Python脚本守护指南:如何自动重启异常退出的进程(附完整代码)


目录导读

  1. 为什么需要自动重启进程?
  2. 核心思路:检测崩溃与拉起新进程
  3. 基于subprocesswhile循环的简单守护
  4. 利用os.fork实现双进程互保
  5. 使用supervisor等系统级工具
  6. 常见陷阱:避免无限重启与日志爆炸
  7. 问答精选:你可能关心的5个问题
  8. 选择适合你的重启策略

为什么需要自动重启进程?

任何长期运行的Python脚本,如Web服务、爬虫、任务队列消费者,都可能因以下原因意外退出:

  • 未捕获的异常(如网络超时、内存泄漏)
  • 操作系统信号(如OOM Killer)
  • 第三方库崩溃
  • 系统资源耗尽

手动重启不仅低效,还会造成服务中断。自动重启机制能显著提升服务的可用性,这也是运维自动化(AIOps)的基础能力之一。


核心思路:检测崩溃与拉起新进程

实现自动重启的通用逻辑如下:

while True:
    启动目标脚本
    等待脚本退出(或检测崩溃信号)
    记录退出码
    根据退出码判断是否重启(例如非0退出码表示异常)
    等待短暂间隔后重新启动

关键点

  • 退出码:正常退出通常返回0,异常退出返回非0。
  • 重启延迟:防止快速崩溃导致CPU空转。
  • 重启次数限制:避免陷入死循环。

方法一:基于subprocesswhile循环的简单守护

这是最直接、最可控的方式,以下是一个完整的守护脚本示例:

#!/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()

用法

  1. TARGET_SCRIPT的路径改为你的实际脚本。
  2. 直接运行本脚本即可开始守护。

优点

  • 纯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创建多进程,但要注意端口冲突,推荐使用supervisornumprocs指令控制副本数。

Q3:如何让重启时保留之前的状态?
A:在目标脚本中定期将状态持久化到文件或数据库(如picklesqlite),启动时检查是否存在状态文件并加载,对于爬虫类任务,可保存当前抓取位置的偏移量。

Q4:进程被kill -9强行杀死后还能被检测到吗?
A:可以,守护进程的process.wait()会返回-9,表示被信号终止,你可以据此判断是否为强制杀死,并决定重启策略。

Q5:是否可以在线更新目标脚本而不中断服务?
A:可以,使用蓝绿部署或热加载:守护进程先启动新版本脚本,新进程成功接收请求后再关闭旧进程。supervisor不支持热更新,但可以配合信号实现优雅重启。


选择适合你的重启策略

方案 适用场景 推荐度
subprocess + while循环 本地开发、简单任务
os.fork双进程互保 学习验证、无第三方依赖
Supervisor 生产环境、企业级服务
Systemd Linux用户、需要与系统集成

行动建议

  • 如果你只是临时守护一个脚本,用方法一就够了。
  • 如果你部署到生产服务器,直接使用Supervisor
  • 如果你的环境不允许安装额外工具,可以用Systemd编写一个服务文件。

无论选择哪种方式,核心原则不变:检测退出、分析原因、控制重启、记录日志,这样你就能从繁琐的手动重启中解放出来,专注于业务逻辑本身。

抱歉,评论功能暂时关闭!