1. 问题现象
凌晨三点的告警突然响起,你发现某个本应自动更新的容器仍在倔强地运行。尝试执行docker stop
后,容器状态却卡在Stopping
长达十分钟。更诡异的是,当使用docker exec
进入容器时,原本的应用程序早已消失不见,但容器依然顽固地保持运行状态。
这种场景就像家里的智能音箱突然"闹脾气"——明明已经说了"关机",它却装作没听见继续播放音乐。不同的是,生产环境中的容器任性起来,可能会引发服务中断、资源泄露等严重后果。
2. 原理剖析容器生命周期
理解容器停止机制就像拆解机械手表。当我们执行docker stop
时,Docker会先发送SIGTERM信号(15号),等待10秒(默认超时时间)后发送SIGKILL(9号)。整个过程类似礼貌的敲门询问:"请收拾好行李离开",超时后直接破门而入。
但实际应用中常见三种异常情况:
- 进程未正确处理SIGTERM信号
- 子进程未正确回收(僵尸进程)
- 共享命名空间残留(如共享PID命名空间)
# 示例1:观察容器停止过程(技术栈:Linux + Docker 20.10+)
# 启动测试容器
docker run -d --name test-stop alpine sleep 300
# 发送停止命令并追踪事件
docker stop -t 5 test-stop & # 设置5秒超时
docker events --since 5m --filter container=test-stop
# 预期看到的事件流:
# kill -> die -> stop
# 实际异常时可能只有kill事件
3. 实战解决方案
3.1 优雅终止方案
方案一:自定义停止信号
# 示例2:Node.js应用的优雅停止(技术栈:Node.js 16+)
FROM node:16-alpine
COPY app.js .
STOPSIGNAL SIGINT # 修改默认停止信号
# app.js核心逻辑
process.on('SIGINT', () => {
console.log('收到关闭信号,正在保存状态...');
// 模拟异步清理操作
setTimeout(() => {
process.exit(0);
}, 2000);
});
方案二:preStop钩子脚本
# 示例3:Kubernetes preStop配置(技术栈:K8s 1.20+)
apiVersion: apps/v1
kind: Deployment
spec:
template:
spec:
containers:
- name: app
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "curl -X POST http://localhost:3000/graceful-shutdown"]
3.2 强制终止方案
方案三:直接发送SIGKILL
# 示例4:强制终止容器(技术栈:Docker 20.10+)
# 立即杀死容器进程(慎用!)
docker kill --signal KILL stubborn-container
# 查看退出的信号代码
docker inspect --format='{{.State.ExitCode}}' stubborn-container
# 预期输出:137(128 + 9)
方案四:调整超时时间
# 示例5:延长停止等待时间
docker stop -t 60 slow-container # 设置60秒超时
# 永久修改守护进程配置
# /etc/docker/daemon.json
{
"shutdown-timeout": 60
}
3.3 进程管理方案
方案五:使用init进程
# 示例6:使用tini初始化系统(技术栈:Ubuntu 22.04)
FROM ubuntu:jammy
RUN apt-get update && apt-get install -y tini
ENTRYPOINT ["/usr/bin/tini", "--"]
CMD ["your_app"]
方案六:进程监控脚本
# 示例7:进程监控脚本(技术栈:Bash)
#!/bin/bash
# 主进程
python app.py &
APP_PID=$!
# 信号处理
trap "kill -TERM $APP_PID" TERM
wait $APP_PID # 等待进程退出
echo "主进程已退出,开始清理..."
# 后续清理逻辑
3.4 系统级解决方案
方案七:cgroup杀手
# 示例8:使用cgroup终止进程(技术栈:Linux)
# 查找容器ID对应的cgroup
CONTAINER_ID=your_container_id
CGROUP_PATH=$(cat /sys/fs/cgroup/memory/docker/$CONTAINER_ID/cgroup.procs)
# 终止整个cgroup
cgdelete -r memory,docker:/$CONTAINER_ID
方案八:命名空间隔离
# 示例9:创建独立PID命名空间
docker run --pid=host -it ubuntu bash
# 在容器内
nsenter -t 1 -p kill -9 <PID>
3.9 方案九:全链路检测
# 示例10:全链路检测脚本(技术栈:Bash)
#!/bin/bash
CONTAINER=$1
# 检查容器状态
STATUS=$(docker inspect -f '{{.State.Status}}' $CONTAINER)
if [ "$STATUS" != "running" ]; then
echo "容器不在运行状态"
exit 1
fi
# 获取容器PID
CONTAINER_PID=$(docker inspect -f '{{.State.Pid}}' $CONTAINER)
# 检查进程树
pstree -p $CONTAINER_PID
# 检查僵尸进程
ps -eo stat,pid | grep Z | grep $CONTAINER_PID
# 检查挂载点
ls -l /proc/$CONTAINER_PID/ns
4. 关联技术:那些你必须知道的背景知识
cgroups的进程控制:就像小区物业的电力总闸,cgroups可以精确控制进程组的资源使用。但当某个进程"偷接电线"绕过管控时,就需要直接操作cgroup进行清理。
命名空间隔离机制:Docker为每个容器创建了独立的PID、网络、文件系统等命名空间,就像给每个住户分配了独立的别墅。但当别墅里出现"非法居住者"时,物业需要特殊手段进行清理。
5. 应用场景:对症下药的艺术
- 微服务优雅下线:适合方案一、二,保证服务注册中心及时更新
- 定时任务容器:推荐方案三、四,快速释放资源
- 第三方商业软件:方案五、六是首选,避免修改应用代码
- 遗留系统迁移:方案七、八可作为最后手段
6. 技术选型:利刃双面
方案类型 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
优雅终止 | 数据安全,状态完整 | 依赖应用改造 | 核心业务系统 |
强制终止 | 即时生效,操作简单 | 可能数据丢失 | 测试环境 |
系统级方案 | 彻底解决,一劳永逸 | 需要特权权限 | 物理节点维护 |
7. 注意事项:那些年我们踩过的坑
- 信号屏蔽陷阱:某Java应用因使用
Runtime.getRuntime().addShutdownHook()
导致信号处理冲突 - 僵尸进程累积:某Python脚本因未正确处理子进程,导致容器内积累上百个僵尸进程
- 存储卷锁定:强制终止导致数据库文件锁定,需手动执行
chown
修复权限
8. 总结:与容器和平共处之道
通过九个实战方案,我们建立了从优雅到强制的完整处理体系。记住三个黄金原则:
- 优先考虑应用层优雅退出
- 系统级方案作为保底手段
- 定期检查进程树和资源状态
就像驯养倔强的宠物,既要有关怀备至的耐心,也要有果断处置的决心。掌握这些技巧后,相信那些顽固的容器终将成为温顺的伙伴。