docker stop 或者 docker kill 不能停止容器
docker stop 或者 docker kill 不能停止容器
原因
这几天在生产环境发现有几个容器一直不能正常的stop,或者rm 掉,而且查看docker daemon 日志里面会出现很多 msg="Container 5054f failed to exit within 10 seconds of signal 15 - using the force"
这样的报错,使用的命令为journalctl -xe -u docker
然后在短暂的时间内 docker ps查看到的容器还在运行中,过了一会没有了我们在创建的时候会提示这个容器已经存在(如果建立同样名称的容器)
docker stop 主流程
1,docker 通过 containerd 向容器主进程发送
SIGTERM(终止进程)信号后等待一段时间后(默认是10s,可以通过-t 参数来修改),如果从containerd 收到了容器退出消息,那么容器退出成功。
2,如果超过等待的时间之后,还是没收到容器退出的消息,那么docker 将使用docker kill方式试图终止容器。
但是对于容器来说,init 系统进程并不是必须的,所以当我们停止容器的时候,docker 通过 containerd 向容器Pid 为 1 的进程发送 SIGTERM
信号并不一定会被采纳。其实可以分为以下两种情况来说明:
1,如果 PID==1 的进程是 init 进程:
那么 PID==1 会将 SIGTERM 信号转发给子进程,然后子进程开始关闭,最后容器终止
2,如果PID==1 的进程不是 init 进程:
那么容器中的应用进程(Dockerfile 中的 ENTRYPOINT 或 CMD 指令指定的应用)的 PId 就是 1,应用进程直接负责响应 SIGTERM 信号。这个时候又分为两种情况
1,应用不处理 SIGTERM 信号:
应用没有监听 SIGTERM 信号,或者应用中没有事先处理 SIGTERM 信号的逻辑,应用就不会停止,容器也不会正常终止,会被 调用 docker kill 方式杀死(我们的程序目前就是这种)
2,容器停止时间很长:
运行命令 docker stop 之后,docker 会默认等待 10S(默认值,可以修改 docker stop -t 指令),如果 10s后容器还没有终止,docker 就会绕过容器应用直接向内核发送 SIGKILL,内核强行杀死应用,从而终止容器。
docker kill主流程
1,docker 引擎通过containerd 使用 SIGKILL 发向容器主进程,等待一段时间后,如果从containerd收到容器退出消息,那么容器kill成功
2,在上一步中如果等待超时,Docker引擎将跳过 containerd 自己亲自动手通过kill系统调用向容器主进程发送 SIGKILL 信号。如果此时 kill 系统调用返回主进程不存在,那么 Docker Kill 成功。否则引擎将一直死等到 containerd 通过引擎,容器退出.
docker 中 PID 进程不能处理 SIGTERM 信号的危害
上面我们讲到如果容器内的 PID 进程不能处理 SIGTERM 信号的时候,docker 会等 10S(默认时间),然后调用 kill 去杀死容器的进程,其实这样会造成下面两个问题
1,进程不能正常终止
Linux 内核中其实会对 PID 1 进程发送特殊的信号量。一般情况下,当给一个进程发送信号时,内核会先检查是否有用户定义的处理函数,如果没有,就会回退到默认行为。例如使用 SIGTERM 直接杀死进程。然而,如果进程的 PID 是 1,那么内核就会特殊对待它。如果没有注册用户处理函数,内核不会回退到默认行为,什么也不做,换句话说,如果你的进程没有处理信号的函数,给他发送 SIGTERM 会一点效果也没有,这个我们在上面讲过了。
常见的使用是 docker run my-container script. 给 docker run
进程发送SIGTERM
信号会杀掉 docker run
进程,但是容器还在后台运行。
2,孤儿僵尸进程不能正常回收
当进程退出时,它会变成僵尸进程,直到它的父进程调用 wait()
( 或其变种 ) 的系统调用。process table 里面会把它的标记为 defunct
状态。一般情况下,父进程应该立即调用 wait()
, 以防僵尸进程时间过长。
如果父进程在子进程之前退出,子进程会变成孤儿进程, 它的父进程会变成 PID 1。因此,init 进程就要对这些进程负责,并在适当的时候调用 wait()
方法。
但是,通常情况下,大部分进程不会处理偶然依附在自己进程上的随机子进程,所以在容器中,会出现许多僵尸进程。
解决容器进程收不到 SIGTERM 信号
通过上面的解释应该能明白,我们不能正常退出,或者等 10s 才能退出的主要原因就是 PID 1 的进程不能处理/不处理 SIGTERM 信号造成的,知道问题所在了,那么久好办了,有如下几种解决方案:
1,让你们公司的程序代码支持处理 SIGTERM 信号。
当我们 pid 1 的进程(自己公司的代码)能处理 SIGTERM 信号,那么这个问题不就解决了吗?比较推荐这种方式,但是涉及到开发有一定的开发量,还是我们自己先用下面的方式解决。
2,构建 docker 包的时候使用 exec 模式的 ENTRYPOINT 指令
docker 官方文档指出:
You can specify a plain string for the
ENTRYPOINT
and it will execute in/bin/sh -c
. This form will use shell processing to substitute shell environment variables, and will ignore anyCMD
ordocker run
command line arguments. To ensure thatdocker stop
will signal any long runningENTRYPOINT
executable correctly, you need to remember to start it withexec
:你可以为ENTRYPOINT指定一个普通字符串,它将在/bin/sh -c中执行。这个形式将使用shell处理来替代shell环境变量,并且会忽略任何CMD或docker运行命令行参数。为了确保docker stop会正确地提示任何长期运行的ENTRYPOINT可执行文件,你需要记得用exec启动它。
使用方式很简单,我们只需要按照如下格式编写 Dockerfile 即可
ENTRYPOINT exec COMMAND param1 param2
以这种方式启动,exec 就会将 shell 进程替换为 COMMAND 进程,
但是这种方式还是需要程序支持 SIGTERM,所以不推荐
3,在容器中使用 init 进程
当上面两种情况我都不推荐的时候,那我们就只能用这种方式了。
在容器里面添加一个 init 系统,让他去处理 SIGTERM 信号。
init 系统有很多,这推荐下面两种
1,tini
FROM alpine:3.7
...
RUN apk add --no-cache tini
ENTRYPOINT ["/sbin/tini", "--", "COMMAND"]
现在 tini
就是 PID 1,它会将收到的系统信号转发给子进程 COMMAND。
使用 tini 后应用还需要处理 SIGTERM 吗?
答案是肯定不需要啊,如果需要那我们还大费周章的来讲上面这么多废话吗?
当一个进程为普通进程,只要他收到系统信号,就会执行与该信号相关的默认动作,不需要再代码中显示实现逻辑,因此容器可以优雅的终止,而不需要强制 kill
他也是一个小型的 init 服务,他启动一个子进程并转发所有接收到的信号量给子进程。而且不需要修改应用代码。
FROM alpine:3.7
...
RUN wget -O /usr/local/bin/dumb-init https://github.com/Yelp/dumb-init/releases/download/v1.2.0/dumb-init_1.2.0_amd64 &&\
chmod +x /usr/local/bin/dumb-init
# Runs "/usr/bin/dumb-init -- /my/script --with --args"
ENTRYPOINT ["/usr/bin/dumb-init", "--"]
CMD ["/my/script", "--with", "--args"]
需要注意的一点是:
虽然现在 PID 1 进程不是应用进程了,应用的行为和在没有 init 进程时是一样的。如果应用进程死掉,那么 init进程也会死掉,并会清理所有其他的子进程。
总结
开始说的那种情况就是应用进程没有正常退出而造成的问题,
ENTRYPOINT的两种模式
参考文档:
docker init https://xcodest.me/docker-init-process.html
https://www.jianshu.com/p/813d8362d497
https://www.coder.work/article/41140
https://blog.csdn.net/shanzhizi/article/details/47320595
http://shareinto.github.io/2019/01/30/docker-init(1)/