我们可能都使用过docker stop命令来停止正在运行的容器,有时候可能会使用docker kill命令强行关闭容器或者把某个信号传递给容器中的进程。这些操作的本质都是通过主机向容器发送信号实现主机与容器中程序的交互。比如说我们可以向容器中的应用发送一个重新加载信号,容器中的应用程序在接到信号后执行相应的处理程序完成重新加载配置文件的任务。我们在这个章节中来看看docker中捕获信号的基本知识。

信号是什么?

信号是进程间通信的一种方式(??这里的描述应该不是很准确)。一个信号就是内核发送给进程的一个消息,告诉进程发生了某个事件。当一个信号被发送给一个进程后,进程会在特定的时机中断当前的执行流,并开始执行信号的处理程序。如果没有为这个信号指定处理程序,就执行默认的处理程序。

进程需要为自己感兴趣的信号注册处理程序,比如为了能让程序优雅地退出(接到退出的请求后能够对资源进行清理)。一般的程序都会处理sigterm信号,而sigkill则会粗暴地结束一个进程,

容器中的信号

docker中的stop和kill都是用来向容器发送信号的。注意,只有容器中的一号进程能够接收到信号,这一点非常关键!
stop命令会首先发送sigterm信号,并等待应用的优雅结束。如果发现应用没有结束,就再发送一个sigkill信号强行结束程序。docker kill命令默认发送的是sigkill信号,当然我们可以通过-s选项指定任何信号。

下面我们通过一个nodejs应用演示信号在容器中的工作过程,创建app.js(虽然我不是很熟悉,但是不妨碍大概的了解):

  1. 'use strict';
  2. var http = require('http');
  3. var server = http.createServer(function (req, res) {
  4. res.writeHead(200, {'Content-Type': 'text/plain'});
  5. res.end('Hello World\n');
  6. }).listen(3000, '0.0.0.0');
  7. console.log('server started');
  8. var signals = {
  9. 'SIGINT': 2,
  10. 'SIGTERM': 15
  11. };
  12. function shutdown(signal, value) {
  13. server.close(function () {
  14. console.log('server stopped by ' + signal);
  15. process.exit(128 + value);
  16. });
  17. }
  18. Object.keys(signals).forEach(function (signal) {
  19. process.on(signal, function () {
  20. shutdown(signal, signals[signal]);
  21. });
  22. });

这是一个http的服务器,监听端口为3000,为sigterm和sigint信号注册了处理程序,接下来我们以不同方式在容器中运行程序,看看它们对信号的处理方式。

应用程序作为容器中的1号进程

创建Dockerfile文件,把上面的应用打包到镜像中:

  1. FROM iojs:onbuild
  2. COPY ./app.js ./app.js
  3. COPY ./package.json ./package.json
  4. EXPOSE 3000
  5. ENTRYPOINT ["node", "app"]

注意,ENTRYPOINT的写法,会让node在容器中以1号进程的身份运行。

接下来创建镜像:

  1. $ docker build --no-cache -t signal-app -f Dockerfile .

然后启动node应用在容器中的进程号为1:
image.png
现在我们让程序退出,执行命令:

  1. $ docker container kill --signal="SIGTERM" my-app

此时应用会以我们期望的方式退出:
image.png

应用程序不是容器中的1号进程

创建一个启动应用程序的脚本文件app1.sh, 内容如下:

  1. #!/usr/bin/env bash
  2. node app

然后创建新的Dockerfile文件,内容如下:

  1. FROM iojs:onbuild
  2. COPY ./app.js ./app.js
  3. COPY ./app1.sh ./app1.sh
  4. COPY ./package.json ./package.json
  5. RUN chmod +x ./app1.sh
  6. EXPOSE 3000
  7. ENTRYPOINT ["./app1.sh"]

接下来创建镜像:

  1. $ docker build --no-cache -t signal-app1 -f Dockerfile .

然后启动容器运行应用程序:

  1. $ docker run -it --rm -p 3000:3000 --name="my-app1" signal-app1

此时node应用在同容器中的进程号不再是1:
image.png
现在给my-app1发送sigterm信号试试,已经无法退出程序了。在这个场景中,应用程序由bash启动,bash作为容器中的1号进程收到了sigterm信号,但是它没有做出任何的响应动作。

我们可以通过:

  1. #docker container stop my-app1
  2. or
  3. #docker container kill --signal="sigkill" my-app1

在脚本中捕获信号

创建另外一个启动应用程序的脚本文件app2.sh, 内容如下:

  1. #!/usr/bin/env bash
  2. set -x
  3. pid=0
  4. # SIGUSR1-handler
  5. my_handler() {
  6. echo "my_handler"
  7. }
  8. # SIGTERM-handler
  9. term_handler() {
  10. if [ $pid -ne 0 ]; then
  11. kill -SIGTERM "$pid"
  12. wait "$pid"
  13. fi
  14. exit 143; # 128 + 15 -- SIGTERM
  15. }
  16. # setup handlers
  17. # on callback, kill the last background process, which is `tail -f /dev/null` and execute the specified handler
  18. trap 'kill ${!}; my_handler' SIGUSR1
  19. trap 'kill ${!}; term_handler' SIGTERM
  20. # run application
  21. node app &
  22. pid="$!"
  23. # wait forever
  24. while true
  25. do
  26. tail -f /dev/null & wait ${!}
  27. done

这个脚本文件在启动应用程序的同时可以捕获发送给它的sigterm和sigusr1,并为它们添加了处理程序。其中,sigterm信号处理程序就是向我们node应用程序发送sigterm信号。

继续创建dockerfile, 内容如下:

  1. FROM iojs:onbuild
  2. COPY ./app.js ./app.js
  3. COPY ./app2.sh ./app2.sh
  4. COPY ./package.json ./package.json
  5. RUN chmod +x ./app2.sh
  6. EXPOSE 3000
  7. ENTRYPOINT ["./app2.sh"]

接下来创建镜像:

  1. $ docker build --no-cache -t signal-app2 -f Dockerfile .

然后启动容器运行应用程序:

  1. $ docker run -it --rm -p 3000:3000 --name="my-app2" signal-app2

此时node应用在容器中进程号也不是1,但是它却可以接收到SIGTERM信号并优雅的退出:
image.png

容器中的1号进程是非常重要的,如果它不能正确的处理相关的信号,那么应用程序退出的方式几乎总是被强制杀死而不是优雅地退出。

参考:在 docker 容器中捕获信号