docker 容器优雅关闭 —— 筑梦之路

什么是优雅关闭?

优雅关闭: 在关闭前,执行正常的关闭过程,释放连接和资源,如我们操作系统执行shutdown

目前业务系统组件众多,互相之间调用关系也比较复杂,一个组件的下线、关闭会涉及到多个组件。

对于任何一个线上应用,如何保证服务更新部署过程中从应用停止到重启恢复服务这个过程中不影响正常的业务请求,这是应用开发运维团队必须要解决的问题。

传统的解决方式是通过将应用更新流程划分为手工摘流量、停应用、更新重启三个步骤,由人工操作实现客户端不对更新感知。这种方式简单而有效,但是限制较多:不仅需要使用借助网关的支持来摘流量,还需要在停应用前人工判断来保证在途请求已经处理完毕。

同时,在应用层也有一些保障应用优雅停机的机制,目前Tomcat、Spring Boot、Dubbo等框架都有提供相关的内置实现,如SpringBoot 2.3内置graceful shutdown可以很方便的直接实现优雅停机时的资源处理,同时一个普通的Java应用也可以基于Runtime.getRuntime().addShutdownHook()来自定义实现,它们的实现原理都基本一致,通过等待操作系统发送的SIGTERM信号,然后针对监听到该信号做一些处理动作。

优雅停机是指在停止应用时,执行的一系列保证应用正常关闭的操作。这些操作往往包括等待已有请求执行完成、关闭线程、关闭连接和释放资源等,优雅停机可以避免非正常关闭程序可能造成数据异常或丢失,应用异常等问题。优雅停机本质上是JVM即将关闭前执行的一些额外的处理代码

现状

现阶段,业务容器化后业务启动是通过shell脚本启动业务,对应的在容器内PID为1的进程为shell进程但shell 程序不转发signals,也不响应退出信号。所以在容器应用中如果应用容器中启动 shell,占据了 pid=1 的位置,那么就无法接收k8s发送的SIGTERM信号,只能等超时后被强行杀死了。 

案例

package mainimport ("fmt""os""os/signal""syscall""time"
)func main()  {c := make(chan os.Signal)signal.Notify(c, syscall.SIGTERM, syscall.SIGINT)go func() {for s := range c {switch s {case syscall.SIGINT, syscall.SIGTERM:fmt.Println("退出", s)ExitFunc()default:fmt.Println("other", s)}}}()fmt.Println("进程启动...")time.Sleep(time.Duration(200000)*time.Second)
}func ExitFunc()  {fmt.Println("正在退出...")fmt.Println("执行清理...")fmt.Println("退出完成...")os.Exit(0)
}

1、Signal.Notify会监听括号内指定的信号,若没有指定,则监听所有信号。

2、通过switch对监听到信号进行判断,如果是SININT和SIGTERM则条用Exitfunc函数执行退出。

SHELL模式和CMD模式带来的差异性

在Dockerfile中CMD和ENTRYPOINT用来启动应用,有shell模式和exec模式

使用shell模式,PID为1的进程为shell;使用exec模式PID为1的进程为业务本身。

SHELL模式FROM golang as builder
WORKDIR /go/
COPY app.go    .
RUN go build app.go
FROM ubuntu
WORKDIR /root/
COPY --from=builder /go/app .
CMD ./app# 构建镜像docker build -t app:v1.0-shell .# 进入容器查看进程docker exec -it app-shell ps auxUSER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root           1  0.7  0.0   2608   548 pts/0    Ss+  03:22   0:00 /bin/sh -c ./
root           6  0.0  0.0 704368  1684 pts/0    Sl+  03:22   0:00 ./app
root          24  0.0  0.0   5896  2868 pts/1    Rs+  03:23   0:00 ps aux可以看见PID为1的进程是sh进程此时执行docker stop,业务进程是接收不到SIGTERM信号的,要等待一个超时时间后被KILL日志没有输出SIGTERM关闭指令docker stop app-shell
app-shelldocker logs app-shell
进程启动...

 

