%99的人都不知道的Docker技巧:优雅的终止容器

在Docker大行其道的今天,我们能够非常方便的使用容器打包我们的应用程序,并且将它在我们的服务器上部署并运行起来。但是,谈论到如何停掉运行中的docker容器并正确的终止其中的程序,这就成为一个非常值得讨论的话题了。

事实上,在我们日常的项目当中,这是我们经常需要面对和处理的问题:

  • 场景A:假如我们打包在容器中的程序,提供HTTP方式的服务,负责处理各种HTTP requests并返回结果,我们必然希望在容器被停掉的时候,能够让程序有时间把已经在处理中的请求继续处理完毕,并返回结果给客户端。
  • 场景B:又比如我们打包在容器中的程序,负责写入数据到某个数据文件中,我们希望程序能够在容器被停掉的时候,有时间把内存中缓存的数据持久化到存储设备中,以防数据丢失。
  • 场景C:再比如现在流行的微服务架构中,一般会有服务发现的机制,也即每一个微服务在启动之后,都会主动把自己的地址信息注册到服务发现模块当中,让其他的服务可以知道自己的存在。而在容器被停掉的时候,微服务需要即时从服务发现模块中注销自己,以防止从API Gateway而来的请求被错误的路由到了已经被停止掉的微服务。

如上的各种场景中,都要求打包在容器中的应用程序能够被优雅的终止(也即gracefully shutdown),这种gracefully shutdown的方式,允许程序在容器被停止的时候,有一定时间做一些后续处理操作,这也是我们需要进一步探讨的话题。

docker stop 与 docker kill 的区别

Docker本身提供了两种终止容器运行的方式,即docker stop与docker kill。

1. docker stop

先来说说docker stop吧,当我们用docker stop命令来停掉容器的时候,docker默认会允许容器中的应用程序有10秒的时间用以终止运行。所以我们查看docker stop命令帮助的时候,会有如下的提示:

→docker stop --help

Usage:docker stop [OPTIONS] CONTAINER [CONTAINER...]

Stop one or more running containers

Options:
      --help       Print usage
  -t, --time int   Seconds to wait for stop before killing it (default 10)

在docker stop命令执行的时候,会先向容器中PID为1的进程发送系统信号SIGTERM,然后等待容器中的应用程序终止执行,如果等待时间达到设定的超时时间,或者默认的10秒,会继续发送SIGKILL的系统信号强行kill掉进程。在容器中的应用程序,可以选择忽略和不处理SIGTERM信号,不过一旦达到超时时间,程序就会被系统强行kill掉,因为SIGKILL信号是直接发往系统内核的,应用程序没有机会去处理它。在使用docker stop命令的时候,我们唯一能控制的是超时时间,比如设置为20秒超时:

docker stop --time=20 container_name

2. docker kill

接着我们来看看docker kill命令,默认情况下,docker kill命令不会给容器中的应用程序有任何gracefully shutdown的机会。它会直接发出SIGKILL的系统信号,以强行终止容器中程序的运行。通过查看docker kill命令的帮助,我们可以看到,除了默认发送SIGKILL信号外,还允许我们发送一些自定义的系统信号:

→docker kill --help

Usage: docker kill [OPTIONS] CONTAINER [CONTAINER...]

Kill one or more running containers

Options:
      --help            Print usage
  -s, --signal string   Signal to send to the container (default "KILL")

比如,如果我们想向docker中的程序发送SIGINT信号,我们可以这样来实现:

docker kill --signal=SIGINT container_name

与docker stop命令不一样的地方在于,docker kill没有任何的超时时间设置,它会直接发送SIGKILL信号,以及用户通过signal参数指定的其他信号。

其实不难看出,docker stop命令,更类似于Linux系统中的kill命令,二者都是发送系统信号SIGTERM。而docker kill命令,更像是Linux系统中的kill -9或者是kill -SIGKILL命令,用来发送SIGKILL信号,强行终止进程。

在程序中接收并处理信号

了解了docker stopdocker kill的区别,我们能够知道,docker kill适合用来强行终止程序并实现快速停止容器。而如果希望程序能够gracefully shutdown的话,docker stop才是不二之选。这样,我们可以让程序在接收到SIGTERM信号后,有一定的时间处理、保存程序执行现场,优雅的退出程序。

接下来我们可以写一个简单的Go程序来实现信号的接收与处理,程序在启动过后,会一直阻塞并监听系统信号,直到监测到对应的系统信号后,输出控制台并退出执行。

// main.go

package main

import (
  "fmt"
  "os"
  "os/signal"
  "syscall"
)

func main() {
  fmt.Println("Program started...")
  ch := make(chan os.Signal, 1)
  signal.Notify(ch, syscall.SIGTERM)
  s := <-ch

  if s == syscall.SIGTERM {
    fmt.Println("SIGTERM received!")
    //Do something...
  }

  fmt.Println("Exiting...")
}

接下来使用交叉编译的方式来编译程序,让程序可以在Linux下运行:

CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o graceful

编译好之后,我们还需要打包程序到容器中运行。于是,我们还得有个Dockerfile。在这里,我们选择使用体积小又轻盈的alpine镜像作为基础镜像,打包这个Go程序:

from alpine:latest
MAINTAINERT test

ADD graceful/graceful
CMD ["/graceful"]

这里需要避开的一个坑,是Dockerfile中CMD命令的用法。CMD命令有两种方式:

  • CMD /graceful

使用 CMD command param1 param2 这种方式,其实是以shell的方式运行程序。最终程序被执行时,类似于/bin/sh -c的方式运行了我们的程序,这样会导致/bin/sh以PID为1的进程运行,而我们的程序只不过是它fork/execs出来的子进程而已。前面我们提到过docker stop的SIGTERM信号只是发送给容器中PID为1的进程,而这样,我们的程序就没法接收和处理到信号了。

  • CMD [“/graceful”]

使用 CMD [“executable”,”param1”,”param2”] 这种方式启动程序,才是我们想要的,这种方式执行和启动时,我们的程序会被直接启动执行,而不是以shell的方式,这样我们的程序就能以PID=1的方式开始执行了。

话题转回来,我们开始执行容器构建操作,打包程序:

docker build -t registry.xiaozhou.net/graceful:latest .

打包过后的镜像,才6MB左右:

→docker images
REPOSITORYTAG                 IMAGE ID            CREATED             SIZE
registry.xiaozhou.net/graceful                           latest

启动并运行容器:

→docker run -d --namegracefulb2210a85

查看容器运行状态:

→docker ps -a
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
fd18eedafd16        b221                "/graceful"            3 seconds ago       Up 2 secondsgraceful

查看容器输出,能看到程序已经正常启动:

→docker logs graceful
Started...

接着我们要使用docker stop大法,看程序能否响应SIGTERM信号:

→docker stop graceful
graceful

最后,查看容器的日志,检验输出:

→docker logs graceful
Started...
SIGTERM received!
Exiting...

总结

docker kill命令,可以简单粗暴的终止docker容器中运行的程序,但是想要优雅的终止掉的话,我们需要使用docker stop命令,并且在程序中多花一些功夫来处理系统信号,这样能保证程序不被粗暴的终止掉,从而实现gracefully shutdown

本文分享自微信公众号 - magiccodes(xl----0)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-10-21

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏自学测试之道

python3.x使用cxfreeze将.p打包成.exe

之前写了一个使用ffplay批量查看格式为h264的图片,每次抽帧后都要打开pycharm编译器来运行程序,然后才能正常查看图片,或者在其他没有安装python...

6530
来自专栏自学测试之道

DevOps-自动化测试阶段详细解读和技术脚本预言

测试环境布署 1.appium功能自动化框架环境搭建 2.python脚本运行环境配置 3.Jenkins本地安装配置和Zenportal的安装部署 4.JDK...

9920
来自专栏小麦苗的DB宝专栏

【DB笔试面试464】动态SQL是什么?

在PL/SQL开发过程中,使用SQL或PL/SQL可以实现大部分的需求,但是,在某些特殊的情况下,在PL/SQL中使用标准的SQL语句或DML语句不能实现自己的...

8310
来自专栏小麦苗的DB宝专栏

【DB笔试面试447】AUTHID CURRENT_USER的作用是什么?

定义者权限(Difiner Right):定义者权限是程序的默认权限。如果是在用户A下创建的程序,但其他用户只要能执行这个程序,那么这个程序所执行的任务都是以用...

7010
来自专栏架构圈的那些事

Golang横空出世的背景(为什么选择Go)

最近十年来,C/C++在计算领域没有很好得到发展,并没有新的系统编程语言出现。对开发程度和系统效率在很多情况下不能兼得。要么执行效率高,但低效的开发和编译,如C...

9140
来自专栏书山有路勤为径

ROS Topic

6530
来自专栏京程一灯

更轻量级的 V8 引擎[每日前端夜话0xC8]

作者:Mythri Alle, Dan Elphick, and Ross McIlroy

9420
来自专栏直播、短视频那些事儿

利用直播app源码搭建平台的3大基本步骤

直播app源码作为直播平台的脊柱,需要投资者不断的去了解它,熟悉它,而明确它的开发步骤就是其中必不可少的环节之一。那么利用直播app源码搭建直播间需要经历哪几个...

20940
来自专栏移动安全

nodejs模块导出变成浏览器可使用的js(sm4浏览器端js实现过程)

有一些实现是依赖nodejs的能力来完成的,没有浏览器端进行实现。所以需要变成浏览器端的可实现版本。

22540
来自专栏自学测试之道

DevOps -测试内持续集成与持续交付

「持续集成(Continuous Integration)」、「持续交付(Continuous Delivery)」和「持续部署(Continuous De...

13510

扫码关注云+社区

领取腾讯云代金券

年度创作总结 领取年终奖励