如何优雅的关闭Docker容器

简介

非常多的文档都在讨论如何通过独立的容器打包并运行你的应用,但很少有人讨论如何如何正确停止你的容器。实际上这取决于你的应用,如何停止容器对部分应用来说是非常重要的。

比如你应用正在处理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的优雅关闭,响应SIGTERMSIGINT信号

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 stopdocker 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容器是很容易的,但是如果你真的想要有序地优雅地结束你的应用程序,那就需要做更多的工作。现在,您应该了解如何向容器发送信号,如何在自定义应用程序中处理这些信号,以及如何确保应用程序能够接收到这些信号。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,099评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,828评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,540评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,848评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,971评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,132评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,193评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,934评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,376评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,687评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,846评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,537评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,175评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,887评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,134评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,674评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,741评论 2 351

推荐阅读更多精彩内容