1. 介绍
Docker的大部分重点是在隔离的容器中打包和运行应用程序的过程。有无数的教程说明了如何在Docker容器中运行应用程序,但是很少有教程讨论如何正确停止容器化的应用程序。这似乎是一个愚蠢的话题-谁在乎您如何停止容器?
嗯,根据您的应用程序,停止应用程序的过程可能非常重要。如果您的应用程序正在处理HTTP请求,则可能需要先完成所有未完成的请求,然后再关闭容器。如果您的应用程序写入文件,则可能要确保在退出容器之前正确刷新数据并关闭文件。
如果您只是启动一个容器并永久运行,事情将会很容易,但是很有可能需要停止并重新启动您的应用程序,以方便升级或迁移到另一个主机。在那些需要停止正在运行的容器的情况下,如果进程可以平稳关闭而不是突然断开用户连接并破坏文件,那将是更好的选择。
因此,让我们来看一些可以优雅地停止Docker容器的操作。
2. 发送信号
您可以使用许多不同的Docker命令来停止正在运行的容器。
2.1 docker stop
当您发出docker stop
命令时,Docker首先会很好地要求停止该过程,如果它在10秒钟内不符合要求,它将强行杀死它。如果您曾经发出docker stop
过命令,并且不得不等待10秒才能返回命令,那么您已经看到了它的作用。
该docker stop
命令首先尝试通过向容器中的根进程(PID 1)发送SIGTERM信号来停止正在运行的容器。如果该进程在超时时间内仍未退出,则将发送SIGKILL信号。
进程可以选择忽略SIGTERM,而SIGKILL则直接进入将终止该进程的内核。该过程甚至根本看不到信号。
当docker stop
您唯一可以控制的是Docker守护程序在发送SIGKILL之前将等待的秒数:
docker stop --time=30 foo
2.2 docker kill
默认情况下,该docker kill
命令不会给容器进程提供正常退出的机会-它只是发出SIGKILL来终止容器。但是,它却可以接受一个--signal
标志,该标志使您可以将SIGKILL之外的其他信号发送到容器进程。
例如,如果要将SIGINT(相当于终端上的Ctrl-C)发送到容器“ foo”,则可以使用以下命令:
docker kill --signal=SIGINT foo
与docker stop
命令不同,kill
它没有任何超时时间。它仅发出一个信号(默认的SIGKILL或您使用--signal
标志指定的任何信号)。
请注意,该docker kill
命令的默认行为不同于kill
其模仿的标准Linux 命令。如果未指定其他参数,则Linux kill
命令将发送SIGTERM(与相似docker stop
)。另一方面,使用docker kill
更像是在做Linux kill -9
或Linux kill -SIGKILL
。
2.3 docker rm -f
停止正在运行的容器的最后一个选择是将--force 或
-f
标志与docker rm
命令结合使用。通常,docker rm
用于删除已经停止的容器,但是使用该-f
标志会使它首先发出SIGKILL。
docker rm --force foo
如果您的目标是清除正在运行的容器的所有痕迹,那么这docker rm -f
是最快的方法。但是,如果要允许容器正常关闭,则应避免使用此选项。
3. 处理信号
虽然操作系统定义了一组信号列表,但是进程对特定信号的响应方式是特定于应用程序的。
例如,如果要启动Nginx服务器的正常关机,则应发送SIGQUIT。默认情况下,所有Docker命令都不会发出SIGQUIT,因此您需要使用以下docker kill
命令:
docker kill --signal=SIGQUIT nginx
收到SIGQUIT时,nginx日志输出如下所示:
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触发正常关闭:
docker kill --signal=SIGWINCH apache
根据Apache文档一个SIGTERM会导致服务器立即退出和终止任何正在进行的请求,所以你可能不希望使用docker stop在
Apache的容器上。
如果您在容器中运行第三方应用程序,则可能需要查看该应用程序的文档,以了解其如何响应不同的信号。简单地运行一个docker stop
可能不会给您想要的结果。
在容器中运行自己的应用程序时,必须确定应用程序将如何解释不同的信号。您将需要确保在应用程序代码中捕获了相关信号,并采取了必要的措施以完全关闭该过程。
如果您知道将应用程序打包在Docker映像中,则可以考虑使用SIGTERM作为正常关闭信号,因为这是docker stop
命令发送的内容。
无论您使用哪种语言,它都有可能支持某种形式的信号处理。我在以下列表中收集了一些语言的相关包/模块/库的链接:
如果您在应用程序中使用Go,请查看tylerb / graceful软件包,该软件包会自动响应SIGINT或SIGTERM信号而正常关闭http.Handler服务器。
4. 接收信号
编写应用程序以响应特定信号而正常关闭是一个不错的第一步,但是您还需要确保应用程序的打包方式使其有机会接收Docker命令发送的信号。如果您不小心启动应用程序,则它可能永远不会收到docker stop
或发送的任何信号docker kill
。
为了演示,让我们创建一个将在Docker容器中运行的简单应用程序:
#!/usr/bin/env bash trap 'exit 0' SIGTERM while true; do :; done
这个琐碎的bash脚本只是进入无限循环,但是如果收到SIGTERM,它将以0状态退出。
我们将使用以下Dockerfile将其打包到Docker映像中:
FROM ubuntu:trusty COPY loop.sh / CMD /loop.sh
这将简单地将loop.sh bash脚本复制到基于Ubuntu的映像中,并将其设置为运行容器的默认命令。
现在,让我们构建此映像,启动一个容器,然后立即停止它。
$ 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
上面的命令花费了大约10秒钟来完成-这通常表明您的容器没有对SIGTERM做出响应,并且必须以SIGKILL强制终止。
我们可以通过查看容器的退出状态来验证这一点。
$ docker inspect -f '{{.State.ExitCode}}' 64d39c3b 137
基于我们在应用程序中设置的处理程序,如果我们的容器收到SIGTERM,则应该看到0退出状态,而不是137。实际上,退出状态大于128通常表示该进程由于以下原因而终止:未处理的信号。137 = 128 + 9-表示该进程由于信号编号9(SIGKILL)而终止。
那么,这里发生了什么?我们的应用程序被编码为捕获SIGTERM并正常退出。我们知道docker stop
将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脚本未在容器内作为PID 1运行。该脚本实际上是作为运行在PID 1 的/ bin / sh进程的子进程运行的
当您使用docker stop
或docker kill
向容器发出信号时,该信号仅发送到以PID 1运行的容器进程。
由于/ bin / sh不会将信号转发给任何子进程,因此我们发送的SIGTERM从未到达我们的脚本。显然,如果我们希望我们的应用程序能够接收来自主机的信号,则需要找到一种将其作为PID 1运行的方法。
为此,我们需要返回到Dockerfile,并查看用于启动脚本的CMD指令。实际上,CMD指令可以采用几种不同的形式。在上面的Dockerfile中,我们使用了如下的shell形式:
CMD command param1 param2
使用shell形式时,指定的命令与/bin/sh -c
shell一起执行。如果您回顾一下我们容器的进程列表,您将看到PID 1处的进程显示命令字符串“ / bin / sh -c /loop.sh”。因此,/ bin / sh作为PID 1运行,然后派生/执行我们的脚本。
幸运的是,Docker还支持CMD指令的exec形式,如下所示:
CMD ["executable","param1","param2"]
请注意,在这种情况下,出现在CMD指令之后的内容被格式化为JSON数组。
当使用CMD指令的exec形式时,该命令将在没有shell的情况下执行。
让我们更改Dockerfile来查看实际效果:
FROM ubuntu:trusty COPY loop.sh / CMD ["/loop.sh"]
重建映像并查看容器中运行的进程:
$ docker build -t loop . [truncated] $ docker run -d loop 4dda905ee902c91d1f56082d1092d6d72ef54b3d4582fe6b453cba90777554e2 $ docker exec 4dda905e ps -ef UID PID PPID C STIME TTY TIME CMD root 1 0 30 16:42 ? 00:00:04 bash /loop.sh root 13 0 0 16:42 ? 00:00:00 ps -ef
现在,我们的脚本以PID 1的身份运行。让我们向容器发送SIGTERM并查看退出状态:
$ docker stop 4dda905e $ docker inspect -f '{{.State.ExitCode}}' 4dda905e 0
这正是我们所期望的结果!我们的脚本收到docker stop
命令发送的SIGTERM,并以0状态干净退出。
最重要的是,您应该审核容器中的进程,以确保它们能够接收要发送的信号。在您的Dockerfile中使用CMD(或ENTRYPOINT)指令的exec形式是一个好的开始。
结论
使用docker kill
命令终止Docker容器非常容易,但是如果您实际上想以有序的方式关闭应用程序,则需要进行更多的工作。现在,您应该了解如何向容器发送信号,如何在自定义应用程序中处理这些信号以及如何确保应用程序甚至可以首先接收到这些信号。
文章原文链接:https://www.ctl.io/developers/blog/post/gracefully-stopping-docker-containers/