当一个运行中的容器被终止时,如何能够执行一些预定义的操作,比如在容器彻底退出之前清理环境。这是一种类似于 pre stop 的钩子体验。但 docker 本身无法提供这种能力,本文结合 Linux 内置命令 trap ,实现在容器优雅关闭之前,可以执行自定义的操作。

如何关闭容器

我了解有三种方式可以关闭一个正在运行中的容器,三者都是由 docker 命令行发起的。

  • 第一种是较为优雅的方式 docker stop ContainerID
  • 第二种看起来就比较武断 docker rm -f ContainerID
  • 第三种用的人会少很多 docker kill --signal=KILL ContainerID

docker 的设计者自然不会平白无故的设计三种命令组合来做关闭容器这件事,三种方式都应该在什么场景下被使用呢?

这三种终止容器的方式之间是略有不同的,在讲解这些不同之前,需要提及一些看似和容器不相关的知识点——SIGNAL 。

进程与信号

用户是可以通过发送信号,来和进程通信的。

基本上每一个运维工程师都执行过如下命令来杀死一个进程:

  1. kill -9 PID

这个命令看起来恰如其分,我 "杀死" 了一个进程,但是,为什么是 "-9" ?

9 是信号 SIGKILL 的代号,上述命令实际上是向对应的进程发送了一个信号,一个可以杀死进程的信号。

kill 命令的真正意义,是向进程发送指定的信号,除了SIGKILL(9) 之外,还可以发送其他多种信号:

  1. root@ubuntuserver:~# kill --help
  2. kill: kill [-s sigspec | -n signum | -sigspec] pid | jobspec ... or kill -l [sigspec]
  3. Send a signal to a job.
  4. root@ubuntuserver:~# kill -l
  5. 1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
  6. 6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
  7. 11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
  8. 16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
  9. 21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
  10. 26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
  11. 31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
  12. 38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
  13. 43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
  14. 48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
  15. 53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
  16. 58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
  17. 63) SIGRTMAX-1 64) SIGRTMAX

我无意去详解每一个信号的意义,我的功力还差得远,在这里只拣取和我们主题相关的知识来进行阐述。

有两个信号和我们的主题相关, SIGTERM. SIGKILL

信号名称 代号 可否被捕获或忽略
SIGTERM 15 可以
SIGKILL 9 不可以

SIGTERMkill 命令默认发送的信号。当用户请求终止进程时,会产生SIGTERM信号。SIGTERM信号可以被捕获或无视。这允许该进程在结束前释放掉所占用的资源并保存其状态。

SIGKILL 发送SIGKILL信号到一个进程可以使其立即终止(KILL)。与SIGTERM不同的是,这个信号不能被捕获或忽略,接收过程在接收到这个信号时不能执行任何清理。但有时候 kill -9 并非一定可以杀死进程,释放资源。还是有一些特殊情况:

  • 僵尸进程不能被杀死,因为它们已经死了,正在等待它们的父进程来收获它们。
  • 处于阻塞状态的进程不会死亡,直到它们再次醒来。
  • init 进程是特殊的: init不接收任何它不打算处理的信号,因此它会忽略SIGKILL。这条规则有一个例外,Linux 上的 init 如果被 ptrace 了,那么它是可以接收 SIGKILL 并被杀死的。
  • 处于不可中断的睡眠的进程即使发送了SIGKILL,也有可能不会终止(并释放其资源)。这是少数 Unix 系统必须重新启动才能解决临时软件问题的几种情况之一。

容器与信号

容器的本质,是一组被封装起来的进程。所以通过开头讲到的三种命令行方式关闭一个运行中的容器,其本质也是在通过发送信号的方式与容器中的进程进行交互,使之被 "杀死" 的过程。

  • docker stop

执行 docker stop ContainerID ,会向容器中的主进程先发送一个 SIGTERM 信号,在一段时间的宽限期后,发送 SIGKILL 信号彻底杀死容器。

Docker 手册原文如下:

The main process inside the container will receive SIGTERM, and after a grace period, SIGKILL

  • docker rm -f

执行 docker rm -f ContainerID ,会向容器中的主进程直接发送SIGKILL 信号,在容器杀死之后,也会把容器删除掉。从删除容器这个操作看来,这个命令是用来删除一个已停止的容器,而非用于停止运行中的容器。

  • docker kill