EXEC模式FROM golang as builder
WORKDIR /go/
COPY app.go    .
RUN go build app.go
FROM ubuntu
WORKDIR /root/
COPY --from=builder /go/app .
CMD ["./app"]# 构建镜像docker build -t app:v1.0-exec .docker exec -it app-exec ps aux
USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root           1  2.0  0.0 703472  1772 pts/0    Ssl+ 03:33   0:00 ./app
root          14  0.0  0.0   5896  2908 pts/1    Rs+  03:34   0:00 ps aux可以看见PID为1的进程是应用进程此时执行docker stop,业务进程是可以接收SIGTERM信号的,会优雅退出docker stop app-exec
app-execdocker logs app-exec
进程启动...
退出 terminated
正在退出...
执行清理...
退出完成...

 注意:以上测试在ubuntu做为应用的base镜像测试成功,在alpine做为应用的base镜像时shell模式和exec模式都一样,都是应用进程为PID 1的进程。

直接启动应用和通过脚本启动区别

 在实际生产环境中,因为应用启动命令后会接很多启动参数,所以通常我们会使用一个启动脚本来启动应用,方便我们启动应用。

对应的在容器内PID为1的进程为shell进程但shell 程序不转发signals,也不响应退出信号。所以在容器应用中如果应用容器中启动 shell,占据了 pid=1 的位置,那么就无法接收k8s发送的SIGTERM信号,只能等超时后被强行杀死了。

# 启动脚本cat > start.sh<< EOF 
#!/bin/sh
sh -c /root/app# dockerfileFROM golang as builder
WORKDIR /go/
COPY app.go    .
RUN go build app.go
FROM alpine
WORKDIR /root/
COPY --from=builder /go/app .
ADD start.sh /root/
CMD ["/bin/sh","/root/start.sh"]# 构建镜像docker build -t app:v1.0-script .docker exec -it app-script ps aux
PID   USER     TIME  COMMAND
1     root     0:00  /bin/sh /root/start.sh
6     root     0:00  /root/app
19    root     0:00  ps auxdocker stop app-script# 登待超时后被强行KILL
docker logs app-script
进程启动...

解决方案

• 应用:应用自身需要实现优雅停机的处理逻辑,确保处理中的请求可以继续完成,资源得到有效的关闭释放,等等。针对应用层,不管是Java应用还是其他语言编写的应用,其实现原理基本一致,都提供了类似的监听处理接口,根据规范要求实现即可。

• 平台:平台层要能够将应用从负载均衡中去掉,确保应用不会再接受到新的请求连接,并且能够通知到应用要进行优雅停机处理。在传统的部署模式下,这部分工作可能需要人工处理,但是在K8s容器平台中,K8s的Pod删除默认就会向容器中的主进程发送优雅停机命令,并提供了默认30s的等待时长,若优雅停机处理超出30s以后就会强制终止。同时,有些应用在容器中部署时,并不是通过容器主进程的形式进行部署,那么K8s也提供了PreStop的回调函数来在Pod停止前进行指定处理,可以是一段命令,也可以是一个HTTP的请求,从而具备了较强的灵活性。通过以上分析,理论上应用容器化部署以后仍然可以很好的支持优雅停机,甚至相比于传统方式实现了更多的自动化操作,本文档后面会针对该方案进行详细的方案验证。

• 容器应用中第三方Init:在构建应用中使用第三方init如tini或dumb-init

方案一:通过k8s Prestop参数调用

 通过k8s的prestop参数调用容器内进程关闭脚本,实现优雅关闭

在前面脚本启动的dockerfile基础上,定义一个优雅关闭的脚本,通过k8s-prestop在关闭POD前调用优雅关闭脚本,实现pod优雅关闭 

