概述

优雅关闭:在关闭前,执行正常的关闭过程,释放连接和资源,如我们操作系统执行 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 信号,只能等超时后被强行杀死了。

案例分析

go 开发的一个 Demo

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)
}

代码参考:https://www.jianshu.com/p/ae72ad58ecb6

1、Signal.Notify 会监听括号内指定的信号,若没有指定,则监听所有信号。2、通过 switch 对监听到信号进行判断,如果是 SININT 和 SIGTERM 则条用 Exitfunc 函数执行退出。

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

编写应用 Dockerfile 文件

概述 在 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 aux
USER         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-shell$ docker 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-exec$ docker logs app-exec
进程启动...
退出 terminated
正在退出...
执行清理...
退出完成...

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

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

在实际生产环境中,因为应用启动命令后会接很多启动参数,所以通常我们会使用一个启动脚本来启动应用,方便我们启动应用。对应的在容器内 PID 为 1 的进程为 shell 进程但 shell 程序不转发 signals,也不响应退出信号。所以在容器应用中如果应用容器中启动 shell,占据了 pid=1 的位置,那么就无法接收 k8s 发送的 SIGTERM 信号,只能等超时后被强行杀死了。启动脚本 start.sh

$ cat > start.sh<< EOF
#!/bin/sh
sh -c /root/app
EOF
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 /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 aux

docker stop 关闭应用

$ docker 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 参数调用容器内进程关闭脚本,实现优雅关闭。

方案二:通过第三方 init 进程传递 SIGTERM 到进程中。

方案验证

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

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

启动脚本 start.sh

$ cat > start.sh<< EOF
#!/bin/sh
./app
EOF

stop.sh 优雅关闭脚本

#!/bin/sh
ps -ef|grep app|grep -v grep|awk '{print $1}'|xargs kill -15
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 /root/
CMD ["/bin/sh","/root/start.sh"]

构建镜像

$ docker build -t app:v1.0-prestop ./

通过 yaml 部署到 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: 172.16.1.31/library/app:v1.0-prestoplifecycle:preStop:exec:command:- sh- /root/stop.sh

查看 POD 日志,然后删除 pod 副本

$ kubectl get pod
NAME                            READY   STATUS    RESTARTS   AGE
app-prestop-847f5c4db8-mrbqr    1/1     Running   0          73s

查看日志

$ kubectl logs app-prestop-847f5c4db8-mrbqr -f
进程启动...

另外窗口删除 POD

$ kubectl logs app-prestop-847f5c4db8-mrbqr -f
进程启动...退出 terminated
正在退出...
执行清理...
退出完成...

可以看见执行了 Prestop 脚本进行优雅关闭。同样的可以将 yaml 文件中的 Prestop 脚本取消进行对比测试可以发现就会进行强制删除。

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

修改start.sh脚本

#!/bin/sh
exec ./app

shell 中添加一个 exec 即可让应用进程替代当前 shell 进程,可将 SIGTERM 信号传递到业务层,让业务实现优雅关闭。

可使用上面例子,进行修改测试。

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

使用 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"]

构建镜像

$ 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 会将关闭信号向子进程传递,但不会等待子进程完全退出后自己在退出。而是传递完后直接就退出了。

相关 issue: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-baron$ docker stop  app-smell-baron进程启动...
退出 terminated
正在退出...
执行清理...
退出完成...

总结:

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

项目地址:

  • https://github.com/insidewhy/smell-baron[1]

  • https://github.com/Yelp/dumb-init[2]

  • https://github.com/krallin/tini[3]

脚注

[1]

https://github.com/insidewhy/smell-baron: https://github.com/insidewhy/smell-baron

[2]

https://github.com/Yelp/dumb-init: https://github.com/Yelp/dumb-init

[3]

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

原文链接:https://www.bladewan.com/2021/05/26/graceful_close/

你可能还喜欢

点击下方图片即可阅读

为什么Pod突然就不见了?

云原生是一种信仰 ????

关注公众号

后台回复◉k8s◉获取史上最方便快捷的 Kubernetes 高可用部署工具,只需一条命令,连 ssh 都不需要!

点击 "阅读原文" 获取更好的阅读体验!

发现朋友圈变“安静”了吗?

