如何编写进程守护脚本

wen 实用脚本 26

从零到一构建高可用服务监控体系

目录导读

  • 为什么需要进程守护脚本?——守护≠简单重启
  • 核心设计原则:五种常见的进程监控模型对比
  • 实战编写指南:基于Shell和Python的守护脚本模板
  • 进阶优化:日志轮转、告警集成与自愈策略
  • 常见问题FAQ:守护进程vs. systemd/ supervisor到底选谁?

为什么需要进程守护脚本?

Q:系统已经有systemd和supervisor,为什么还要自己写守护脚本? A:在企业生产环境中,systemd适合系统级服务,supervisor适合Python应用,但对于以下场景,自定义守护脚本更灵活:

如何编写进程守护脚本

  • 需要监控特定进程名称(如gunicorn工作进程数多于预期)
  • 需要在进程崩溃后自动拉取最新代码清理残留在内存中的状态文件
  • 需要按业务逻辑恢复(例如先关闭端口再启动,而非简单kill)
  • 需要在不修改系统服务文件的情况下快速部署临时监控(如运维应急脚本)

守护脚本的核心价值在于按需定制故障恢复策略,而非简单的“挂了就重启”。


核心设计原则:五种进程监控模型

监控模型 实现方式 适用场景 缺点
PID文件检测 检查 /var/run/xxx.pid 是否存在且进程存活 传统服务(如mysqld) PID文件可能残留
进程名匹配 ps aux \| grep [x]xx 计数 无PID文件的脚本服务 需防grep自匹配
端口检测 netstat -tnlp \| grep :8080 网络服务(如Nginx) 不适用内部进程
健康检查接口 curl http://localhost:8080/health Web服务(最可靠) 需应用配合
系统服务接口 systemctl is-active xxx 系统级守护 依赖systemd

最佳实践:组合使用“端口检测+进程名匹配”,

# 检测端口存在且进程名匹配
if ! netstat -tnlp 2>/dev/null | grep -q ":80 "; then
    systemctl restart nginx
fi

实战编写指南:Shell版守护脚本模板

1 基础版:每秒检测一次,崩溃立即重启

#!/bin/bash
# 守护进程名称
PROCESS_NAME="myapp"
WORK_DIR="/opt/app"
LOG_FILE="/var/log/guard-${PROCESS_NAME}.log"
# 启动命令
start_app() {
    cd "$WORK_DIR" || exit 1
    nohup ./${PROCESS_NAME} > /dev/null 2>&1 &
    echo "$(date) - 已启动 $PROCESS_NAME PID: $!" >> "$LOG_FILE"
}
# 主监控循环
while true; do
    if ! pgrep -x "$PROCESS_NAME" > /dev/null; then
        echo "$(date) - 检测到 $PROCESS_NAME 进程不存在" >> "$LOG_FILE"
        start_app
    fi
    sleep 3
done

关键点pgrep -x 匹配精确进程名,避免因 bash ./myapp 导致的误判。

2 进阶版:增加最大重启次数与健康检查

import os
import subprocess
import time
import requests
PROCESS_CMD = ["python3", "/opt/app/main.py"]
HEALTH_URL = "http://localhost:8080/health"
MAX_RESTARTS = 5
RESTART_INTERVAL = 60  # 秒
def is_process_alive():
    """通过端口+进程树双重检测"""
    try:
        # 检测端口是否监听
        requests.get(HEALTH_URL, timeout=2)
        return True
    except:
        return False
def restart_process():
    subprocess.run(PROCESS_CMD, cwd="/opt/app")
def main():
    restart_count = 0
    while True:
        if not is_process_alive():
            restart_count += 1
            if restart_count > MAX_RESTARTS:
                # 超过阈值时发送告警(可替换为邮件/钉钉)
                print("ALERT: 进程频繁崩溃,需人工介入")
                time.sleep(300)  # 冷却5分钟
                continue
            restart_process()
            time.sleep(RESTART_INTERVAL)
        else:
            restart_count = 0  # 正常则重置计数器
        time.sleep(5)
if __name__ == "__main__":
    main()

进阶优化:告警集成与自愈策略

1 日志轮转(防止磁盘写满)

# 在守护脚本中追加日志管理
LOG_MAX_SIZE=10485760  # 10MB
if [ -f "$LOG_FILE" ] && [ "$(stat -c%s "$LOG_FILE")" -gt $LOG_MAX_SIZE ]; then
    mv "$LOG_FILE" "${LOG_FILE}.old"
fi

2 多层级恢复策略

  1. 一级恢复:仅重启进程(耗时2秒)
  2. 二级恢复:杀死所有相关子进程后重启(解决僵尸进程)
  3. 三级恢复:执行预定义的修复脚本(如清理临时文件)

3 集成告警(以钉钉为例)

import requests
def send_alert(msg):
    webhook = "https://oapi.dingtalk.com/robot/send?access_token=xxx"
    requests.post(webhook, json={"msgtype":"text","text":{"content":msg}})

常见问题FAQ

Q:守护脚本本身崩溃了怎么办? A:使用双重守护策略——将脚本注册为systemd服务,或使用crontab每分钟检测脚本是否存活:

* * * * * pgrep -x guard.sh || /usr/local/bin/guard.sh &

Q:如何区分“正常退出”和“崩溃”? A:为进程设置退出信号捕获,正常退出时写入退出码文件,守护脚本优先检查退出码文件,若无文件则视为崩溃。

Q:守护脚本与Docker/K8s的冲突点? A:在容器内部应避免启动守护脚本,因为K8s健康检查(Readiness/Liveness Probe)已提供类似功能,守护脚本更适合裸金属服务器被隔离的VM环境

Q:是否应该使用kill -9(SIGKILL)暴力杀死进程? A:不要!kill -9无法让进程清理连接池和临时文件,应优先使用kill -TERM(SIGTERM),若进程无响应,再升级到kill -KILL


守护脚本的黄金法则

  • 不要写死启动命令:将命令路径提取为变量或配置文件
  • 保持幂等性:连续执行两次启动函数不应产生副作用
  • 监控自身消耗:确保守护脚本CPU占用<0.1%,内存<10MB
  • 测试故障场景:使用kill -9模拟崩溃、pkill模拟误杀、systemctl stop模拟管理员操作

对于需要高可用性的服务,建议将上述示例代码部署前进行混沌工程测试——故意杀死进程,验证脚本能否在5秒内恢复,只有经过严苛测试的守护脚本,才能成为线上环境的可靠“看门狗”。

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