如何编写进程异常重启脚本

wen 实用脚本 29

从零搭建高可用守护进程

目录导读

  1. 为什么需要进程异常重启脚本?
  2. 脚本编写的核心原则与常见陷阱
  3. 实战:四种主流重启脚本写法详解
  4. 进阶技巧:健康检查与日志规范
  5. 常见问题问答(QA)

为什么需要进程异常重启脚本?

在服务器运维中,进程因内存泄漏、连接超时、资源耗尽等原因意外退出,是最常见的故障之一,根据某IT运维平台的统计,超过60%的应用层故障表现为进程非正常终止,如果依赖人工盯防,不仅效率低下,而且无法在凌晨等非工作时间快速响应。

如何编写进程异常重启脚本

进程异常重启脚本的核心价值在于:

  • 自动化:当进程退出时,自动恢复服务
  • 稳定性:减少因单次故障导致的服务中断时长
  • 可观测:记录异常发生的时间、原因,便于后续排查

但需注意:重启脚本是应急手段,不是根治措施,生产环境中应配合监控系统(如Prometheus+Grafana)深度追踪根因。


脚本编写的核心原则与常见陷阱

1 五大核心原则

原则 说明
幂等性 重复执行不会产生副作用
防循环 启动后应检测是“已运行”还是“刚启动”
日志记录 每次重启原因、时间、PID需落盘
资源释放 重启前关闭旧socket、清理临时文件
退出策略 连续重启失败N次后发告警并停止尝试

2 三大常见陷阱

  • 陷阱1:未判断进程是否僵尸 – 使用ps aux时可能捕获到defunct进程导致误判
  • 陷阱2:直接kill -9无等待 – 进程未完全释放资源就立即重启,可能端口被占用
  • 陷阱3:crontab频率过高 – 每1分钟检测一次可能造成服务抖动,建议5-10分钟

实战:四种主流重启脚本写法详解

1 方法一:Bash循环监控(最基础)

#!/bin/bash
# 进程名称
PROCESS="nginx"
# 启动命令
START_CMD="/usr/sbin/nginx"
# 日志文件
LOG_FILE="/var/log/process_monitor.log"
while true; do
    # 检测进程是否存在(排除grep本身)
    PID=$(pgrep -x $PROCESS)
    if [ -z "$PID" ]; then
        echo "$(date) - $PROCESS 已崩溃,正在重启..." >> $LOG_FILE
        $START_CMD
        # 等待3秒确认启动成功
        sleep 3
        NEW_PID=$(pgrep -x $PROCESS)
        echo "$(date) - 新PID: $NEW_PID" >> $LOG_FILE
    fi
    sleep 10
done

适用场景:开发环境或单机简单服务
缺点:无错误处理,死循环占用cpu

2 方法二:systemd服务自愈(生产推荐)

[Unit]
Description=My App Service
After=network.target
[Service]
Type=simple
User=appuser
ExecStart=/opt/app/start.sh
ExecStop=/opt/app/stop.sh
Restart=always
RestartSec=5
StartLimitInterval=0
StartLimitBurst=5
[Install]
WantedBy=multi-user.target

核心参数

  • Restart=always:无论退出码都重启
  • RestartSec=5:重启延迟5秒
  • StartLimitBurst=5:连续5次失败后停止

注意:需使用systemctl daemon-reload加载配置,配合systemd-cgtop监控资源更佳。

3 方法三:Python增强版(含错误分级)

import subprocess
import time
import logging
process_name = "java -jar app.jar"
max_retries = 3
retry_count = 0
logging.basicConfig(
    filename='/var/log/process_guard.log',
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s'
)
def is_running():
    result = subprocess.run(
        ['pgrep', '-f', process_name],
        capture_output=True,
        text=True
    )
    return result.returncode == 0
def start_process():
    subprocess.Popen(
        process_name.split(),
        stdout=subprocess.DEVNULL,
        stderr=subprocess.DEVNULL
    )
    time.sleep(2)
    return is_running()