# 启动脚本cat > start.sh<< EOF 
#!/bin/sh
./app
EOF# 关闭脚本
cat > stop.sh << EOF
#!/bin/sh
ps -ef|grep app|grep -v grep|awk '{print $1}'|xargs kill -15
EOF# dockerfileFROM golang as builder
WORKDIR /go/
COPY app.go    .
RUN go build app.go
FROM alpine
WORKDIR /root/
COPY --from=builder /go/app .
ADD start.sh /root/
CMD ["/bin/sh","/root/start.sh"]# 构建镜像docker build -t app:v1.0-prestop .# k8s部署apiVersion: apps/v1
kind: Deployment
metadata:name: app-prestoplabels:app: prestop
spec:replicas: 1selector:matchLabels:app: prestoptemplate:metadata:labels:app: prestopspec:containers:- name: prestopimage: harbor.codemiracle.com/library/app:v1.0-prestoplifecycle:preStop:exec:command:- sh- /root/stop.sh# 查看POD日志,然后删除pod副本kubectl get pod kubectl logs app-prestop-847f5c4db8-mrbqr -f
进程启动...# 另外窗口删除PODkubectl logs app-prestop-847f5c4db8-mrbqr -f
进程启动...退出 terminated
正在退出...
执行清理...
退出完成...可以看见执行了Prestop脚本进行优雅关闭。同样的可以将yaml文件中的Prestop脚本取消进行对比测试可以发现就会进行强制删除。

 方案二:shell脚本修改为exec执行

# start.sh#!/bin/sh
exec ./appshell中添加一个 exec 即可让应用进程替代当前shell进程,可将SIGTERM信号传递到业务层,让业务实现优雅关闭

方案三:通过第三init工具启动

通过第三方init进程传递SIGTERM到进程中

使用dump-init或tini做为容器的主进程,在收到退出信号的时候,会将退出信号转发给进程组所有进程。,主要适用应用本身无关闭信号处理的场景。docker–init本身也是集成的tini。 