执行 docker kill --signal=KILL ContainerID ,是专门向容器主进程发送各种自定义信号的方式。换言之,它就是面向容器的 kill 命令。当前命令是在向容器主进程发送一个 SIGKILL 信号。

通过比对,docker rm -f ContainerID 这种方式是不应该用于停止运行中容器的。而剩余两种方式之间, docker stop ContainerID 也明显要优雅一些,它既可以保证容器会被最终杀死,也会提供 SIGTERM 供用户后续捕获处理。

接下来终于要进入正题了。

捕获信号并处理

信号 SIGTERM 是一种可以被捕获的信号。当容器主进程捕获到这个信号之后,可以触发事先设计好的逻辑,在彻底退出之前完成预定的任务。比如可以执行环境的清理、数据的保存、关闭其他不受主进程控制的进程等等。在某些场景下,这种需求非常突出。

Linux 提供内置的 trap 命令,负责捕获信号,并确保在进程彻底退出前,执行某些任务。

  1. root@ubuntuserver:~# trap --help
  2. trap: trap [-lp] [[arg] signal_spec ...]
  3. Trap signals and other events.

其基本的使用方式如下:

  1. trap do_some_things SIGSPEC

思路已经清晰了,我们需要在容器的启动脚本中,加入 trap 指令,来完成容器在退出前需要做的所有事情。

以下是一个脚本示例,这个脚本被作为容器的入口(ENTRYPOINT)执行。

  1. #!/bin/bash
  2. function clean_up_term {
  3. rm -rf /data/tmp
  4. echo "clean_up_term in execution"
  5. }
  6. trap clean_up_term SIGTERM
  7. for ((i=1;i<=1000;i++))
  8. do
  9. echo "Wait for $i"
  10. sleep 1
  11. done

容器启动后,从其他终端执行了 docker stop ContainerID 命令,可以观察到以下结果。

  1. guox@MacBook-Pro-For-Guox: /Users/guox/GitHub/test-plugin git:(master)
  2. docker run -ti --name=clean -v $(pwd)/data:/data clean
  3. Wait for 1
  4. Wait for 2
  5. Wait for 3
  6. Wait for 4
  7. Wait for 5
  8. Wait for 6
  9. Wait for 7
  10. Wait for 8
  11. Wait for 9
  12. Wait for 10
  13. Wait for 11
  14. Wait for 12
  15. Wait for 13
  16. clean_up_term in execution
  17. Wait for 14
  18. Wait for 15
  19. Wait for 16
  20. Wait for 17
  21. Wait for 18
  22. Wait for 19
  23. Wait for 20
  24. Wait for 21
  25. Wait for 22
  26. Wait for 23
  27. guox@MacBook-Pro-For-Guox: /Users/guox/GitHub/test-plugin git:(master)

信号 SIGTERM 的确被容器捕获,并进行了相关的清理操作。 docker stop ContainerID 提供了一段宽限期,所以在执行了清理操作后,容器主进程还是继续执行了一会才退出。

