docker stop 为什么要花整整 10 秒
一句话总结
容器中的第一个进程是 PID 1,而 Linux 内核对 PID 1 的
处理方式与普通进程不同。docker stop 会等待 10 秒、Ctrl+C 不起作用,
以及僵尸进程不断堆积,全都源于此处。
为什么需要理解 PID 1
在部署脚本中加入 docker stop 后,每个容器都恰好需要 10 秒才能停止。
20 个容器就是 200 秒。日志里没有任何错误,只是速度很慢。
原因如下:docker stop 会先发送 SIGTERM,等待一段时间;
如果进程仍未退出,再发送 SIGKILL。默认“等待”时间就是 10 秒。
因此,耗时 10 秒意味着进程收到了 SIGTERM,却没有退出。
为什么没有退出?普通进程没有注册信号处理器时,内核会替它执行默认行为,例如终止进程。 但是,PID 1 不具备这种默认行为。 内核假定 PID 1 是 init,因而会直接丢弃 没有注册处理器的信号。这是一种保护机制,因为系统中的最后一个进程如果意外退出, 就会引发 kernel panic。
因此,如果通过 CMD ["python", "app.py"] 启动的 Python 没有注册 SIGTERM 处理器,
该容器就会忽略 SIGTERM。10 秒后,它会被 SIGKILL 强制终止。
尚未关闭的连接与只写了一半的文件,也会在原处被截断。
它是如何工作的
第二个陷阱是 shell 形式。
CMD python app.py # 셸 폼 → /bin/sh -c "python app.py"
CMD ["python", "app.py"] # exec 폼 → python 이 직접 PID 1
使用 shell 形式时,PID 1 不是 Python,而是 /bin/sh。sh 即使收到 SIGTERM,
也不会把它转发给子进程。应用程序甚至根本看不到这个信号。
| 场景 | PID 1 | SIGTERM 的结果 |
|---|---|---|
shell 形式 CMD python app.py |
/bin/sh |
不传给应用 → 10 秒后 KILL |
| exec 形式,无处理器 | python |
被内核丢弃 → 10 秒后 KILL |
| exec 形式,有处理器 | python |
立即正常退出 ✅ |
使用 --init |
docker-init |
转发信号并回收僵尸进程 ✅ |
第三个问题是僵尸进程。子进程退出后,父进程必须通过 wait() 收取退出状态,
该进程才会从进程表中消失。父进程不进行回收时,子进程就会作为僵尸残留。
这种清理原本是 init 的职责,但如果容器中的 PID 1 是应用程序,该应用通常不会
回收僵尸进程。持续运行 shell 脚本的容器中,进程数量经过数天不断增加,通常就是这个原因。
--init 会在 PID 1 位置插入一个极小的 init 进程,同时解决信号转发与僵尸回收问题。
exec 与 attach 不同
docker exec |
docker attach |
|
|---|---|---|
| 执行的操作 | 在容器中运行一个新进程 | 连接到 PID 1 的标准输入输出 |
| 退出时 | 只终止该进程 | 按下 Ctrl+C 会终止容器 |
| 用途 | 调试、检查文件 | 直接查看原进程的输出 |
很多人曾经用 attach 连接运行中的容器,然后习惯性地按下 Ctrl+C,
结果停止了服务。需要进入容器时,请使用 exec。
实际工作中会遇到的情况
- 部署缓慢 →
docker stop每次都等待 10 秒。应用程序没有处理 SIGTERM。 - 请求被中断 → 进程被 SIGKILL 终止,正在处理的请求没有响应就断开。 这与尚未从负载均衡器移除就终止的症状完全相同,因此容易混淆。
- 进程不断堆积 →
ps中的<defunct>越来越多。没有进程负责回收僵尸。
如何查看无法进入的容器内部
docker exec -it ... sh 只有在镜像中存在 shell 时才有效。distroless 或 scratch 镜像
既没有 shell,也没有 ps。但仍然可以查看内部——因为命名空间也能从外部观察。
直接从主机查看该进程。 容器中的 PID 1 在主机上也只是一个普通进程, 只是位于不同的命名空间。
pid=$(docker inspect -f '{{.State.Pid}}' myapp)
ps -p $pid -o pid,ppid,rss,etime,args
ls -l /proc/$pid/fd # 열린 파일과 소켓
cat /proc/$pid/limits # ulimit
cat /proc/$pid/cgroup # 어느 cgroup 에 매였는지
无需挂载就能读取文件系统。 /proc/<pid>/root 是该进程所看到的根目录。
可以使用主机上的工具读取容器内的文件。
cat /proc/$pid/root/etc/app/config.yml
把工具从外部带进命名空间。 nsenter 会进入指定命名空间并执行命令,
镜像内部不需要包含这些工具。
sudo nsenter -t $pid -n ss -tlnp # 그 컨테이너의 네트워크에서 포트를 본다
sudo nsenter -t $pid -m ls /tmp # 그 컨테이너의 마운트에서 파일을 본다
在 Kubernetes 中,临时容器可以完成同样的工作。将一个包含工具的容器 附加到目标 Pod,并让它共享进程命名空间。
kubectl debug -it pod/myapp --image=nicolaka/netshoot --target=app
省略 --target 就看不到进程。 此时,新容器只共享网络,PID 命名空间仍然独立。
要查看进程,必须指定目标容器。
日志不可见通常是缓冲造成的。 应用程序通过管道写入 stdout 时,libc 会改用块缓冲;
在填满 4 KB 之前,不会输出任何内容。Python 可设置 PYTHONUNBUFFERED=1,
普通命令则使用 stdbuf -oL 按行输出。所谓“日志没有打印”,往往只是“还没输出”。
下一步检查什么
接下来的测验会根据前一个实验的观察,区分 PID 1 的信号转发与僵尸回收,以及
docker stop 触发强制终止的条件。请依据 exec 形式与 shell 形式的差异如何体现在退出时间和
进程树中作答。