FROM golang as builder
WORKDIR /go/
COPY app.go    .
RUN go build app.go
FROM alpine
WORKDIR /root/
COPY --from=builder /go/app .
ADD start.sh tini /root/
RUN chmoad a+x start.sh && apk add --no-cache tini
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["/root/tini", "--", /root/start.sh"]

首先运行了 /root/tini 作为初始化进程,然后启动了 /root/start.sh 脚本。这种设置有助于确保容器中的进程以正确的方式启动和终止,并处理信号,以便在容器停止时进行清理和关闭工作。

tini 是一个用于初始化进程并正确处理信号的工具,通常用于确保容器中的进程以正确的方式启动和终止。

# 构建镜像docker build -t app:v1.0-tini .# 运行docker run -itd --name app-tini app:v1.0-tini# 查看日志docker logs app-tini进程启动...发现容器快速停止了,但没有输出应用关闭和清理的日志

使用tini或dump-init做为应用启动的主进程。tini和dumb-init会将关闭信号向子进程传递,但不会等待子进程完全退出后自己在退出。而是传递完后直接就退出了 

https://github.com/krallin/tini/issues/180 

另外一个第三方的组件smell-baron能实现等待子进程优雅关闭后在关闭本身功能。但这个项目本身热度不是特别高,并且有很久没有维护 

FROM golang as builder
WORKDIR /go/
COPY app.go    .
RUN go build app.go
FROM ubuntu
WORKDIR /root/
COPY --from=builder /go/app .
ADD start.sh /root/
ADD smell-baron /bin/smell-baron
RUN chmod a+x /bin/smell-baron  && chmod a+x start.sh
ENTRYPOINT ["/bin/smell-baron"]
CMD ["/root/start.sh"]# 构建镜像docker build -t app:v1.0-smell-baron .docker run -itd --name app-smell-baron app:v1.0-smell-barondocker stop  app-smell-baron进程启动...
退出 terminated
正在退出...
执行清理...
退出完成...

1. 对于容器化应用启动命令建议使用EXEC模式。

2. 对于应用本身代码层面已经实现了优雅关闭的业务,但有shell启动脚本,容器化后部署到k8s上建议使方案一和方案二。

3. 对于应用本身代码层面没有实现优雅关闭的业务,建议使用方案三

 GitHub - insidewhy/smell-baron: A tiny init system written in C for docker containers.

GitHub - Yelp/dumb-init: A minimal init system for Linux containers

https://github.com/krallin/tini

搜集自网络,作为学习笔记

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/150413.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

centos7安装 ActiveMq Artemis,安装服务开机自启动

ActiveMQ Artemis 2.31.2 有java11环境 [rootlocalhost ~]# java -version openjdk version "11.0.20" 2023-07-18 LTS OpenJDK Runtime Environment (Red_Hat-11.0.20.0.8-1.el7_9) (build 11.0.208-LTS) OpenJDK 64-Bit Server VM (Red_Hat-11.0.20.0.8-1.el7_9) …

HTML5学习系列之响应式图像

HTML5学习系列之响应式图像 前言响应式图像响应视图大小响应屏幕方向响应像素密度响应图像格式自适应像素比自适应视图宽 总结 前言 学习记录 响应式图像 响应视图大小 容器 srcset&#xff1a;图片地址&#xff0c;必需有。media&#xff1a;设置媒体查询。sizes&#xff…

【Vue渲染】 条件渲染 | v-if | v-show | 列表渲染 | v-for

目录 前言 v-if和v-show的区别和联系 v-show和v-if如何选择 条件渲染|v-if|v-show v-if v-if v-else v-if v-else-if v-else template v-show 列表渲染|v-for v-for 前言 本文介绍Vue渲染&#xff0c;包含条件渲染v-if和v-show的区别和联系以及列表渲染v-for v-if和…

AtCoder ABC156

C - Rally 从0到100模拟一遍位置最小的点 最小值实际上可以计算导数&#xff0c;将复杂度降为 O ( 1 ) O(1) O(1) D - Bouquet 组合数 2 n − 1 − C ( n , a ) − C ( n , b ) 2^{n}-1-C(n,a)-C(n,b) 2n−1−C(n,a)−C(n,b) 由于a.b都不大&#xff0c;因此可以做。注意用递推…

Android Studio 写一个Java调用c++ 的demo

前提条件&#xff1a; 本地已经配置好了ndk环境,如果没有配置好&#xff0c;建议参考macos 配置ndk环境-CSDN博客 这篇链接。 新建一个Empty Project 比如我这里的Project的名字是HelloJNI&#xff0c;包名是com.example.hellojni 然后在src目录下&#xff0c;右键选择Add C …

Tensorflow2.0:CNN、ResNet实现MNIST分类识别

以下仅是个人的学习笔记 &#xff0c;内容可能是错误 CNN&#xff1a; import tensorflow as tf from tensorflow import keras from tensorflow.keras import layers# 导入数据 (x_train, y_train), (x_test, y_test) keras.datasets.mnist.load_data()# 数据预处理 x_tra…

Flink之OperatorState

在Flink中状态主要分为三种: Operator State(算子状态)Keyed State(键控状态)Broadcast State(广播状态) 这里简单介绍一下Operator State的使用,说到使用State就必然要使用到Flink的容错机制也就是Checkpoint.具体内容见代码注解 数据源 这里选用Socket作为Source输入,便于…

JVM虚拟机:通过日志学习PS+PO垃圾回收器

我们刚才设置参数的时候看到了-XXPrintGCDetails表示输出详细的GC处理日志&#xff0c;那么我们如何理解这个日志呢&#xff1f;日志是有规则的&#xff0c;我们需要按照这个规则来理解日志中的内容&#xff0c;它有两个格式&#xff0c;一个格式是GC的格式&#xff08;新生代&…

二叉树题目:二叉树的最近公共祖先

文章目录 题目标题和出处难度题目描述要求示例数据范围 解法一思路和算法代码复杂度分析 解法二思路和算法代码复杂度分析 题目 标题和出处 标题&#xff1a;二叉树的最近公共祖先 出处&#xff1a;236. 二叉树的最近公共祖先 难度 5 级 题目描述 要求 给定一个二叉树&…

Redis 的集群模式实现高可用

来源&#xff1a;Redis高可用&#xff1a;武林秘籍存在集群里&#xff0c;那稳了~ (qq.com) 1. 引言 前面我们已经聊过 Redis 的主从同步&#xff08;复制&#xff09;和哨兵机制&#xff0c;这期我们来聊 Redis 的集群模式。 但是在超大规模的互联网应用中&#xff0c;业务规…

2023年Java核心技术大会(Core Java Week 2023)-核心PPT资料下载

一、峰会简介 人工智能在22年、23年的再次爆发让Python成为编程语言里最大的赢家&#xff1b;云原生的持续普及令Go、Rust等新生的语言有了进一步叫板传统技术体系的资本与底气。我们必须承认在近几年里&#xff0c;Java阵营的确受到了前所未有的挑战&#xff0c;出现了更多更…

机器视觉系统选型-定光照强度

同一个外形结构的光源&#xff0c;光照强度受如下影响&#xff1a; 单颗灯珠的亮度灯珠排列的数量和密度漫射板/防护板的材质&#xff08;透明、半透明、全漫射&#xff09; 在合理范围内提升光照强度&#xff0c;可降低对相机曝光时长的要求 外形结构尺寸相同的两款光源&am…

Mac电脑VSCode配置PHP开发环境

1.安装 PHP 首先&#xff0c;打开终端&#xff0c;安装 Homebrew&#xff0c;输入如下命令&#xff1a; $ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" 安装了 Homebrew 之后&#xff0c;你可以使用下面的…

C++学习 --pair

目录 1&#xff0c; 什么是pair 2&#xff0c; 创建pair 2-1&#xff0c; 标准数据类型 2-2&#xff0c; 自定义数据类型 3&#xff0c; 查询元素 3-1&#xff0c; 标准数据类型 3-2&#xff0c; 自定义数据类型 1&#xff0c; 什么是pair 数据以键值对形式存放的容器&…

基于C++实现循环赛日程表(分治算法)

一、问题描叙 设有n2^k个运动员&#xff0c;要进行网球循环赛。现在要设计一个满足以下要求的比赛日程表 每个选手必须与其他n-1个选手各赛一场每个选手一天只能赛一次循环赛一共进行n-1天 二、问题分析 按此要求可将比赛日程表设计成n行n-1列的表&#xff0c;在表中第 i 行…

【微客云】外卖霸王餐-城市分站加盟-区域代理-服务商模式

主要功能介绍 程序主要功能&#xff1a; 1.自动定位用户展示活动 2.两个活动推广展示位 3.外卖CPS接入&#xff0c;或者更多CPS接入自己操作 4.报名支付报名费&#xff08;自定义报名费价格&#xff09;&#xff0c;取消订单30分钟内自动退款&#xff08;时间可调&#xff09; …

uniapp中使用render.js进行openers、arcgis等地图操作

uniapp中使用render.js进行openers、arcgis等地图操作 一、为啥需要render.js render.js主要作用于APP上&#xff0c;因为Uniapp本质为vuejshtml进行开发&#xff0c;整个技术栈还是H5&#xff0c;对DOM元素进行操作。而APP中没用Dom元素这个概念。因此利用render.js这个视图层…

NX二次开发UF_CAM_ask_cam_preferences 函数介绍

文章作者&#xff1a;里海 来源网站&#xff1a;里海NX二次开发3000例专栏 UF_CAM_ask_cam_preferences Defined in: uf_cam.h int UF_CAM_ask_cam_preferences(UF_CAM_preferences_p_t prefs ) overview 概述 This function provides the current settings of the CAM pre…

【Linux】安全审计-audit

文章目录 一、audit简介二、开启auditd服务三、相关文件四、审计规则五、审计日志查询及分析附录1&#xff1a;auditctl -h附录2&#xff1a;systemcall 类型 参考文章&#xff1a; 1、安全-linux audit审计使用入门 2、audit详细使用配置 3、Linux-有哪些常见的System Call&a…

优步让一切人工智能化

优步(Uber)的商业模式建立在对数据的颠覆性使用上--通过将双方智能手机的位置数据关联起来&#xff0c;将出租车司机与乘客配对。这意味着&#xff0c;它可以比传统出租车公司更快地安排司机去接乘客&#xff0c;极大地冲击了传统出租车公司的业务。 优步自成立以来&#xff0…