简介
非常多的文档都在讨论如何通过独立的容器打包并运行你的应用,但很少有人讨论如何如何正确停止你的容器。实际上这取决于你的应用,如何停止容器对部分应用来说是非常重要的。
比如你应用正在处理HTTP请求,你希望在停止前能完成所有未完成的请求。如果你的应用正在写入文件,你也许希望在停止容器前能够正确的刷新数据到磁盘并关闭文件。
简单的做法是你启动一个容器后一直运行下去,永远不去关闭它。但是你的应用很可能需要在某一时间点停止或重启,用于方便的更新或迁移至其他主机节点。在这些时候,你需要平稳的关闭正在运行中的容器,而不是突然的断开用户连接并损坏文件。
所以,下面让我们来看一下如何优雅的停止容器
Sending Signals
docker守护进程提供了许多不同的Docker命令可以用来停止运行中的容器:
docker stop
当你执行docker stop
命令时,Docker首先会友好的求情容器进程停止,如果10秒后仍未停止,便会强制kill掉该进程。这也是为什么有时执行docker stop
命令会等待很久(10s)才执行成功。
docker stop
命令试图向容器内的根进程(PID1)发送SIGTERM信号来停止正在运行中的容器。如果根进程在超时时间(默认10s)内没有退出,便会发出SIGKILL信号
尽管部分进程可以选择忽略SIGTERM信号。但是SIGKILL信号会直接发送至内核,从而终止程序。这个过程中进程甚至都没有看到SIGKILL信号
当我们使用docker stop
时,你唯一可控制的就是Docker守护进程等待根进程关闭的超时时间,也就是多久未关闭便发送SIGKILL信号。
docker stop --time=30 foo
docker kill
默认情况下,docker kill
命令不会向容器提供一个优雅关闭的机会。它只是向容器发送SIGKILL信号来终止容器。不过,它允许通过--signal
参数来指定向容器发送除SIGKILL之外的信号。
例如,如果你希望向容器发送SIGINT信号(等于在终端执行Ctrl-C)至容器,你可以使用下列命令:
docker kill --signal=SIGINT foo
与docker stop
命令不同,docker kill
命令没有任何的超时时间,它只是发送指定的信号至容器。
请注意,docker kill
命令与它所模仿的linux kill
命令不同。在没有其他参数指定的情况下,kill
命令默认会发送SIGTERM信号(类似docker stop
)。而docker kill
更类似Linux下的kill -9
命令
docker rm -f
最后一个可以用于停止运行中容器的命令是与docker rm
命令一起使用的--force
或-f
参数。通常,docker rm
用于删除一个已停止的容器,但当它与--force
参数一起使用时,会首先发送SIGKILL信号,等待容器停止后再删除。
docker rm --force foo
如果你需求时抹除一个容器运行的所有痕迹,那么docker rm -f
命令是很方便的。但,如果你希望优雅的停止容器,则应该尽量避免使用该命令
处理信号
虽然操作系统定义了一系列信号,但进程响应不同信号的方式是根据不同应用程序来定义的。
例如,如果你想优雅的关闭一个ningx服务,需要发送SIGQUIT信号。默认情况下docker命令不会发送SIGQUIT信号的,所以需要通过下列docker kill
命令指定发送信号( graceful shutdown of an nginx server):
docker kill --signal=SIGQUIT nginx
nginx收到SIGQUIT信号后的日志如下:
2015/05/11 20:30:20 [notice] 1#0: signal 3 (SIGQUIT) received, shutting down
2015/05/11 20:30:20 [notice] 9#0: gracefully shutting down
2015/05/11 20:30:20 [notice] 9#0: exiting
2015/05/11 20:30:20 [notice] 9#0: exit
2015/05/11 20:30:20 [notice] 1#0: signal 17 (SIGCHLD) received
2015/05/11 20:30:20 [notice] 1#0: worker process 9 exited with code 0
2015/05/11 20:30:20 [notice] 1#0: exit
与之相对的,Apache应用则需要SIGWINCH信号来实现优雅关闭(graceful shutdown)
docker kill --signal=SIGWINCH apache
根据Apache文档,SIGTERM会导致服务器立即退出并终止任何正在进行的请求,所以尽量避免在Apache容器上使用docker stop
。
如果你在容器内运行了第三方的应用,那么你需要查看对应的应用文档来确认此应用收到不同信号时的处理方式。单纯的执行docker stop
也许并不会达到你想要的效果
当你在容器内运行你自己的应用,你必须定义该应用如何处理不同的信号。你必须确认你的代码里捕获了对应的信号,并且可以优雅的关闭应用。如果你知道你的应用将在docker容器内运行,那么你可以将SIGTERM信号配置为优雅关闭的信号,因为这是docker stop
默认的信号。
无论你使用那种编程语言,基本上都会支持信号处理,我收集了些不同语言的信号处理模块/包:
如果你使用Go语言,推荐使用 tylerb/graceful 包。它会自动启动http.Handler的优雅关闭,响应SIGTERM或SIGINT信号
Receiving Signals
在编写你的代码中正确的监听信号,优雅的关闭应用是很重要的。同时也要确认你的应用被正确的打包成image,以便它可以正常的接受到docker命令发送的信号(确认该应用运行在PID1上,并且未使用sh -c
来启动等等)。
为了演示,我们创建一个简单的应用程序运行在容器内:
#!/usr/bin/env bash
trap 'exit 0' SIGTERM
while true; do :; done
上面的bash脚本只是简单的运行一个无限循环,但如果收到了SIGTERM信号,便会以状态码0的方式退出
我们用下例Dockerfile将脚本打包成image
FROM ubuntu:trusty
COPY loop.sh /
RUN chmod +x /loop.sh
CMD /loop.sh
现在我们构建这个image,运行容器,然后立即通过docker stop
停止它
$ docker build -t loop .
Sending build context to Docker daemon 3.072 kB
Sending build context to Docker daemon
Step 0 : FROM ubuntu:trusty
---> 07f8e8c5e660
Step 1 : COPY loop.sh /
---> 161f583a7028
Removing intermediate container e0988f66358a
Step 2 : CMD /loop.sh
---> Running in 6d6664be02da
---> 18b3feccee90
Removing intermediate container 6d6664be02da
Successfully built 18b3feccee90
$ docker run -d loop
64d39c3b49147f847722dbfd0c7976315533a729d9453c34cb6cbdaa11d46c21
$ docker stop 64d39c3b
当你完成上述步骤,会发现docker stop
命令大概花了10s才执行完成。这说明你的容器忽略了SIGTERM信号,直到接受了10s后的强制退出信号(SIGKILL),容器才被停止。
我们通过查看容器的退出状态码来验证这一点:
docker inspect -f '{{.State.ExitCode}}' 64d39c3b
137
但根据我们在程序中的配置,如果捕获到了SIGTERM信号应该以状态码0退出。而不是137(事实上,状态码大于128表示表进程退出于未处理的信号,137=128+9 表示该进程被终止于信号9(SIGKILL))
这和我们编写程序时的设想并不一样,我们的程序被编写为捕获SIGTERM信号,并优雅的退出(exit 0)。我们知道执行docker stop
命令便会发送SIGTERM信号至容器内根进程。然而,我们的应用似乎从未收到SIGTERM信号。
为了理解具体原因,我们启动另一个容器,并查看它的进程列表:
$ docker run -d loop
512c36b5b517b3a43246b519bc5cdb756cdbc4d2ff1e0a3984e83b094f3db136
$ docker exec 512c36b5 ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 16:03 ? 00:00:00 /bin/sh -c /loop.sh
root 13 1 61 16:03 ? 00:00:10 bash /loop.sh
root 14 0 0 16:03 ? 00:00:00 ps -ef
需要注意的一点,在上述输出中我们的应用loop.sh
并未运行在容器的PID1进程。实际上它作为了PID1进程/bin/sh
的子进程在运行。
当你执行docker stop
或docker kill
发送信号至容器,该信号只会被发送给PID1进程。
因为/bin/sh
不会转发信号至任何子进程。所以我们的应用将永远不会收到SIGTERM信号。显然要解决这个问题,就需要将我们的进程作为PID1进程运行。
为此,我们需要返回查看Dockerfile中用于配置启动命令的CMD指令。实际上CMD指令可以采取不同的形式,上述例子中我们才采用了如下的shell格式:
CMD command param1 param2
当使用shell格式时,指定的命令将以/bin/sh -c
的方式作为子进程执行。在之前的例子中可以看到PID1进程为/bin/sh -c /loop.sh
,这代表/bin/sh -c
作为PID1运行,然后fork/execs了我们指定的程序。
幸好docker除了shell格式外,也支持以exec格式的CMD指令(详情参考:https://www.jianshu.com/p/9de29b2527ee):
CMD ["executable","param1","param2"]
当使用exec格式的CMD指令时,命令将直接作为PID1进程启动。
我们修改之前的Dockerfile文件:
FROM ubuntu:trusty
COPY loop.sh /
RUN chmod +x /loop.sh
CMD ["/loop.sh"]
重新构建image,并运行容器查看容器内的进程:
<pre class="md-fences md-end-block ty-contain-cm modeLoaded" spellcheck="false"
现在,我们的程序作为PID1运行了,让我们再尝试发送SIGTERM信号后查看容器的退出状态:
$ docker stop 4dda905e
$ docker inspect -f '{{.State.ExitCode}}' 4dda905e
0
可以看到我们的程序捕获了docker stop
命令发送的SIGTERM信号后以状态码0退出,这次是我们期望的结果
通过上例可得知,在编写Dockerfile构建image时,需要我们确认容器内的进程可以接受到我们发送的信号。在Dockerfile中使用exec格式的CMD或ENTRYPOINT指令是很重要的。
总结
终止一个Docker容器是很容易的,但是如果你真的想要有序地优雅地结束你的应用程序,那就需要做更多的工作。现在,您应该了解如何向容器发送信号,如何在自定义应用程序中处理这些信号,以及如何确保应用程序能够接收到这些信号。