本文目录导读:

- 目录导读(Table of Contents)
- 为什么需要“优雅停止”容器?
- 容器优雅停止的核心机制
- Shell脚本实现优雅停止的5种经典方案
- 脚本完整示例:生产级优雅停止脚本
- 常见问题问答(FAQ)
- 总结与最佳实践建议
Shell脚本如何优雅停止容器服务:原理、实践与高频问题解析
目录导读(Table of Contents)
为什么需要“优雅停止”容器?
在容器化部署的日常运维中,docker stop 或 docker-compose down 是最常见的停止指令,但很多运维同学忽略了“优雅停止”(Graceful Shutdown)的重要性,粗暴地使用 docker kill 或 docker stop 默认超时(10秒)后强制杀死容器,可能导致:
- 数据丢失:数据库尚未完成持久化或回滚事务。
- 连接中断:正在处理的HTTP请求被强行终止,客户端收到502错误。
- 分布式系统不一致:正在进行的分布式事务或消息队列消费未完成。
- 日志丢失:日志缓冲区内尚未刷盘的数据被丢弃。
核心原则:容器的停止应当像一个人下班时的“流程化关门”——先关闭业务处理,再释放资源,最后退出进程。
容器优雅停止的核心机制
1 SIGTERM与SIGKILL的区别
| 信号 | 行为 | 应用场景 |
|---|---|---|
| SIGTERM (15) | 进程可捕获、处理、忽略 | 优雅停止首选,进程有机会做清理工作 |
| SIGKILL (9) | 进程无法捕获,立即终止 | 紧急情况,强制杀死无响应进程 |
容器内进程(通常是PID 1)应当实现SIGTERM信号处理函数,执行以下操作:
- 停止接受新请求
- 完成当前正在处理的任务(或优雅回滚)
- 关闭数据库连接、文件句柄、日志流
- 最终退出
2 Docker Stop背后的信号流程
docker stop my_container
↓
Docker daemon 发送 SIGTERM 信号给容器内 PID 1
↓
等待默认超时(10秒,可通过 -t 参数调整)
↓
若进程未退出,发送 SIGKILL
↓
容器状态变为 Exited (0 或非零退出码)
关键点:如果你的容器内进程不具备SIGTERM处理能力(例如某些旧版Node应用),则docker stop效果等同于kill。
Shell脚本实现优雅停止的5种经典方案
1 方案一:标准Docker Stop + 超时控制
#!/bin/bash
# 最简单的优雅停止:指定容器等待时间
CONTAINER_NAME="my-app"
echo "[INFO] 开始优雅停止容器: $CONTAINER_NAME"
docker stop --time=30 "$CONTAINER_NAME"
if [ $? -eq 0 ]; then
echo "[OK] 容器已优雅停止"
else
echo "[ERROR] 停止失败,尝试强制终止..."
docker kill "$CONTAINER_NAME"
fi
适用场景:容器内应用已内置SIGTERM处理(如Spring Boot、Gunicorn、Nginx等)。
2 方案二:显式发送SIGTERM信号
当某些容器仅支持SIGTERM但docker stop超时较短,可手动控制流程:
#!/bin/bash
CONTAINER_NAME="my-node-app"
# 发送SIGTERM
docker kill --signal=SIGTERM "$CONTAINER_NAME"
# 等待进程自行退出(最多60秒)
TIMEOUT=60
while [ $TIMEOUT -gt 0 ]; do
STATE=$(docker inspect --format='{{.State.Status}}' "$CONTAINER_NAME" 2>/dev/null)
if [ "$STATE" = "exited" ]; then
echo "[OK] 进程已优雅退出"
break
fi
sleep 1
((TIMEOUT--))
done
# 超时后强制终止
if [ $TIMEOUT -eq 0 ]; then
echo "[WARN] 超时,强制停止"
docker kill "$CONTAINER_NAME"
fi
3 方案三:结合健康检查的渐进式停止
适用于负载均衡环境,先摘除容器流量,再执行停止:
#!/bin/bash # 假设容器通过consul/nacos注册服务,服务名为 "my-srv" SERVICE_NAME="my-srv" CONTAINER_NAME="my-container" echo "[1/3] 从注册中心摘除服务..." curl -X PUT "http://consul:8500/v1/agent/service/deregister/$SERVICE_NAME" || true sleep 2 # 等待同步 echo "[2/3] 等待正在处理的请求完成(假设预留5秒)..." sleep 5 echo "[3/3] 执行容器优雅停止..." docker stop --time=30 "$CONTAINER_NAME"
最佳实践:适用于微服务架构,保证客户端不感知服务中断。
4 方案四:停止依赖链中的多个容器
使用docker-compose时,需要按依赖顺序逆向停止:
#!/bin/bash
COMPOSE_PROJECT="my-project"
# 定义停止顺序:依赖者先停,被依赖者后停
ORDERS=("web" "api" "db" "redis")
for svc in "${ORDERS[@]}"; do
echo "[INFO] 停止服务: $svc"
docker-compose -p "$COMPOSE_PROJECT" stop "$svc" 2>/dev/null || \
docker-compose -p "$COMPOSE_PROJECT" kill "$svc" 2>/dev/null
done
echo "[DONE] 所有服务已停止"
5 方案五:带有回滚机制的安全停止
如果停止失败,可以尝试恢复容器:
#!/bin/bash
CONTAINER_NAME="my-critical-app"
# 备份当前容器状态(若容器未运行则跳过)
if [ "$(docker inspect -f '{{.State.Running}}' "$CONTAINER_NAME" 2>/dev/null)" = "true" ]; then
docker commit "$CONTAINER_NAME" "${CONTAINER_NAME}_backup:latest" > /dev/null
fi
# 尝试优雅停止
echo "[INFO] 尝试优雅停止..."
docker stop --time=20 "$CONTAINER_NAME"
if [ $? -ne 0 ]; then
echo "[WARN] 优雅停止失败,启动回滚备份容器..."
docker run -d --name "${CONTAINER_NAME}_rollback" "${CONTAINER_NAME}_backup:latest"
fi
脚本完整示例:生产级优雅停止脚本
以下脚本整合了健康检查、信号发送、超时控制与日志记录,建议直接用于生产环境(注意替换变量):
#!/bin/bash
# =============================================================
# 文件: graceful_stop.sh
# 描述: 通用容器优雅停止脚本,支持单容器与容器组
# 依赖: docker, curl(用于服务摘除)
# 用法: ./graceful_stop.sh [container_name1] [container_name2 ...]
# =============================================================
set -e
# 配置
GRACEFUL_TIMEOUT=30
KILL_TIMEOUT=5
HEALTH_CHECK_URL="http://localhost:8080/health"
# 颜色输出
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
# 检查参数
if [ $# -eq 0 ]; then
log_error "请提供至少一个容器名称!"
echo "用法: $0 container1 [container2 ...]"
exit 1
fi
graceful_stop() {
local container=$1
log_info "开始处理容器: $container"
# 检查容器是否存在且运行中
local running=$(docker inspect -f '{{.State.Running}}' "$container" 2>/dev/null || echo "false")
if [ "$running" != "true" ]; then
log_warn "容器 $container 未运行,跳过"
return 0
fi
# 步骤1:尝试服务摘除(可选)
if [ -n "$HEALTH_CHECK_URL" ]; then
local service_id=$(docker inspect -f '{{index .Config.Labels "consul.service.id"}}' "$container" 2>/dev/null || echo "")
if [ -n "$service_id" ]; then
log_info "正在从Consul摘除服务: $service_id"
curl -sfX PUT "http://consul:8500/v1/agent/service/deregister/$service_id" > /dev/null 2>&1 || true
sleep 2
fi
fi
# 步骤2:发送SIGTERM并等待
log_info "发送SIGTERM信号..."
docker kill --signal=SIGTERM "$container" 2>/dev/null || true
# 步骤3:等待容器退出
local elapsed=0
while [ $elapsed -lt $GRACEFUL_TIMEOUT ]; do
local status=$(docker inspect -f '{{.State.Status}}' "$container" 2>/dev/null || echo "unknown")
if [ "$status" = "exited" ]; then
local exit_code=$(docker inspect -f '{{.State.ExitCode}}' "$container")
log_info "容器已退出, 退出码: $exit_code"
return 0
fi
sleep 1
((elapsed++))
done
# 步骤4:超时,强制停止
log_warn "等待超时($GRACEFUL_TIMEOUT秒),执行强制停止..."
docker kill "$container" > /dev/null 2>&1 || true
# 步骤5:验证最终状态
sleep 2
local final_status=$(docker inspect -f '{{.State.Status}}' "$container" 2>/dev/null || echo "not_found")
if [ "$final_status" = "exited" ]; then
log_info "强制停止成功,容器已退出"
else
log_error "容器状态异常: $final_status"
return 1
fi
}
# 主循环
for container in "$@"; do
graceful_stop "$container" || {
log_error "停止容器 $container 失败"
exit 1
}
done
log_info "所有操作完成"
使用方式:
chmod +x graceful_stop.sh ./graceful_stop.sh web-app api-app db-app
常见问题问答(FAQ)
Q1:为什么有些容器使用 docker stop 却无法优雅退出?
A:主要原因有两个:
- 容器内PID 1进程未实现SIGTERM处理函数(例如
/bin/bash启动的进程不会传递信号给子进程)。 - 进程处理信号时阻塞(例如等待数据库连接池超时)。
解决方案:使用exec命令启动主进程,或使用tini作为init进程。
Q2:docker stop 的默认超时时间如何修改?
A:
- 单容器:
docker stop --time=60 my_container - docker-compose:在
docker-compose.yml的service下添加stop_grace_period: 60s - 全局调整:修改Docker daemon配置(不推荐,影响所有容器)
最佳实践:根据不同应用类型设置不同超时(数据库30秒,Web应用5-10秒即可)。
Q3:如果容器内进程不响应SIGTERM,是否还有其他优雅方式?
A:可以尝试:
- 通过容器的管理接口(如Spring Boot的/actuator/shutdown)。
- 直接在容器内执行
docker exec my_container pkill -TERM my_process。 - 使用
docker kill --signal=SIGQUIT(可捕获,但通常用于生成core dump)。
Q4:如何验证容器是否真正完成了优雅停止?
A:查看容器退出时间和日志:
# 查看退出时间和退出码
docker inspect my_container --format='{{.State.FinishedAt}} {{.State.ExitCode}}'
# 查看最后几行日志
docker logs --tail 20 my_container
如果日志中包含“shutdown complete”、“请求处理完毕”等字样,说明优雅停止成功。
Q5:在Kubernetes下,优雅停止的机制是否相同?
A:Kubernetes的Pod停止流程类似(发送SIGTERM → 等待terminationGracePeriodSeconds → 发送SIGKILL),但K8s还提供了preStop钩子,可以在发送SIGTERM前执行自定义命令(如服务摘除),建议阅读K8s官方文档相关章节。
总结与最佳实践建议
优雅停止容器不仅是运维规范,更是保证服务可靠性的必要条件,以下是核心建议:
- 容器内进程必须处理SIGTERM:使用
exec启动、或在Dockerfile中使用CMD ["executable", "param"]形式。 - 超时设置要合理:根据应用特性设置
stop_grace_period(数据库可长至30-60秒,API服务5-10秒)。 - 脚本应包含幂等性和容错:防止容器不存在、已停止等异常。
- 结合服务发现:停止前先从注册中心摘除,避免流量中断。
- 日志记录:每次停止操作记录到日志文件,便于事后审计。
- 测试验证:定期进行优雅停止演练,观察应用日志和客户端连接中断情况。
优雅停止不是“玄学”,而是有章可循的工程实践,掌握Shell脚本的信号控制与条件等待,就能从容应对容器化环境的各种停止场景。