docker容器如何优雅的终止详解
前言
在Docker大行其道的今天,我们能够非常方便的使用容器打包我们的应用程序,并且将它在我们的服务器上部署并运行起来。但是,谈论到如何停掉运行中的docker容器并正确的终止其中的程序,这就成为一个非常值得讨论的话题了。
事实上,在我们日常的项目当中,这是我们经常需要面对和处理的问题:
场景A:假如我们打包在容器中的程序,提供HTTP方式的服务,负责处理各种HTTPrequests并返回结果,我们必然希望在容器被停掉的时候,能够让程序有时间把已经在处理中的请求继续处理完毕,并返回结果给客户端。
场景B:又比如我们打包在容器中的程序,负责写入数据到某个数据文件中,我们希望程序能够在容器被停掉的时候,有时间把内存中缓存的数据持久化到存储设备中,以防数据丢失。
场景C:再比如现在流行的微服务架构中,一般会有服务发现的机制,也即每一个微服务在启动之后,都会主动把自己的地址信息注册到服务发现模块当中,让其他的服务可以知道自己的存在。而在容器被停掉的时候,微服务需要即时从服务发现模块中注销自己,以防止从APIGateway而来的请求被错误的路由到了已经被停止掉的微服务。
如上的各种场景中,都要求打包在容器中的应用程序能够被优雅的终止(也即gracefullyshutdown),这种gracefullyshutdown的方式,允许程序在容器被停止的时候,有一定时间做一些后续处理操作,这也是我们需要进一步探讨的话题。
dockerstop与dockerkill的区别
Docker本身提供了两种终止容器运行的方式,即dockerstop与dockerkill。
dockerstop
先来说说dockerstop吧,当我们用dockerstop命令来停掉容器的时候,docker默认会允许容器中的应用程序有10秒的时间用以终止运行。所以我们查看dockerstop命令帮助的时候,会有如下的提示:
→dockerstop--help Usage:dockerstop[OPTIONS]CONTAINER[CONTAINER...] Stoponeormorerunningcontainers Options: --helpPrintusage -t,--timeintSecondstowaitforstopbeforekillingit(default10)
在dockerstop命令执行的时候,会先向容器中PID为1的进程发送系统信号SIGTERM,然后等待容器中的应用程序终止执行,如果等待时间达到设定的超时时间,或者默认的10秒,会继续发送SIGKILL的系统信号强行kill掉进程。在容器中的应用程序,可以选择忽略和不处理SIGTERM信号,不过一旦达到超时时间,程序就会被系统强行kill掉,因为SIGKILL信号是直接发往系统内核的,应用程序没有机会去处理它。在使用dockerstop命令的时候,我们唯一能控制的是超时时间,比如设置为20秒超时:
dockerstop--time=20container_name
dockerkill
接着我们来看看dockerkill命令,默认情况下,dockerkill命令不会给容器中的应用程序有任何gracefullyshutdown的机会。它会直接发出SIGKILL的系统信号,以强行终止容器中程序的运行。通过查看dockerkill命令的帮助,我们可以看到,除了默认发送SIGKILL信号外,还允许我们发送一些自定义的系统信号:
→dockerkill--help Usage:dockerkill[OPTIONS]CONTAINER[CONTAINER...] Killoneormorerunningcontainers Options: --helpPrintusage -s,--signalstringSignaltosendtothecontainer(default"KILL")
比如,如果我们想向docker中的程序发送SIGINT信号,我们可以这样来实现:
dockerkill--signal=SIGINTcontainer_name
与dockerstop命令不一样的地方在于,dockerkill没有任何的超时时间设置,它会直接发送SIGKILL信号,以及用户通过signal参数指定的其他信号。
其实不难看出,dockerstop命令,更类似于Linux系统中的kill命令,二者都是发送系统信号SIGTERM。而dockerkill命令,更像是Linux系统中的kill-9或者是kill-SIGKILL命令,用来发送SIGKILL信号,强行终止进程。
在程序中接收并处理信号
了解了dockerstop与dockerkill的区别,我们能够知道,dockerkill适合用来强行终止程序并实现快速停止容器。而如果希望程序能够gracefullyshutdown的话,dockerstop才是不二之选。这样,我们可以让程序在接收到SIGTERM信号后,有一定的时间处理、保存程序执行现场,优雅的退出程序。
接下来我们可以写一个简单的Go程序来实现信号的接收与处理,程序在启动过后,会一直阻塞并监听系统信号,直到监测到对应的系统信号后,输出控制台并退出执行。
//main.go packagemain import( "fmt" "os" "os/signal" "syscall" ) funcmain(){ fmt.Println("Programstarted...") ch:=make(chanos.Signal,1) signal.Notify(ch,syscall.SIGTERM) s:=<-ch ifs==syscall.SIGTERM{ fmt.Println("SIGTERMreceived!") //Dosomething... } fmt.Println("Exiting...") }
接下来使用交叉编译的方式来编译程序,让程序可以在Linux下运行:
CGO_ENABLED=0GOOS=linuxGOARCH=amd64gobuild-ograceful
编译好之后,我们还需要打包程序到容器中运行。于是,我们还得有个Dockerfile。在这里,我们选择使用体积小又轻盈的alpine镜像作为基础镜像,打包这个Go程序:
fromalpine:latest MAINTAINERTimothy ADDgraceful/graceful CMD["/graceful"]
这里需要避开的一个坑,是Dockerfile中CMD命令的用法。
CMD命令有两种方式:
CMD/graceful
使用CMDcommandparam1param2这种方式,其实是以shell的方式运行程序。最终程序被执行时,类似于/bin/sh-c的方式运行了我们的程序,这样会导致/bin/sh以PID为1的进程运行,而我们的程序只不过是它fork/execs出来的子进程而已。前面我们提到过dockerstop的SIGTERM信号只是发送给容器中PID为1的进程,而这样,我们的程序就没法接收和处理到信号了。
CMD[“/graceful”]
使用CMD[“executable”,”param1”,”param2”]这种方式启动程序,才是我们想要的,这种方式执行和启动时,我们的程序会被直接启动执行,而不是以shell的方式,这样我们的程序就能以PID=1的方式开始执行了。
话题转回来,我们开始执行容器构建操作,打包程序:
dockerbuild-tregistry.xiaozhou.net/graceful:latest.
打包过后的镜像,才6MB左右:
λTimothy[workspace/src/graceful]→dockerimages REPOSITORYTAGIMAGEIDCREATEDSIZE registry.xiaozhou.net/gracefullatestb2210a85ca5520hoursago6.484MB
启动并运行容器:
λTimothy[workspace/src/graceful]→dockerrun-d--namegracefulb2210a85
查看容器运行状态:
λTimothy[workspace/src/graceful]→dockerps-a CONTAINERIDIMAGECOMMANDCREATEDSTATUSPORTSNAMES fd18eedafd16b221"/graceful"3secondsagoUp2secondsgraceful
查看容器输出,能看到程序已经正常启动:
λTimothy[workspace/src/graceful]→dockerlogsgraceful Started...
接着我们要使用dockerstop大法,看程序能否响应SIGTERM信号:
λTimothy[workspace/src/graceful]→dockerstopgraceful graceful
最后,查看容器的日志,检验输出:
λTimothy[workspace/src/graceful]→dockerlogsgraceful Started... SIGTERMreceived! Exiting...
总结
以上就是这篇文章的全部内容了,用dockerkill命令,可以简单粗暴的终止docker容器中运行的程序,但是想要优雅的终止掉的话,我们需要使用dockerstop命令,并且在程序中多花一些功夫来处理系统信号,这样能保证程序不被粗暴的终止掉,从而实现gracefullyshutdown。希望本文的内容对大家的学习或者工作能有所帮助,如果有疑问大家可以留言交流。