LabHub
学习 学习路径 课程

Docker 基础

docker stop 为什么要花整整 10 秒

在 LabHub 中继续学习

一句话总结

容器中的第一个进程是 PID 1,而 Linux 内核对 PID 1 的 处理方式与普通进程不同。docker stop 会等待 10 秒、Ctrl+C 不起作用, 以及僵尸进程不断堆积,全都源于此处。

概念图: PID 1 · SIGTERM · SIGKILL · 收到了 SIGTERM,却没有退出

为什么需要理解 PID 1

在部署脚本中加入 docker stop 后,每个容器都恰好需要 10 秒才能停止。 20 个容器就是 200 秒。日志里没有任何错误,只是速度很慢。

原因如下:docker stop 会先发送 SIGTERM,等待一段时间; 如果进程仍未退出,再发送 SIGKILL。默认“等待”时间就是 10 秒。 因此,耗时 10 秒意味着进程收到了 SIGTERM,却没有退出

比较四种 PID 1 处理 docker stop 十秒等待时间的时间线。shell 形式和没有处理器的 exec 形式都无人接收 SIGTERM,耗尽十秒后被 SIGKILL 终止;带处理器的 exec 形式与 --init 会立即正常退出

为什么没有退出?普通进程没有注册信号处理器时,内核会替它执行默认行为,例如终止进程。 但是,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 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 形式的差异如何体现在退出时间和 进程树中作答。