容器应用优雅关闭的终极大招相关推荐

  1. docker中java钩子_springboot项目在docker容器中如何优雅关闭

    前言 什么是优雅关闭 在我看来所谓的优雅关闭,就是在系统关闭时,预留一些时间,让你有机会来善后一些事情 什么时候需要优雅关闭 是否所有项目都需要优雅关闭?那也不一定,毕竟所谓的优雅关闭,另一面就意味这 ...

  2. 如何优雅关闭 Spring Boot 应用

    点击蓝色"程序猿DD"关注我 回复"资源"获取独家整理的学习资料! 前言 随着线上应用逐步采用 SpringBoot 构建,SpringBoot应用实例越来多, ...

  3. Spring Boot使用@Async实现异步调用:ThreadPoolTaskScheduler线程池的优雅关闭

    上周发了一篇关于Spring Boot中使用@Async来实现异步任务和线程池控制的文章:<Spring Boot使用@Async实现异步调用:自定义线程池>.由于最近身边也发现了不少异步 ...

  4. Docker学习总结(50)——Docker 微服务优雅关闭

    背景 使用 docker stop 关闭容器时, 只有 init(pid 1)进程能收到中断信号, 如果容器的pid 1 进程是 sh 进程, 它不具备转发结束信号到它的子进程的能力, 所以我们真正的 ...

  5. 如何在 Spring Boot 优雅关闭加入一些自定义机制

    个人创作公约:本人声明创作的所有文章皆为自己原创,如果有参考任何文章的地方,会标注出来,如果有疏漏,欢迎大家批判.如果大家发现网上有抄袭本文章的,欢迎举报,并且积极向这个 github 仓库 提交 i ...

  6. K8s Pod优雅关闭,没你想象的那么简单!

    更新部署服务时,旧的 Pod 会终止,新 Pod 上位.如果在这个部署过程中老 Pod 有一个很长的操作,我们想在这个操作成功完成后杀死这个 pod(优雅关闭),如果无法做到的话,被杀死的 pod 可 ...

  7. Java程序优雅关闭的两种方法(程序停止前做一些善后工作)

    java程序关闭时,往往需要做一些善后工作,称之为优雅关闭.这里介绍两种比较典型的方法: 一.注册关闭钩子 通过调用Runtime.getRuntime().addShutdownHook()方法,添 ...

  8. 找回被盗iPhone的终极大招:查询ICCID

    找回被盗iPhone的终极大招:查询ICCID iPhone不慎丢失后怎么办?看图中的ICCID. 臧智渊 iPhone不慎丢失后怎么办?普通青年:立刻报警,基本没用.文艺青年:用Find my iP ...

  9. 如何优雅关闭 Netty服务

    Netty 中将写出数据分成了两个部分 第一部分先缓存起来 第二部分再通过 Java 原生的 SocketChannel 发送出去. 问题 try {// 省略其他代码// 9. 等待服务端监听端口关 ...

最新文章

  1. 为什么要低温保存_新酒为什么要贮存一段时间才能喝?瓶装白酒这样保存最好!...
  2. MEET大会报名开启 | 李开复张亚勤等产学研大咖邀你共同见证智能未来
  3. Function HDU - 6546 (数学,贪心)
  4. sql server charindex函数和patindex函数详解(转)
  5. Git 分支管理-git stash 和git stash pop
  6. MYSQL错误: ERROR 1205: Lock wait timeout exceeded(处理MYSQL锁等待)解决办法
  7. windows7更改开始菜单外观的方法
  8. mysql8.0.19初始密码输入错误_MySQL 8.0.19支持输入3次错误密码锁定账户功能(例子)...
  9. 手rm-linux联网后自动dhcp,Linux操作系统下DHCP基础配置
  10. Oracle日志切换及频率跟踪脚本
  11. python ocr文字识别竖排繁体_小巧免费的图片文字识别OCR软件 支持简体识别和竖排繁体中文...
  12. 跳跃表(skiplist )详解及其C++编程实现
  13. 第62篇:批量去除EXCEL文件密码
  14. c语言编程基础 王森,《C语言编程基础第2版》王森版 习题答案
  15. win7啊,我的纠结,ip啊
  16. python培训上岗
  17. py2neo——Neo4jpython的配合使用
  18. 自荐Mall4j项目一个基于spring boot的Java开源商城系统
  19. Ubuntu18.04中roboware安装问题
  20. 三极管的工作原理详解,图文+案例

热门文章

  1. 手握游戏王、宝可梦,卡牌游戏巨头云涌控股再闯IPO,还能打出好牌吗?
  2. oracle数据长度超过4000,有没有办法,突破VARCHAR2最大长度是4000的限制
  3. linux mysql开启事务_linux mysql 相关操作命令
  4. android 联系人 字母索引,Android ListView字母索引(仿微信通訊錄列表)
  5. 禅道任务指派,没有新增数据
  6. c 语言文件读写ppt,C/C++ 文件读写
  7. 面向对象三大特性五大原则 + 低耦合高内聚
  8. 使用Konva操作HTML5 Canvas:第5部分,事件
  9. c3p0连接池使用教程及实例
  10. [MySQL] like “%XX“ 和 like “XX%“ 的特殊情况