进程管理脚本怎样实现优雅启停

wen 实用脚本 3

本文目录导读:

进程管理脚本怎样实现优雅启停

  1. 核心概念:信号处理 (Signal Handling)
  2. 方案一:通用 Bash 进程管理脚本(最常用)
  3. 方案二:使用 trap 命令 + 后台运行的守护进程
  4. 方案三:针对 Java 应用的 systemctl 服务文件(推荐生产环境)
  5. 方案四:针对容器化环境(Docker/Kubernetes)
  6. 如何选择与优化

实现进程的优雅启停(Graceful Start/Stop)是生产环境中保证服务可靠性的关键,核心思路是:启动时确保依赖就绪,关闭时给进程足够时间完成当前任务(而非直接 kill -9)。

以下是几种常见场景下的优雅启停脚本实现方案:

核心概念:信号处理 (Signal Handling)

  • SIGTERM (15): 请求进程终止,进程可以捕获并执行清理逻辑(保存状态、关闭连接),这是默认的 killkill -15
  • SIGQUIT (3): 类似SIGTERM,但通常还会生成core dump。
  • SIGKILL (9): 强制立即终止,进程无法捕获或忽略。应避免在常规停服中使用。
  • SIGHUP (1): 常用于重载配置文件。

优雅停服的原则:

  1. 发送 SIGTERM (或 SIGQUIT)。
  2. 等待一段时间(如 graceful_timeout)。
  3. 若进程仍在运行,再发送 SIGTERMSIGQUIT 作为警告。
  4. 最终超时后,才使用 SIGKILL

通用 Bash 进程管理脚本(最常用)

这个脚本适用于大多数Java、Python、Node.js、Go应用,它通过PID文件管理进程,并实现优雅关闭。

#!/bin/bash
# 优雅启停脚本: graceful.sh
# 用法: ./graceful.sh {start|stop|restart|status}
APP_NAME="myapp"
APP_PATH="/opt/$APP_NAME"
JAR_NAME="$APP_NAME.jar"  # 或者 Python main.py, Node app.js 等
PID_FILE="/var/run/${APP_NAME}.pid"
LOG_FILE="/var/log/${APP_NAME}.log"
# 优雅停机等待时间(秒)
GRACEFUL_TIMEOUT=30
start() {
    if [ -f "$PID_FILE" ]; then
        pid=$(cat "$PID_FILE")
        if kill -0 "$pid" 2>/dev/null; then
            echo "$APP_NAME is already running (PID: $pid)"
            exit 1
        else
            # 僵尸PID,清理
            rm -f "$PID_FILE"
        fi
    fi
    echo "Starting $APP_NAME..."
    # ❗ 关键:启动命令,根据实际情况修改
    # 确保命令在后台运行并输出PID
    cd "$APP_PATH" || exit 1
    nohup java -jar "$JAR_NAME" >> "$LOG_FILE" 2>&1 &
    # 或: nohup python main.py >> "$LOG_FILE" 2>&1 &
    pid=$!
    echo "$pid" > "$PID_FILE"
    echo "$APP_NAME started (PID: $pid)"
}
stop() {
    if [ ! -f "$PID_FILE" ]; then
        echo "$APP_NAME is not running (PID file not found)"
        return 0
    fi
    pid=$(cat "$PID_FILE")
    if ! kill -0 "$pid" 2>/dev/null; then
        echo "$APP_NAME is not running (stale PID file)"
        rm -f "$PID_FILE"
        return 0
    fi
    echo "Stopping $APP_NAME gracefully (PID: $pid)..."
    # 第一步:发送SIGTERM(优雅终止信号)
    kill -15 "$pid"
    # 第二步:等待一段时间
    waited=0
    while [ $waited -lt $GRACEFUL_TIMEOUT ]; do
        if ! kill -0 "$pid" 2>/dev/null; then
            echo "$APP_NAME stopped gracefully."
            rm -f "$PID_FILE"
            return 0
        fi
        sleep 1
        waited=$((waited + 1))
    done
    # 第三步:超时,再次尝试终止,然后强制终止
    echo "Timeout reached ($GRACEFUL_TIMEOUT sec). Sending force kill..."
    kill -9 "$pid" 2>/dev/null
    wait "$pid" 2>/dev/null  # 等待回收子进程
    rm -f "$PID_FILE"
    echo "$APP_NAME forcefully stopped (PID: $pid)."
}
restart() {
    echo "Restarting $APP_NAME..."
    stop
    sleep 2  # 给系统释放端口等资源的时间
    start
}
status() {
    if [ -f "$PID_FILE" ]; then
        pid=$(cat "$PID_FILE")
        if kill -0 "$pid" 2>/dev/null; then
            echo "$APP_NAME is running (PID: $pid)"
        else
            echo "$APP_NAME PID file exists but process dead"
        fi
    else
        echo "$APP_NAME is not running"
    fi
}
case "$1" in
    start) start ;;
    stop) stop ;;
    restart) restart ;;
    status) status ;;
    *)
        echo "Usage: $0 {start|stop|restart|status}"
        exit 1
        ;;
