本文目录导读:

- 核心概念:信号处理 (Signal Handling)
- 方案一:通用 Bash 进程管理脚本(最常用)
- 方案二:使用
trap命令 + 后台运行的守护进程 - 方案三:针对 Java 应用的
systemctl服务文件(推荐生产环境) - 方案四:针对容器化环境(Docker/Kubernetes)
- 如何选择与优化
实现进程的优雅启停(Graceful Start/Stop)是生产环境中保证服务可靠性的关键,核心思路是:启动时确保依赖就绪,关闭时给进程足够时间完成当前任务(而非直接 kill -9)。
以下是几种常见场景下的优雅启停脚本实现方案:
核心概念:信号处理 (Signal Handling)
- SIGTERM (15): 请求进程终止,进程可以捕获并执行清理逻辑(保存状态、关闭连接),这是默认的
kill和kill -15。 - SIGQUIT (3): 类似SIGTERM,但通常还会生成core dump。
- SIGKILL (9): 强制立即终止,进程无法捕获或忽略。应避免在常规停服中使用。
- SIGHUP (1): 常用于重载配置文件。
优雅停服的原则:
- 发送
SIGTERM(或SIGQUIT)。 - 等待一段时间(如
graceful_timeout)。 - 若进程仍在运行,再发送
SIGTERM或SIGQUIT作为警告。 - 最终超时后,才使用
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)。 - Python:
signal.signal(signal.SIGTERM, handler)。 - Node.js:
process.on('SIGTERM', callback)。 - Go:
signal.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
关键点:
exec: 在容器内,exec让 Java 进程直接成为 PID 1,确保它直接接收 Docker Engine 发出的 SIGTERM。STOPSIGNAL: 在 Dockerfile 中可设置STOPSIGNAL SIGTERM(默认就是SIGTERM)。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"]
- 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(可选) |
实现优雅启停的三个必要条件:
- 应用本身支持: 注册 SIGTERM 处理函数,做清理工作(如 Spring Boot
server.shutdown=graceful)。 - 正确的信号传递: 确保 PID 1 是实际应用进程(而非 shell 包装器)。
- 合理的超时机制: 给进程足够的时间完成当前请求(10-60 秒),超时后再强制终止。