最近又看了一遍Docker官方的DockerReference文档,发现还有很多细节需要深挖。对于写Dockerfile,大部分时候,只要跟着葫芦娃,不会有什么大问题,但是如果深入理解,会更有趣。说到如何优雅地关闭容器,就不得不提到信号(Signal)的概念以及Dockerfile中的ENTRYPOINT和CMD指令。在讲优雅关机之前,我们先来了解一下Linux中信号的基本概念。1信号信号是在事件发生时通知进程的机制,有时称为软件中断。有不同类型的信号。Linux编号1~31用于标准信号。通过kill-l可以得到信号名称:#kill-l1)SIGHUP2)SIGINT3)SIGQUIT4)SIGILL5)SIGTRAP6)SIGABRT7)SIGBUS8)SIGFPE9)SIGKILL10)SIGUSR111)SIGSEGV12)SIGUSR213)SIGPIPE14)SIGALRM15)SIGTERM...还有实际列出的信号超过31个,有些是其他名称的同义词,有些已定义但未使用。下面是一些常用的信号:1)SIGHUP当终端断开连接(挂机)时,这个信号会被发送到终端控制进程。SIGHUP信号也可用于守护进程(例如,init等)。许多守护进程在收到SIGHUP信号时会重新初始化并重新读取配置文件。2)SIGINT当用户键入终端中断字符(通常是Control-C)时,终端驱动程序会将此信号发送到前台进程组。此信号的默认行为是终止进程。3)SIGQUIT当用户在键盘上敲出退出字符(通常是Control-\)时,这个信号会被发送到前台进程组。默认情况下,此信号会终止进程并生成用于调试的核心转储文件。当进程陷入无限循环或不再响应时,SIGQUIT信号是合适的。9)SIGKILL该信号是“surekill”信号,处理器程序无法阻塞、忽略或捕获它,因此是“surekill”,可以随时终止程序。15)SIGTERM这是用来终止进程的标准信号,也是kill、killall、pkill命令默认发送的信号。一个设计良好的应用程序应该为SIGTERM信号设置一个处理程序,以便它可以抢先清理临时文件并释放其他资源以毫发无损地退出。因此,您应该始终首先尝试使用SIGTERM信号杀死进程,并在最后使用SIGKILL来处理不响应SIGTERM信号的失控进程。20)SIGTSTP这是作业控制的停止信号。当用户在键盘上输入暂停字符(通常是Control-Z)时,会向前台进程组发出信号,使其停止运行。值得注意的是,Control-D不会发起信号,它表示EOF(End-Of-File),关闭标准输入(stdin)管道(例如可以通过Control-D退出当前shell)。如果程序不读取当前输入,则不受Control-D的影响。程序可以捕捉到信号,然后执行相应的功能:以上知识大部分来自《Linux/UNIX 系统编程手册》。如果想了解更多,可以查看本书的第20、21、22章。2ENTRYPOINT,CMD有人会问,说了半天,这个信号跟容器优雅关闭有半毛钱关系?换句话说,这与钱无关,但与如何优雅地关闭容器有很大关系。接下来说一下Dockerfile中的ENTRYPOINT和CMD指令。它们的主要作用是指定容器启动时要执行的程序。CMD有三种格式:CMD["executable","param1","param2"](exec格式,推荐这种格式)CMD["param1","param2"](作为ENTRYPOINT命令参数)CMD命令param1param2(Shell格式,默认/bin/sh-c)ENTRYPOINT有两种格式:ENTRYPOINT["executable","param1","param2"](exec格式,建议先使用这种格式)ENTRYPOINTcommandparam1param2(shell格式)其中,无论你在你的Dockerfile中使用哪个命令,这两个命令都建议使用exec格式,而不是shell格式。原因是因为使用shell格式后,程序会以/bin/sh-c这个子命令启动,不会给shell格式的程序传递任何信号。这也导致docker在停止容器时,以这种格式运行的程序无法捕捉到发送过来的信号,因此无法正常关闭。?~dockerstop--helpUsage:dockerstop[OPTIONS]CONTAINER[CONTAINER...]StoponeormorerunningcontainersOptions:--helpPrintusage-t,--timeintSecondstowaitforstopbeforekillingit(default10)dockerstop停止容器时,默认发送SIGTERM信号,默认10s如果容器没有停止,SIGKILL会强行停止容器。等待时间可以通过-t选项设置。?~dockerkill--helpUsage:dockerkill[OPTIONS]CONTAINER[CONTAINER...]KilloneormorerunningcontainersOptions:--helpPrintusage-s,--signalstringSignaltosendtothecontainer(default"KILL")dockerkill的-s选项还可以指定发送到容器的信号容器。那么,说了这么多,只要在Dockerfile中以exec格式执行容器启动命令就万事大吉了?当然,事情并没有那么简单。下面我们通过一个例子来看看具体的效果。3示例在Go中编写一个简单的信号处理程序:?~catsignals.gopackagemainimport("fmt""os""os/signal""syscall")funcmain(){sigs:=make(chanos.Signal,1)done:=make(chanbool,1)signal.Notify(sigs,syscall.SIGINT,syscall.SIGTERM)gofunc(){sig:=<-sigsfmt.Println()fmt.Println(sig)done<-true}()fmt。Println("awaitingsignal")<-donefmt.Println("exiting")}3.1例子1?~GOOS=linuxGOARCH=amd64gobuildsignals.go?~lsDockerfilesignalssignals.go?~catDockerfileFROMbusyboxCOPYsignals/signalsCMD["/signals"]#exec格式执行?~dockerbuild-tsignals。通过tmux打开两个面板,一个运行容器,一个执行dockerstop:?~dockerrun-it--rm--namesignalssignalsawaitingsignalterminatedexiting?~timedockerstopsignalssignalsdockerstopsignals0.01suser0.02ssystem4%cpu0.732total?~可以发现容器stops之前,程序接收到信号并输出??相应的信息,停止总耗时为0.732s,达到了优雅的效果。修改Dockerfile中的CMD执行格式,执行同样的操作:?~catDockerfileFROMbusyboxCOPYsignals/signalsCMD/signals#shell格式执行?~dockerbuild-tsignals.?~dockerrun-it--rm--namesignalssignalsawaitingsignal?~?~timedockerstopsignalssignalsdockerstopsignals0.01suser0%cpusystems0.01suser0%cpu1s.719total通过shell格式可以发现,程序在容器停止前没有收到任何信号,停止时间为10.719s,说明容器被强行停止了。结论很明显,为了优雅地退出容器,我们应该使用exec格式。3.2示例2通过示例1,我们都是执行Dockerfile中exec格式的程序,那如果执行的程序本身也是一个shell脚本呢??~lsDockerfilesignalssignals.gostart.sh?~catDockerfileFROMbusyboxCOPYsignals/signalsCOPYstart.sh/start.sh#引入shell脚本启动CMD["/start.sh"]?~catstart.sh#!/bin/sh/signals?~测试是还是参考了例1中的方法:?~dockerrun-it--rm--namesignalssignalsawaitingsignal?~?~timedockerstopsignalssignalsdockerstopsignals0.01suser0.02ssystem0%cpu10.765total?~可以发现即使Dockerfile中的CMD命令使用了exec格式,程序还是没有收到信号,最后被迫关闭。因为在shell脚本中执行,信号还是没有下达。我们需要对shell脚本做一些改动:?~catstart.sh#!/bin/shexec/signals#添加exec执行?~dockerbuild-tsignals.?~dockerrun-it--rm--namesignalssignalsawaitingsignalterminatedexiting?~timedockerstopsignalssignalsdockerstopsignals0.02suser0.02ssystem4%cpu0.744total?~可以看到添加exec命令后,程序可以正常接收到信号并退出。当然,如果你的Dockerfile中的CMD是以shell格式运行的,即使在启动脚本中加上exec也是无效的。此外,如果您的程序本身不能对信号进行某些处理,那么就没有所谓的正常关闭。