esac

如何让应用“优雅”? 上述脚本中,kill -15 只是发送信号,真正优雅的关键是应用本身要注册信号处理函数

  • Java (Spring Boot): 默认支持优雅关闭(server.shutdown=graceful)。
  • Pythonsignal.signal(signal.SIGTERM, handler)
  • Node.jsprocess.on('SIGTERM', callback)
  • Gosignal.Notify(ch, syscall.SIGTERM)

使用 trap 命令 + 后台运行的守护进程

如果你希望脚本本身能捕获退出信号,并将其传递给子进程。

#!/bin/bash
# 另一种优雅启停方式:脚本作为守护进程的父进程
pid=""
cleanup() {
    echo "Received stop signal. Shutting down gracefully..."
    if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
        kill -15 "$pid"
        wait "$pid"  # 等待子进程处理完SIGTERM
    fi
    exit 0
}
trap cleanup SIGTERM SIGINT SIGQUIT
echo "Starting service..."
# 启动后台进程
java -jar myapp.jar &
pid=$!
echo "Service started with PID: $pid"
# 等待子进程结束(阻塞)
wait
echo "Service stopped."

用法:

  • ./daemon.sh & :在后台运行此脚本。
  • kill -15 <daemon_pid> :触发 trap,再优雅停止子进程。

针对 Java 应用的 systemctl 服务文件(推荐生产环境)

对于生产环境,使用 systemd 是最佳实践,它原生支持优雅关闭。

创建服务文件 /etc/systemd/system/myapp.service

[Unit]
Description=My Java App
After=network.target
[Service]
Type=simple
User=appuser
WorkingDirectory=/opt/myapp
ExecStart=/usr/bin/java -jar /opt/myapp/myapp.jar
ExecStop=/bin/kill -15 $MAINPID
RestartSec=10
Restart=on-failure
TimeoutStopSec=30  # ⚠️ 关键参数:等待SIGTERM的最长时间,超时后发送SIGKILL
[Install]
WantedBy=multi-user.target

操作命令:

systemctl daemon-reload
systemctl start myapp
systemctl stop myapp      # 会自动发送SIGTERM,等待TimeoutStopSec后发送SIGKILL
systemctl restart myapp

优点:

  • systemd 自动处理信号发送和超时逻辑。
  • 进程监控、日志收集(journalctl)更完善。

针对容器化环境(Docker/Kubernetes)

容器化环境下,优雅启停依赖容器运行时(Docker Engine / kubelet)的信号传递。

Docker 脚本示例(需在应用进程内实现):

#!/bin/bash
# 容器内启动脚本 entrypoint.sh
# 启动主进程,建议使用 exec,保证 PID 1 是应用进程
exec java -jar myapp.jar &
# 等待任何信号(SIGTERM 由 docker stop 发送)
trap 'kill -15 $child; wait' SIGTERM SIGINT
wait

关键点:

  1. exec: 在容器内,exec 让 Java 进程直接成为 PID 1,确保它直接接收 Docker Engine 发出的 SIGTERM。
  2. STOPSIGNAL: 在 Dockerfile 中可设置 STOPSIGNAL SIGTERM(默认就是SIGTERM)。
  3. Dockerfile 示例
    FROM openjdk:17-jre-slim
    COPY myapp.jar /app/
    COPY entrypoint.sh /app/
    RUN chmod +x /app/entrypoint.sh
    STOPSIGNAL SIGTERM
    ENTRYPOINT ["/app/entrypoint.sh"]
  4. Kubernetes: Pod 的 terminationGracePeriodSeconds 字段对应等待 SIGTERM 的超时时间(默认30秒),Kubelet 会先发送 SIGTERM,超时后发送 SIGKILL。

如何选择与优化

场景 推荐方案 核心逻辑
简单 Shell 管理 方案一(通用 Bash 脚本) PID文件 + kill -15 + 超时检测 + kill -9
生产服务器 方案三(systemd 服务) TimeoutStopSec + ExecStop 配置
Docker 单容器 方案四(Docker+exec 主进程PID 1 + 信号传递 + STOPSIGNAL
Kubernetes 集群 方案四(K8s + 容器) terminationGracePeriodSeconds + preStop(可选)

实现优雅启停的三个必要条件:

  1. 应用本身支持: 注册 SIGTERM 处理函数,做清理工作(如 Spring Boot server.shutdown=graceful)。
  2. 正确的信号传递: 确保 PID 1 是实际应用进程(而非 shell 包装器)。
  3. 合理的超时机制: 给进程足够的时间完成当前请求(10-60 秒),超时后再强制终止。

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