while retry_count < max_retries:
    if not is_running():
        logging.warning(f"进程不存在,尝试第{retry_count+1}次重启")
        if start_process():
            logging.info("进程启动成功")
            retry_count = 0
        else:
            retry_count += 1
            time.sleep(5 * retry_count)  # 指数退避
    else:
        retry_count = 0
    time.sleep(30)
else:
    logging.critical("已达最大重试次数,放弃重启")
    # 可替换为发送告警:requests.post(url, data=...)

优势:可扩展告警模块、指数退避避免雪崩

4 方法四:Docker容器自动重启

version: '3.8'
services:
  app:
    image: myapp:latest
    restart: unless-stopped  # 除非手动停止,否则自动重启
    healthcheck:
      test: ["CMD-SHELL", "curl -f http://localhost:8080/health || exit 1"]
      interval: 15s
      timeout: 3s
      retries: 3
      start_period: 10s

最佳实践:定义HEALTHCHECK时,应同样监控CPU、内存(如ps -p %PID -o %cpu,resident


进阶技巧:健康检查与日志规范

1 健康检查的三种层级

层级 方式 示例
L1-进程存在 ps/pgrep 返回PID即存活
L2-端口监听 netstat/ss 检查TCP端口状态
L3-业务可用 HTTP请求 返回200且body含特定关键字

建议组合使用:先L1快速检测,若通过再L3深度检查,减少误报。

2 日志规范

# 日志示例格式
{
  "timestamp": "2024-01-15T14:32:07Z",
  "level": "WARN",
  "process": "myapp",
  "action": "restart",
  "reason": "pid文件丢失",
  "previous_pid": 12345,
  "new_pid": 67890,
  "restart_count": 3
}

关键字段:时间戳、动作(start/stop/restart)、原因、PID前后值、启动耗时

3 防重复启动

使用flock实现文件锁:

exec 200>/tmp/monitor.lock
flock -n 200 || exit 1  # 若已有实例运行则退出

常见问题问答(QA)

Q1:脚本启动后导致进程频繁重启怎么办?
A:检查两点:1) 启动命令是否使用了相对路径,建议用绝对路径;2) 是否忘记添加nohupsetsid,使用Restart=always的systemd服务在进程崩溃时可能会卷入“启动->崩溃->启动”循环,可设置StartLimitBurst=3限制频率。

Q2:如何区分进程是主动停止还是异常退出?
A:在启动命令中添加exec命令,并捕获退出信号,在脚本中设置trap 'echo "Got SIGTERM, exiting"' SIGINT SIGTERM,若未捕获到用户信号而退出,则判定为异常。

Q3:多个脚本监控同一个进程会冲突吗?
A:绝对会,同一进程若被两个监控脚本检测,可能导致“A刚重启完成,B又检测到未运行并触发重启”,形成竞争条件,解决方案:单例模式,使用文件锁(如前面用flock)确保只有一个监控实例。

Q4:重启后需要保留原进程的上下文吗?
A:视业务而定,有状态服务(如游戏服务器、消息队列)需小心处理,建议:1) 在停止前将状态持久化到共享存储 2) 使用优雅关闭(SIGTERM而非SIGKILL) 3) 启动时恢复状态,无状态服务(如HTTP API)则直接重启即可。

Q5:在Kubernetes中如何实现类似功能?
A:K8s中有内置的livenessProbereadinessProbe,示例:

livenessProbe:
  exec:
    command:
    - cat
    - /tmp/healthy
  initialDelaySeconds: 5
  periodSeconds: 10

Pod状态变为CrashLoopBackOff时,kubelet会自动重启,并且有指数退避(最长5分钟)。


总结建议

编写进程异常重启脚本时,不要追求复杂功能,而是遵循最小可用、可观测、可停止的原则,生产环境首选systemd或Docker内置机制,其次才是自定义脚本(务必包含退出策略和告警),推荐组合:systemd + healthcheck + 日志采集,既保证高可用,又避免雪崩效应。

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