New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

进程的平滑关闭 #3

Open
inetfuture opened this Issue Mar 10, 2017 · 1 comment

Comments

Projects
None yet
2 participants
@inetfuture
Copy link
Owner

inetfuture commented Mar 10, 2017

引言

所谓平滑关闭(graceful shutdown),即给进程(代码)足够时间,进行善后清理工作并自动退出,对于以下两种常见的进程类型:

  • http 服务进程:需要停止接受新的请求,并将所有正在处理中的请求处理完毕后自动退出。
  • 后台任务进程:取决于业务需要,要么将正在处理中的任务做完再自动退出,要么将正在做的任务标记为失败后自动退出,这样任务可以继续由其它进程重新处理。

平滑关闭可以减少甚至完全避免部署时重启进程导致的客户端请求失败、任务处理失败,提高服务的可用性。

如何实现平滑关闭

进程关闭是通过信号(signal)来通知的,我们的代码需要捕获相应信号,然后做处理。常见的关闭信号有以下这些:

  • SIGINT,INT 是 interrupt 的缩写,在终端按 Ctrl+C 发的就是这个信号。
  • SIGQUIT,在终端按 Ctrl+\ 发的就是这个信号,按标准定义,进程收到这个后需要比 SIGINT 多做一件事情,就是 dump core。
  • SIGHUP,HUP 是 hang up 的缩写,终端退出时会向后台子进程发送这个信号,例如先执行 ping github.com &,然后退出终端,在后台执行的 ping 进程就会收到该信号。
  • SIGTERM,TERM 是 terminate 的缩写,多数进程管理系统默认使用此信号关闭进程,比如 supervisord、docker 等。
  • SIGKILL,强杀,进程代码无法捕获该信号,不会有机会做任何处理。前几个信号进程都可以捕获用来做平滑关闭,但如果代码有 bug,始终无法自动退出怎么办?进程管理系统一般会设一个超时,超过一定时间就使用 SIGKILL 强杀。

上面这些信号都可以通过 kill -s signal_name pid 命令发送给指定进程,比如 kill -s QUIT 777kill 默认发的是 SIGTERM。

那么对于四个常见的关闭信号 SIGINT、SIGQUIT、SIGHUP、SIGTERM,我们应该具体捕获哪几个呢?这里并没有强制的标准,我的建议是对于简单的应用程序,这四个全部捕获,全部做平滑关闭,尽量保证服务的可用性。如果你就是不想做平滑关闭,想立即终止,始终还有 SIGKILL 可以用。

了解了这些常见的关闭信号,接下来就是在代码中捕获并处理,当然具体怎么写各语言是不一样的,比如在 shell 中是这样的:

shutdown() {
  # do some cleanup here
}

trap shutdown SIGTERM

上面代码表示当进程收到 SIGTERM 时执行方法 shutdown,此时就可以实现清理善后工作。

提示:在 shell 中可以用 EXIT 伪信号代表所有的关闭信号。

平滑关闭第三方程序

上面提到,该使用哪个信号做平滑关闭并没有强制的标准,那么使用第三方程序的时候如何决定使用哪个信号来达到平滑关闭效果(如果这个程序支持的话)呢?答案是看这个程序的文档,不要做假设,比如 nginx 是这样的:

screen shot 2017-03-10 at 13 04 57

在此之前,我一直以为大多数程序应该是支持用 SIGTERM 做平滑关闭的,因为多数进程管理系统、kill 命令默认发送的都是 SIGTERM,结果就被 nginx 打脸了,它需要用 SIGQUIT(实际上很多程序用的都是这个信号)。

使用 docker 时的技巧

如上面说的,docker stop 默认发送 SIGTERM,不过可以用 docker kill -s 发送指定信号,注意有别于 kill 命令,docker kill 默认发送的是 SIGKILL。

此外,Dockerfile 和 compose file 分别支持使用 STOPSIGNALstop_signal 来控制执行 docker stop 命令时使用的信号,这两个配置信息都是保存到所创建的 container 上的。

还有,dokcer stop 默认超时是 10s,对于某些需要较长时间停止的进程,可以使用 -t 参数控制,在 compose file 中可用 stop_grace_period 控制。

信号的传递

有时候我们会把进程的启动过程包装在一个 shell 脚本里,比如先准备好日志目录,再启动真正的服务进程。那么这里就会有一个坑,shell 默认是不会传递信号给子进程的。收到关闭信号后,shell 自己会退出,但子进程会被置之不理,然后子进程就会变成所谓的孤儿进程,进而被 init 进程收养。

在 Dockerfile 中使用 CMD myapp 启动服务进程就会有这个问题,因为它执行起来实际是 /bin/sh -c myapp,后果就是服务进程傻傻的等着超时被强杀,根本不知道该退出了。解决方案有很多,最直接的是使用 CMD ['myapp']的写法,所谓的 exec 格式。

还有一些基础镜像,通过引入一个进程管理系统来解决这个问题,比如 https://github.com/phusion/baseimage-docker, 使用了 runit 。使用它的时候有几个注意事项:

  • 它不会响应 SIGQUIT 信号。

  • 写 run 脚本时,依然要谨记 shell 不会传递信号,比如要启动 nginx,应该写成 exec nginx,就是把当前 shell 进程替换成 nginx 进程,其实 Dockerfile 的 exec 格式跟这个是一样的。那为啥还要用这个基础镜像呢?因为它还有一些别的好处,具体请查看它的文档。当然,再结合上面提到的,nginx 平滑关闭需要 SIGQUIT 信号,那么真正正确的写法应该是这样的:

    shutdown() {
      echo 'gracefully shutting down nginx...'
      kill -s QUIT ${pid}
      wait ${pid}
      exit $?
    }
    
    trap shutdown EXIT
    
    /usr/sbin/nginx &
    
    pid=$!
    wait ${pid}

总结

因情况而异,实现平滑关闭是一个比较复杂的事情,总而言之:

  • 对于一个高可用的服务来说,该功能一定要有。
  • 一定要验证传递并处理了正确的信号,中间很多环节可能出错,不要想当然。

参考资料

@packageman

This comment has been minimized.

Copy link

packageman commented Mar 10, 2017

👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment