作为一名系统工程师,你可能经常需要重启容器,毕竟Kubernetes的优势就是快速弹性伸缩和故障恢复,遇到问题先重启容器再说,几秒钟即可恢复,实在不行再重启系统,这就是系统重启工程师的杀手锏。然而现实并没有理论上那么美好,某些容器需要花费10s左右才能停止,这是为啥?有以下几种可能性:

  1.容器中的进程没有收到SIGTERM信号。

  2.容器中的进程收到了信号,但忽略了。

  3.容器中应用的关闭时间确实就是这么长。

  对于第3种可能性我们无能为力,本文主要解决1和2。

  如果要构建一个新的Docker镜像,肯定希望镜像越小越好,这样它的下载和启动速度都很快,一般我们都会选择一个瘦了身的操作系统(例如Alpine,Busybox等)作为基础镜像。

  问题就在这里,这些基础镜像的init系统也被抹掉了,这就是问题的根源!

  init系统有以下几个特点:

  1.它是系统的第一个进程,负责产生其他所有用户进程。

  2.init 以守护进程方式存在,是所有其他进程的祖先。

  它主要负责:

  1.启动守护进程

  2.回收孤儿进程

  3.将操作系统信号转发给子进程

1.Docker容器停止过程

  对于容器来说,init系统不是必须的,当你通过命令docker stop mycontainer来停止容器时,docker CLI会将TERM信号发送给mycontainer的PID为1的进程。

    • 如果PID 1是init进程:那么PID 1会将TERM信号转发给子进程,然后子进程开始关闭,最后容器终止。
    • 如果没有init进程:那么容器中的应用进程(Dockerfile中的ENTRYPOINT或CMD指定的应用)就是 PID 1,应用进程直接负责响应TERM信号。这时又分为两种情况:
      • 应用不处理SIGTERM:如果应用没有监听SIGTERM信号,或者应用中没有实现处理SIGTERM信号的逻辑,应用就不会停止,容器也不会终止。
      • 容器停止时间很长:运行命令docker stop mycontainer之后,Docker会等待10s,如果10s后容器还没有终止,Docker就会绕过容器应用直接向内核发送SIGKILL,内核会强行杀死应用,从而终止容器。

2.容器进程收不到SIGTERM信号?

  如果容器中的进程没有收到SIGTERM 信号,很有可能是因为应用进程不是 PID 1,PID 1 是 shell,而应用进程只是 shell 的子进程。而 shell 不具备 init 系统的功能,也就不会将操作系统的信号转发到子进程上,这也是容器中的应用没有收到 SIGTERM 信号的常见原因。

  问题的根源就来自Dockerfile,例如:

FROM alpine:3.7
COPY popcorn.sh .
RUN chmod +x popcorn.sh
ENTRYPOINT ./popcorn.sh

  ENTRYPOINT指令使用的是shell模式,这样Docker就会把应用放到shell中运行,因此shell是PID 1。

  解决方案有以下几种:

  1.使用exec模式的ENTRYPOINT指令

  与其使用shell模式,不如使用exec模式,例如:

FROM alpine:3.7
COPY popcorn.sh .
RUN chmod +x popcorn.sh
ENTRYPOINT ["./popcorn.sh"]

  这样PID 1就是./popcorn.sh,它将负责响应所有发送到容器的信号,至于./popcorn.sh 是否真的能捕捉到系统信号,那是另一回事。

  举个例子,假设使用上面的Dockerfile来构建镜像,popcorn.sh脚本每过一秒打印一次日期:

#!/bin/sh
while true
do
date
sleep 1
done

  构建镜像并创建容器:

 → docker build -t truek8s/popcorn .
→ docker run -it --name corny --rm truek8s/popcorn

  打开另外一个终端执行停止容器的命令,并计时:

 → time docker stop corny

  因为popcorn.sh并没有实现捕获和处理SIGTERM信号的逻辑,所以需要10s左右才能停止容器。要想解决这个问题,就要往脚本中添加信号处理代码,让它捕获到SIGTERM信号时就终止进程:

#!/bin/sh

# catch the TERM signal and then exit
trap "exit" TERM
while true
do
date
sleep 1
done

  注意:下面这条指令与shell模式的ENTRYPOINT指令是等效的:

ENTRYPOINT ["/bin/sh", "./popcorn.sh"]

  2.直接使用exec命令

  如果你就想使用shell模式的ENTRYPOINT 指令,也不是不可以,只需将启动命令追加到exec后面即可,例如:

FROM alpine:3.7
COPY popcorn.sh .
RUN chmod +x popcorn.sh
ENTRYPOINT exec ./popcorn.sh

  这样exec就会将shell进程替换为./popcorn.sh进程,PID 1仍然是./popcorn.sh。

  3.使用init系统

  如果容器中的应用默认无法处理SIGTERM信号,又不能修改代码,这时候方案1和2都行不通了,只能在容器中添加一个init系统。init 系统有很多种,这里推荐使用tini,它是专用于容器的轻量级init系统,使用方法也很简单:

  1.安装tini

  2.将tini设为容器的默认应用

  3.将popcorn.sh作为tini的参数

  具体的Dockerfile如下:

FROM alpine:3.7
COPY popcorn.sh .
RUN chmod +x popcorn.sh
RUN apk add --no-cache tini
ENTRYPOINT ["/sbin/tini", "--", "./popcorn.sh"]

  现在tini就是PID 1,它会将收到的系统信号转发给子进程popcorn.sh。

  注意: 如果你想直接通过docker命令来运行容器,可以直接通过参数--init来使用 tini,不需要在镜像中安装tini。如果是Kubernetes就不行了,还得老老实实安装tini。

3.使用tini后应用还需要处理SIGTERM吗?

  最后一个问题:如果移除popcorn.sh中对SIGTERM信号的处理逻辑,容器会在我们执行停止命令后立即终止吗?

  答案是肯定的。在Linux系统中,PID 1和其他进程不太一样,准确地说应该是init 进程和其他进程不一样,它不会执行与接收到的信号相关的默认动作,必须在代码中明确实现捕获处理SIGTERM信号的逻辑,方案1和2干的就是这个事。

  普通进程就简单多了,只要它收到系统信号,就会执行与该信号相关的默认动作,不需要在代码中显示实现逻辑,因此可以优雅终止。

Docker 优雅终止方案的更多相关文章

  1. Docker 容器优雅终止方案

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

  2. Go的优雅终止姿势

    最近优化了一版程序:用到了golang的优雅退出机制. 程序使用etcd的election sdk做高可用选主,需要在节点意外下线的时候,主动去etcd卸任(删除10s租约), 否则已经下线的节点还会 ...

  3. 体验SpringBoot(2.3)应用制作Docker镜像(官方方案)

    关于<SpringBoot-2.3容器化技术>系列 <SpringBoot-2.3容器化技术>系列,旨在和大家一起学习实践2.3版本带来的最新容器化技术,让咱们的Java应用更 ...

  4. 详解SpringBoot(2.3)应用制作Docker镜像(官方方案)

    关于<SpringBoot-2.3容器化技术>系列 <SpringBoot-2.3容器化技术>系列,旨在和大家一起学习实践2.3版本带来的最新容器化技术,让咱们的Java应用更 ...

  5. 一个神奇的bug:OOM?优雅终止线程?系统内存占用较高?

    摘要:该项目是DAYU平台的数据开发(DLF),数据开发中一个重要的功能就是ETL(数据清洗).ETL由源端到目的端,中间的业务逻辑一般由用户自己编写的SQL模板实现,velocity是其中涉及的一种 ...

  6. Node 出现 uncaughtException 之后的优雅退出方案

    Node 的异步特性是它最大的魅力,但是在带来便利的同时也带来了不少麻烦和坑,错误捕获就是一个.由于 Node 的异步特性,导致我们无法使用 try/catch 来捕获回调函数中的异常,例如: try ...

  7. Docker 多主机方案

    利用OpenVSwitch构建多主机Docker网络 [编者的话]当你在一台主机上成功运行Docker容器后,信心满满地打算将其扩展到多台主机时,却发现前面的尝试只相当于写了个Hello World的 ...

  8. Docker 私有仓库方案比较与搭建

    我们知道docker镜像可以托管到dockerhub中,跟代码库托管到github是一个道理.但如果我们不想把docker镜像公开放到dockerhub中,只想在部门或团队内部共享docker镜像,能 ...

  9. 【云计算】Docker 多进程管理方案

    docker容器内多进程的管理方案 时间 2015-05-08 00:00:00                                               涯余            ...

随机推荐

  1. 高阶函数 / abs方法

    abs()求绝对值,填括号里面

  2. zabbix学习笔记:zabbix监控之短信报警

    zabbix学习笔记:zabbix监控之短信报警 zabbix的报警方式有多种,除了常见的邮件报警外,特殊情况下还需要设置短信报警和微信报警等额外方式.本篇文章向大家介绍短信报警. 短信报警设置 短信 ...

  3. WPS2019党政机关单位版(无广告困扰)

    WPS2019党政机关单位版(无广告困扰) 科技趣闻 ​ 中国石油大学(华东) 控制科学与工程硕士 17 人赞同了该文章 导读 WPS Office 2019专业版机关版是由WPS官方专为企业.机关单 ...

  4. linux服务之FTP服务篇

    一.FTP协议 FTP服务器(File Transfer Protocol Server)是在互联网上提供文件存储和访问服务的计算机,它们依照FTP协议提供服务. FTP (File Transfer ...

  5. Linux进阶之日志管理

    一.何为日志 1.在程序执行时,可以通过标准输出以及错误输出,让我们知道程序的执行情况,而系统不可能将所有程序的输出信息一起显示,要知道后台执行的程序非常之多,如果一起显示,那我们不用操作了,整天只看 ...

  6. 8.模块定义导入优化time datetime内置模块

    1.模块(module)的定义:本质就是.py的python文件用来从逻辑上组织python代码(变量\函数\类\逻辑:实现一个功能)包(package)的定义:用来从逻辑上组织模块的,本质就是一个文 ...

  7. 第一章 DevOps概述

    什么是软件开发 软件开发是根据用户要求建造出软件系统或者系统中的软件部分的过程. 软件开发是一项包括需求捕捉,需求分析,实现和测试的系统工程 软件开发有哪些困难? 软件开发的本质困难 复杂性 不可见性 ...

  8. 也谈如何写一个Webserver(三)

    在上一篇里,我介绍了如何应用socket和epoll来组织和管理从客户端(如,浏览器)传入的连接,通过设置非阻塞连接让Webserver有更好的性能. 下面,我介绍一下在我写的Webserver Ma ...

  9. Python小白的数学建模课-04.整数规划

    整数规划与线性规划的差别只是变量的整数约束. 问题区别一点点,难度相差千万里. 选择简单通用的编程方案,让求解器去处理吧. 『Python小白的数学建模课 @ Youcans』带你从数模小白成为国赛达 ...

  10. CRC校验原理简介及C代码实现说明

    1 原理 参考文档:CRC校验 (qq.com) 参考书籍:<计算机网络(第7版)-谢希仁> 1.1 原理简介 CRC是一种检错方法. 在发送端,先把数据划分为组,假定每组k个比特.现假定 ...