vlambda博客
学习文章列表

如何优雅关闭 Docker 中的 Nginx

Signal in Nginx

nginx 默认注册了自己的系统信号处理程序,nginxmaster 进程会根据不同的接收信号量执行不同的操作。Linux 中可以通过 nginx -s <param> 选项或者 kill 命令来向 nginx master 发送系统信号,下面列出了不同 -s <param> 命令和 kill 命令与发送信号对应关系:

-s 参数 kill 参数 信号 含义
stop 15 SIGTERM 快速关闭
quit 3 SIGQUIT 优雅关闭
reload 1 SIGHUP 重新加载配置文件
reopen 10 SIGUSR1 重新打开日志文件

对于 nginx 而言,优雅的关闭进程的信号是 SIGQUIT,也就是 -s quit 命令。nginx master 在接收到 SIGQUIT 信号后,nginx worker 进程将关闭监听端口,不会再接收新的请求,并且在当前处理的请求完成后退出,nginx master 会在所有的 nginx worker 进程退出后执行退出。

SIGTERM 信号则会调用 ngx_master_process_exit 立刻关闭 nginx worker 进程。若 1snginx worker 未退出,则 nginx master 就会发送SIGKILL 信号强制关闭 nginx worker 进程。

Signal in Docker

docker 中,一般会通过 docker stop 或者 docker kill 命令来停止容器。

  • docker stop

    默认 docker stop 发送的是 SIGTERM 信号。容器中 PID1 的进程在接收到 SIGTERM 信号后,会开始执行停止进程的操作,同时 docker 默认会给出 10s 时间来等待进程中止,10s 时间结束后,如果程序还没停止,docker 就会发送 SIGKILL 信号来强行停止进程。

    这里还涉及一个问题,由于 docker stop 命令只会向 dockerPID1 的进程发送信号,如果 PID1 的进程并没有注册处理 Linux系统信号的程序,那么就会忽略 SIGTERM 信号,从而在 10s 后被强行 Kill。比如 docker 启动时启动的是一个 Shell 脚本,真正的进程被放进了 Shell 脚本中运行,那么此时 PID1 的进程就是这个 Shell 脚本,而不是脚本中的那个进程。对于这种情况,可以在启动 Shell 脚本的命令前面加上 exec 命令,exec 命令会让真正的进程继承 Shell 脚本进程的 PID

    此外,还可以在 docker 中增加专为容器而生的 tinit 系统,那么 docker 启动后,tinit 会来管理 docker 中的进程,以及处理 docker stop 发送来的系统信号。

      docker run --init
  • docker kill

    docker kill 命令会直接向 docker 中的进程发出 SIGKILL 系统信号,没有10s的等待时间,立刻强行停止 docker 中的程序。

    docker kill 命令还支持参数 --signal 指定发送自定义的信号,比如让 Apache 优雅的关闭进程:

      docker kill --signal=SIGWINCH apache
  • docker rm -f

    docker rm 一般用来删除一个已经停止的容器,如果加上 -f 参数对一个处于运行中的容器执行 docker rm,就会立刻发送一个 SIGKILL 信号来停止进程并删除容器。

所以,默认 docker stop 会发送一个 SIGTERM 信号,这就像在 Linux 系统中发送一个 kill 命令,能做的就是通过 --time 参数延缓发送 SIGKILL 的信号时间。而 docker kill 则没有 --time 参数,它会立刻对 docker 中的 PID 1 进程发送 SIGKILL 的信号,不过 docker kill 可以通过 --signal 参数来改变发送的系统信号。

STOPSIGNAL

docker stop 命令并没有 docker kill 的  --signal 参数,那么,对于 nginx 容器来说,docker stop 命令其实就是向 nginx 发出了 -s stop 的命令,这对于 nginx 来说并不是最优雅的停止进程的方式,如何能向docker 发送 -s quit 命令,也就是 QUIT 系统信号呢?

docker1.9 之后在 dockerfile 中增加了 STOPSIGNAL 这个关键字,可以自定义 docker stop 发送给容器的信号。

STOPSIGNAL signal

The STOPSIGNAL instruction sets the system call signal that will be sent to the container to exit. This signal can be a valid unsigned number that matches a position in the kernel’s syscall table, for instance 9, or a signal name in the format SIGNAME, for instance SIGKILL.

按照官网的描述,通过 STOPSIGNAL 关键字可以自定义给容器发送的系统调用信号,其中 signal 既可以设置为内核系统调用表中定义的数字,比如 3,也可以是信号名称,比如 SIGQUIT

下面引用来自Github上的一个例子来验证通过 STOPSIGNAL 关键字将 docker stop 的默认 SIGTERM 信号修改为 SIGQUIT

  • dockerfile 中增加 STOPSIGNAL SIGQUIT,修改发送的信号量
# cat Dockerfile

FROM nginx

RUN echo 'server {\n\
    listen 80 default_server;\n\
    location / {\n\
      proxy_pass      http://httpbin.org/delay/10;\n\
    }\n\
}' > /etc/nginx/conf.d/default.conf

STOPSIGNAL SIGQUIT

CMD ["nginx", "-g", "daemon off;"]

在上面 nginx 的配置中,通过 url 中的 delay/10 设置了 10s 的延迟响应
时间。

  • 构建镜像
# docker build -t delay-image .
  • 启动 docker
# docker run --rm --name delay-ctnr --detach --publish 80:80 delay-image
  • 发送一个 HTTP 请求,等待1秒中,执行停止 docker 的命令
# curl -I localhost 2>&1 | egrep 'curl:|HTTP' &
# time docker stop delay-ctnr

HTTP/1.1 200 OK
delay-ctnr
0:08.51

通过返回的信息,我们可以看到从发送 docker stop delay-ctnr 命令到指定 docker 经过了 8.51s。满足 nginx 配置中设置的 10s 延迟的设置。