利用 trap 在 docker 容器优雅关闭前执行环境清理的更多相关文章

  1. Docker 容器优雅终止方案

    原文链接:Docker 容器优雅终止方案 作为一名系统重启工程师(SRE),你可能经常需要重启容器,毕竟 Kubernetes 的优势就是快速弹性伸缩和故障恢复,遇到问题先重启容器再说,几秒钟即可恢复 ...

  2. 利用 ELK 搭建 Docker 容器化应用日志中心

    利用 ELK 搭建 Docker 容器化应用日志中心 概述 应用一旦容器化以后,需要考虑的就是如何采集位于 Docker 容器中的应用程序的打印日志供运维分析.典型的比如SpringBoot应用的日志 ...

  3. 使用pipework将Docker容器配置到本地网络环境中

    使用pipework将Docker容器配置到本地网络环境中 需求 在使用Docker的过程中,有时候我们会有将Docker容器配置到和主机同一网段的需求.要实现这个需求,我们只要将Docker容器和主 ...

  4. 海纳百川无所不容,Win10环境下使用Docker容器式部署前后端分离项目Django+Vue.js

    原文转载自「刘悦的技术博客」https://v3u.cn/a_id_179 随着现代化产品研发的不断推进,我们会发现,几乎每个产品线都会包含功能各异的服务,而且服务与服务之间存在也会存在着错综复杂的依 ...

  5. 详解利用ELK搭建Docker容器化应用日志中心

    概述 应用一旦容器化以后,需要考虑的就是如何采集位于Docker容器中的应用程序的打印日志供运维分析.典型的比如SpringBoot应用的日志 收集.本文即将阐述如何利用ELK日志中心来收集容器化应用 ...

  6. 利用pipework为docker容器设置固定IP

    今天介绍如何在redhat/centos7系列机器上使用pipework为docker启动的容器指定一个固定ip,我们知道默认情况下,docker会使用 bridge网络模式为每一个启动的容器动态分配 ...

  7. Docker容器优雅重启

    默认情况下,当 Docker 守护进程终止时,它将关闭正在运行的容器.您可以配置守护程序,以便容器在守护程序不可用时保持运行.此功能称为live-restore.live-restore选项有助于减少 ...

  8. 使用pipework将Docker容器桥接到本地网络环境中

    在使用Docker的过程中,有时候我们会有将Docker容器配置到和主机同一网段的需求.要实现这个需求,我们只要将Docker容器和主机的网卡桥接起来,再给Docker容器配上IP就可以了.pipew ...

  9. 利用dotnet-dump分析docker容器内存泄露

    目录 一 运行官方示例 1,Clone代码并编译 2,创建Dockerfile构建镜像 3,启动容器 二 生成dump转储文件 1,制造问题 2,创建dump文件 三 分析dump文件 1,创建一个用 ...

随机推荐

  1. uniapp增加自定义埋点功能

    起因 首先来说,uniapp其实是自带系统埋点统计功能的.基本也算是面面俱到. 但是一些未知原因,貌似数据有所丢失,再加上没有一些重要的定制化功能,以及最重要的数据安全方面的考虑,还是决定接入公司的埋 ...

  2. UOJ #36 -【清华集训2014】玛里苟斯(线性基+暴搜)

    UOJ 题面传送门 看到 \(k\) 次方的期望可以很自然地想到利用低次方和维护高次方和的套路进行处理,不过.由于这里的 \(k\) 达到 \(5\),直接这么处理一来繁琐,二来会爆 long lon ...

  3. Codeforces 739C - Alyona and towers(线段树)

    Codeforces 题目传送门 & 洛谷题目传送门 可能有人会问我为什么为这道 *2500 的 D1C 写题解,我觉得大概是想要在写题解数量上 dd ycx 吧,因为 ycx 到目前为止写了 ...

  4. 斗地主的综合案例实现(Map有序)

    斗地主的综合案例实现(Map有序) 整体思路 代码实现 import java.util.ArrayList; import java.util.Collections; import java.ut ...

  5. mysql-计算排名

    mysql计算排名,获取行号rowno 学生成绩表数据 SELECT * FROM table_score ORDER BY score DESC; 获取某个学生成绩排名并计算该学生和上一名学生成绩差 ...

  6. LATEX公式语法

    see how any formula was written in any question or answer, including this one, right-click on the ex ...

  7. MybatisPlus使用Wrapper实现查询功能

    Wrapper---条件查询器 :使用它可以实现很多复杂的查询 几个案例 环境: 参照博客:MybatisPlus入门程序 1.条件查询 1.1 查询name不为空的用户,并且邮箱不为空的用户,年龄大 ...

  8. CSS3单行文本两端对齐

    CSS3实现单行文本两端对齐 p { height: 24px; text-align: justify; text-last-align: justify; } p::after { display ...

  9. day08 文件属性

    day08 系统目录 今日内容 一.重要目录 1./usr 2./var 3./proc 二.文件的属性 1.文件属性的介绍 2.文件属性的详述 3.企业案例 /usr 安装第三方软件的目录: 1./ ...

  10. day6 基本数据类型及内置方法

    day6 基本数据类型及内置方法 一.10进制转其他进制 1. 十进制转二进制 print(bin(11)) #0b1011 2. 十进制转八进制 print(hex(11)) #0o13 3. 十进 ...