这几天在生产环境发现有几个容器一直不能正常的stop,或者rm 掉,并且查看docker daemon 日志里面会出现不少 msg="Container 5054f failed to exit within 10 seconds of<br/>signal 15 - using the force"
这样的报错,使用的命令为journalctl -xe -u docker
而后在短暂的时间内 docker ps查看到的容器还在运行中,过了一会没有了咱们在建立的时候会提示这个容器已经存在(若是创建一样名称的容器)html
1,docker 经过 containerd 向容器主进程发送
SIGTERM(终止进程)信号后等待一段时间后(默认是10s,能够经过-t 参数来修改),若是从containerd 收到了容器退出消息,那么容器退出成功。
2,若是超过等待的时间以后,仍是没收到容器退出的消息,那么docker 将使用docker kill方式试图终止容器。git
可是对于容器来讲,init 系统进程并非必须的,因此当咱们中止容器的时候,docker 经过 containerd 向容器Pid 为 1 的进程发送 SIGTERM
信号并不必定会被采纳。其实能够分为如下两种状况来讲明:github
1,若是 PID==1 的进程是 init 进程:docker
那么 PID==1 会将 SIGTERM 信号转发给子进程,而后子进程开始关闭,最后容器终止shell
2,若是PID==1 的进程不是 init 进程:xcode
那么容器中的应用进程(Dockerfile 中的 ENTRYPOINT 或 CMD 指令指定的应用)的 PId 就是 1,应用进程直接负责响应 SIGTERM 信号。这个时候又分为两种状况ide
1,应用不处理 SIGTERM 信号:函数
应用没有监听 SIGTERM 信号,或者应用中没有事先处理 SIGTERM 信号的逻辑,应用就不会中止,容器也不会正常终止,会被 调用 docker kill 方式杀死(咱们的程序目前就是这种).net
2,容器中止时间很长:命令行
运行命令 docker stop 以后,docker 会默认等待 10S(默认值,能够修改 docker stop -t 指令),若是 10s后容器尚未终止,docker 就会绕过容器应用直接向内核发送 SIGKILL,内核强行杀死应用,从而终止容器。
1,docker 引擎经过containerd 使用 SIGKILL 发向容器主进程,等待一段时间后,若是从containerd收到容器退出消息,那么容器kill成功
2,在上一步中若是等待超时,Docker引擎将跳过 containerd 本身亲自动手经过kill系统调用向容器主进程发送 SIGKILL 信号。若是此时 kill 系统调用返回主进程不存在,那么 Docker Kill 成功。不然引擎将一直死等到 containerd 经过引擎,容器退出.
上面咱们讲到若是容器内的 PID 进程不能处理 SIGTERM 信号的时候,docker 会等 10S(默认时间),而后调用 kill 去杀死容器的进程,其实这样会形成下面两个问题
Linux 内核中其实会对 PID 1 进程发送特殊的信号量。通常状况下,当给一个进程发送信号时,内核会先检查是否有用户定义的处理函数,若是没有,就会回退到默认行为。例如使用 SIGTERM 直接杀死进程。然而,若是进程的 PID 是 1,那么内核就会特殊对待它。若是没有注册用户处理函数,内核不会回退到默认行为,什么也不作,换句话说,若是你的进程没有处理信号的函数,给他发送 SIGTERM 会一点效果也没有,这个咱们在上面讲过了。
常见的使用是 docker run my-container script. 给 docker run
进程发送SIGTERM
信号会杀掉 docker run
进程,可是容器还在后台运行。
当进程退出时,它会变成僵尸进程,直到它的父进程调用 wait()
( 或其变种 ) 的系统调用。process table 里面会把它的标记为 defunct
状态。通常状况下,父进程应该当即调用 wait()
, 以防僵尸进程时间过长。
若是父进程在子进程以前退出,子进程会变成孤儿进程, 它的父进程会变成 PID 1。所以,init 进程就要对这些进程负责,并在适当的时候调用 wait()
方法。
可是,一般状况下,大部分进程不会处理偶然依附在本身进程上的随机子进程,因此在容器中,会出现许多僵尸进程。
经过上面的解释应该能明白,咱们不能正常退出,或者等 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进程也会死掉,并会清理全部其余的子进程。
开始说的那种状况就是应用进程没有正常退出而形成的问题,
参考文档:
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)/