最佳实践|在新版应用引擎中优雅停止应用实例的两种方法
一个Docker应用要优雅的退出,需要系统在停止这个应用前给它发送SIGTERM信号,应用收到信号后进行必要的清理工作,比如停止接收外部请求,处理未完成的工作、记录日志等等操作,然后再退出。
下面我们提供了可以在新版小米应用引擎中使用的两种方法,包括:在程序中接收并处理SIGTERM信号和指定容器退出设置时运行的命令。
在介绍这两个方法之前,如果没有特别的清理善后工作,可以采用下面这个最简单的方法。
停止容器最简单的方法
在这里,我们在强制停止容器前,让容器sleep 10秒钟的时间,同时系统不再分派新的请求到这个容器,容器可以在这段时间内完成正在处理的请求,到达10秒后,容器会强制被删除。sleep的时间,可以根据实际情况进行设置。
优雅退出应用的两种方法
一些相关的基础知识,比如Linux系统信号处理,Docker对信号的支持等,可以点击本文最下方“阅读原文”查看《应用优雅停止最佳实践》快速了解。
下面我们介绍这两个方法,涵盖了典型的使用场景,我们为每个方法提供了详细的测试及复现步骤。
Docker停止容器的过程和Linux系统处理SIGTERM信号过程类似,首先Docker Daemon会向容器中PID为1的主进程发送SIGTERM信号,等待容器中的应用程序终止执行,如果等待超时,会继续发送一个SIGKILL信号强行杀掉进程。容器中的应用程序,可以选择忽略和不处理SIGTERM信号,等到超时时间,程序就会被系统强行杀掉,因为SIGKILL信号不会发送给应用程序,后者没有机会进行处理。
提示:
Docker 1.9以后支持自定义stop-signal,在定义Dockerfile文件时使用命令STOPSIGNAL <signal>来指定。
下面是一个简单接受和处理信号的Go程序。程序在启动过后,会一直阻塞并监听系统信号,直到监测到对应的系统信号后,输出到控制台并退出执行。
// main.go
package main
import (
"fmt"
"os"
"os/signal"
"syscall"
)
func main() {
fmt.Println("Program started…")
ch := make(chan os.signal, 1) // notify signal SIGTERM(15)
signal.Notify(ch, syscall.SIGTERM) // notify signal SIGINT(2)
signal.Notify(ch, syscall.SIGINT)
s := <-ch
switch {
case s == syscall.SIGINT:
fmt.Println("SIGINT received!") //Do something…
case s == syscall.SIGTERM:
fmt.Println("SIGTERM received!") //Do something…
}
fmt.Println("Exiting…")
}
接下来使用交叉编译的方式来编译程序,让程序可以在Linux下运行:
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o graceful
编译好之后。我们做测试:
(1) 测试接收SIGTNT信号。在前台启动进程,然后输入Ctrl + C发送SIGINT(2)信号。
lynzabo@ubuntu ~/r/g/s/edit> ./graceful Program started… ^CSIGINT received! Exiting… lynzabo@ubuntu ~/r/g/s/edit>
(2)测试接收SIGTERM信号
lynzabo@ubuntu ~/r/g/s/edit> ./graceful & Program started… lynzabo@ubuntu ~/r/g/s/edit> ps -ef | grep graceful lynzabo 21223 21082 0 15:57 pts/21 00:00:00 ./graceful lynzabo 21287 21082 0 15:57 pts/21 00:00:00 grep --color=auto graceful lynzabo@ubuntu ~/r/g/s/edit> kill 21223 SIGTERM received! Exiting… “./graceful &” has ended lynzabo@ubuntu ~/r/g/s/edit>
(3) 将上面程序打包到容器中运行
Dockerfile
FROM alpine:latest
LABEL maintainer "opl-xws@xiaomi.com"
ADD graceful /graceful
CMD ["/graceful"]
处理SIGTERM信号时常见的一个坑
在Dockerfle中可以使用CMD、ENTRYPOINT命令来定义容器启动命令,关于这两个命令的区别大家可以参考Dockerfile官方文档,我们这里讲一下在使用时要注意的问题。
这两个命令都支持下面几种格式:
shell 格式:CMD <命令>
exec 格式:CMD [“可执行文件”, “参数1”, “参数2”…]
参数列表格式:CMD [“参数1”, “参数2”…]。在指定了 ENTRYPOINT 指令后,用 CMD 指定具体的参数。
一般推荐使用exec格式,这类格式在解析时会被解析为 JSON 数组,因此一定要使用双引号 "
,而不要使用单引号'
。
如果使用 shell 格式的话,实际的命令会被包装为sh -c的参数的形式进行执行。比如:
CMD echo $HOME
在实际执行中,会将其变更为:
CMD [ "sh", "-c", "echo $HOME" ]
因此容器的主进程是sh,当给容器发送信号,接收信号的是sh进程,sh进程收到信号后会直接退出,自然就会令容器退出。我们的程序永远收不到信号。
(4)启动容器
lynzabo@ubuntu ~/r/g/s/edit> docker run -d --name graceful cnbj6-repo.cloud.mi.com/k8s/graceful-golang-case:1.0.0 08d871007b58e55e9552cff23960c80faf51bf8637014a745dec060b80ac9a6f lynzabo@ubuntu ~/r/g/s/edit> docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 08d871007b58 cnbj6-repo.cloud.mi.com/k8s/graceful-golang-case:1.0.0 "/graceful" 10 seconds ago Up 9 seconds graceful lynzabo@ubuntu ~/r/g/s/edit>
(5)查看容器输出,能看到程序已经正常启动
lynzabo@ubuntu ~/r/g/s/edit> docker logs graceful Program started… lynzabo@ubuntu ~/r/g/s/edit>
(6)接着我们要使用docker stop看程序能否响应SIGTERM信号
docker stop默认在发出SIGTERM信号后的10秒钟,再发送SIGKILL信号强制停掉容器内所有进程,删除容器,假如我的程序处理很复杂,10秒内干不完清理工作,可以在执行docker stop时指定一个更长的等待时间,比如30秒:
lynzabo@ubuntu ~/r/g/s/edit> docker stop --time=30 graceful graceful lynzabo@ubuntu ~/r/g/s/edit> docker logs graceful Program started… SIGTERM received! Exiting… lynzabo@ubuntu ~/r/g/s/edit>
查看上面日志,我们可以看到,我们程序确实可以对Docker发来的SIGTERM信号进行处理。
那么这个Docker应用如何部署到新版应用引擎上,让新版应用引擎支持它的优雅退出的?
创建新应用后,修改应用配置,在“容器退出设置”中指定等待时间时间为30秒。
应用启动成功
查看应用启动日志,并保持一直在查看日志。
Program started
在界面上点击删除应用,可以看到日志中已经由新版应用引擎发送给应用的SIGTERM信号的日志。
Program started…
SIGTERM received!
Exiting
上面的步骤介绍了如何通过应用处理SIGTERM信号来优雅退出的办法,接下来看一下怎么通过指定容器退出设置时运行的命令来优雅停止应用。
有时候我们可以在应用停止前,通过执行一条命令或者发送一个HTTP请求来进行清理工作,例如:
对于Spring Boot应用,Spring Boot Actuator提供了应用优雅停止办法,当要停止应用时,可以向应用发送一个POST方法的Shutdown HTTP请求(注意:如果使用HTTP方式,切记不要把HTTP地址暴露到了公网)。
对于Nginx服务,当要停止服务时,可以执行命令Kill -QUIT <Nginx主进程>,来停止服务。
下面我们以Nginx服务来讲解如何指定容器退出设置时运行的命令来停止服务。
先回顾一下有关Nginx的基础知识:
Nginx是一个多进程服务,Master进程和一堆Worker进程,Master进程只负责校验配置文件语法,创建Worker进程,真正的执行、接收客户请求、处理配置文件中指令都是由Worker进程来完成的。Master进程与Worker进程之间主要是通过Linux信号来交互。Nginx提供了大量的命令和处理信号来实现对配置文件的语法检查,服务优雅停止,进程平滑重启、升级等功能,我们这里仅简单介绍优雅停止Nginx时触发的信号的执行过程和执行原理。
Nginx 的停止方法有很多,一般通过发送系统信号给Nginx的Master进程的方式来停止 Nginx。
(1)优雅停止 Nginx
Nginx用SIGQUIT(3)信号来优雅停止服务。
[root@localhost ~]# nginx -s quit [root@localhost ~]# kill -QUIT <Nginx主进程号> [root@localhost ~]# kill -QUIT /usr/local/nginx/logs/nginx.pid
Master进程接到SIGQUIT信号时,将此信号转发给所有工作进程。工作进程随后关闭监听端口以便不再接收新的连接请求,并闭空闲连接,等待活跃连接全部正常结束后,调用 ngx_worker_process_exit 退出。而 Master 进程在所有工作进程都退出后,调用 ngx_master_process_exit 函数退出;
(2)快速停止 Nginx
[root@localhost ~]# Nginx -s stop [root@localhost ~]# kill -TERM <Nginx主进程号> [root@localhost ~]# kill -INT <Nginx主进程号>
Master进程接收到SIGTERM或者SIGINT信号时,将信号转发给工作进程,工作进程直接调用ngx_worker_process_exit 函数退出。Master进程在所有工作进程都退出后,调用 ngx_master_process_exit 函数退出。另外,如果工作进程未能正常退出,Master进程会等待1秒后,发送SIGKILL信号强制终止工作进程。
(3)强制停止所有 Nginx 进程
[root@localhost ~]# nginx -s stop [root@localhost ~]# pkill -9 nginx
直接给所有的Nginx进程发送SIGKILL信号。
Nginx服务使用新版应用引擎容器退出设置功能的流程如下:
(1)运行Docker Hub官方提供的Nginx镜像
为了查看Nginx能输出信号日志,我们设置Nginx日志级别到Notice。修改nginx.conf文件,将error_log属性值由error改成Notice。
error_log /var/log/nginx/error.log notice;
pid /var/run/nginx.pid;
(2)然后在本地启动运行容器
root@vm10-1-1-28:~/nginx# docker run -v $(pwd)/nginx.conf:/etc/nginx/nginx.conf:ro nginx 2018/01/26 04:53:51 [notice] 1#1: using the "epoll" event method 2018/01/26 04:53:51 [notice] 1#1: nginx/1.13.8 2018/01/26 04:53:51 [notice] 1#1: built by gcc 6.3.0 20170516 (Debian 6.3.0-18) 2018/01/26 04:53:51 [notice] 1#1: OS: Linux 4.4.0-62-generic 2018/01/26 04:53:51 [notice] 1#1: getrlimit(RLIMIT_NOFILE): 1048576:1048576 2018/01/26 04:53:51 [notice] 1#1: start worker processes 2018/01/26 04:53:51 [notice] 1#1: start worker process 5
(3)新启动一个终端,为容器发送QUIT信号,优雅停止Nginx
root@vm10-1-1-28:~# docker exec -ti bc0a0272448a nginx -s quit root@vm10-1-1-28:~# 或者 root@vm10-1-1-28:~# docker kill --signal=SIGQUIT bc0a0272448a bc0a0272448a root@vm10-1-1-28:~#
再次查看容器日志
2018/01/26 04:55:04 [notice] 1#1: signal 3 (SIGQUIT) received, shutting down 2018/01/26 04:55:04 [notice] 5#5: gracefully shutting down 2018/01/26 04:55:04 [notice] 5#5: exiting 2018/01/26 04:55:04 [notice] 5#5: exit 2018/01/26 04:55:04 [notice] 1#1: signal 17 (SIGCHLD) received from 5 2018/01/26 04:55:04 [notice] 1#1: worker process 5 exited with code 0 2018/01/26 04:55:04 [notice] 1#1: exit root@vm10-1-1-28:~/nginx# root@vm10-1-1-28:~/nginx#
可以看到,Nginx收到SIGQUIT信号后,做优雅的停止服务操作。
(4)使用新版应用引擎提供的容器退出设置功能来控制应用的优雅停止
创建新应用,填写Nginx应用信息,保存后,修改应用的容器退出设置,指定等待时间为30秒。
选择"退出前执行"类型为"Container Command"类型,填写为["nginx","-s","quit"]。
点击保存。
这样,当应用的某些实例被停止的时候,新版应用引擎会先调用这里配置的命令来停止服务。
总结:
我们在这篇文章分享了在新版应用引擎中优雅停止应用实例的两种方法,大家可以根据自己的实际场景来使用,让自己的应用和服务更加健壮。
如果你对Linux信号,Docker对信号的支持感兴趣,以及如何使用应用引擎发送HTTP GET、执行命令行优雅停止应用实例感兴趣,可以戳左下方“阅读原文”。