转载备份
影子 DOM(Shadow DOM)
你的 docker stop,它优雅吗? - 无糖拿铁,谢谢
清理Docker的container,image与volume · 零壹軒·笔记
Create a PyPI Mirror Site with devpi-server – SRE
优雅的终止 docker 容器 | iTimothy
Odoo 14 开发者指南第二十一章 性能优化 | Alan Hou 的个人博客
Odoo 14 开发者指南第八章 高级服务端开发技巧 | Alan Hou 的个人博客
kafka 系列:设置日志数据保存过期时间(含某个 topic)、日志策略_NIO4444-CSDN 博客_kafka 配置数据过期时间
Chromium 历史版本离线安装包 - 下载方法
怎样将 props 传递给 {this.props.children} | WebFuse
HappyBaseDoc
用户指南 — HappyBase 1.2.0 文档
安装指南 — HappyBase 1.2.0 文档
API 参考 — HappyBase 1.2.0 文档
PostgreSQL 时间转换
JS 中创建给定长度的数组
GSAP 入门 - 学习中心 - 绿袜
操作系统复习 | Happy Coding
如何理解 ip 路由和操作 linux 的路由表 - CodeAntenna
Elasticsearch 7.11 tokenizer, analyzer and filter 以及 IK 分词配置同义词、远程拓展词库 – Brave new world
podman 容器内访问 host 主机的端口 - 知识库 - BSMI KB 基础标准矿产工业
吐血总结!100 道经典 Python 面试题集锦上(附答案)
中共党史简表(1919 年 - 1949 年)
Dockerfile 详解_万 wu 皆可爱的博客 - CSDN 博客_dockerfile
为你的 Python 应用选择一个最好的 Docker 映像 | 亚马逊 AWS 官方博客
Ubuntu Server 支持中文
docker push | Docker Documentation
docker 创建本地仓库详解 (push/pull)_乱红飞的博客 - CSDN 博客_docker push 本地仓库
基于 Ubuntu 20.04 安装 Kubernetes 1.18
PostgreSQL 集群篇——PostgreSQL 的配置文件解析_51CTO 博客_postGresql
【PostgreSQL】——主从流复制_Teingi 的博客 - CSDN 博客_postgresql 主从复制
PostgreSQL: Documentation: 14: 27.4. Hot Standby
postgresql 主从复制、主从切换_偷懒的小陈的博客 - CSDN 博客_postgresql 主从
Postgres 用户、角色与权限 :: 68hub — 技术博客
中国共产党第二十次全国代表大会在京开幕 一图速览二十大报告
配置 docker 通过代理服务器拉取镜像
IPVS no destination available - Kubernetes 实践指南
Python 风格规范 — Google 开源项目风格指南
互动测试!党的二十大报告 100 题
自定义 ESlint 规则
Java 读取 OpenSSL 生成的秘钥, 进行 RSA 加解密 | 数字魔法
CSS(一)chrome 浏览器表单自动填充默认样式 - autofil_半个 GIS 半个前端的博客 - CSDN 博客
Nginx 多级代理下的真实 IP 透传 - CodeAntenna
Jenkins 环境变量
人民币金额大写规范 - 内蒙古农业大学财务处
[转]nginx 开启 websocket - 浅忆博客
ceph 创建使用 rbd
《三》配置 ceph 存储池 pool - Buxl's blog
基于 K8S 搭建 Ceph 分部署存储 – 唐玥璨 | 博客
序言 · Kubernetes 中文指南——云原生应用架构实战手册
服务器配置 - Redis 安装配置 | 灰帽子 - 任令仓的技术博客
Ubuntu 配置 sudo 命令不需要输入密码_ubuntu sudo 免密_一路向前 - 执着的博客 - CSDN 博客
修改 Docker 数据目录位置,包含镜像位置 - 腾讯云开发者社区 - 腾讯云
微服务架构实践(API Gateway)
微服务网关:从对比到选型,由理论到实践 | Java 程序员进阶之路
聊聊微服务网关
微服务网关:从对比到选型,由理论到实践
odoo 实现表分区 partition
使用 keepalived 搭建高可用服务 - 简书
业务网关的落地实践_文化 & 方法_Qunar 技术沙龙_InfoQ 精选文章
部署 Kubernetes PostgreSQL 实例 | domac 的菜园子
一套包含完整前后端的系统如何在 K8S 中部署?_k8s 前端_木讷大叔爱运维的博客 - CSDN 博客
前端安全系列(二):如何防止 CSRF 攻击? - 美团技术团队
traefik 自定义中间件 | coolcao 的小站
CSRF 原理和实战利用 - FreeBuf 网络安全行业门户
安全运维 - 如何在 Kubernetes 中使用注释对 ingress-nginx 及后端应用进行安全加固配置实践_唯一极客知识分享的技术博客_51CTO 博客
Kubernetes 进阶使用之 Helm,Kustomize
各种加密算法比较
Docker 的三种网络代理配置 · 零壹軒 · 笔记
本文档使用 MrDoc 发布
-
+
首页
你的 docker stop,它优雅吗? - 无糖拿铁,谢谢
# [转载:你的 docker stop,它优雅吗?](https://blog.gaoyuexiang.cn/posts/graceful-shutdown-docker-container/) 我们平时在使用 `Docker` 的时候,一般会使用 `ctrl+c` 或者 `docker stop` 的方式关闭容器。 但有时候我们可能会遇到 `ctrl+c` 不生效,或者 `docker stop` 之后要等待 10s 的情况,就像这样:  也许你会觉得 10s 是一个可以忍受的时间, 但这样的问题真的只有 10s 这么简单吗? 为什么有的时候不能立即关闭容器呢? ## docker stop 怎么关闭容器 首先我们来看一下这两个命令做了什么。 ### `ctrl+c` 到底做了什么 我们能够使用 `ctrl+c` 的场景,是我们在使用 `foreground` 模式运行容器的时候。 这时我们按下 `ctrl+c` 就像在普通的 `shell` 中按下这个组合键一样,发送一个 `SIGINT` 信号给当前的进程,通知它终止运行。 ### docker stop 做了什么 `docker stop` 命令是在对 `detached` 模式运行的容器发出停止命令时使用的,从发送信号上来讲,它将发送 `SIGTERM` 信号给容器,通知其结束运行。 > `SIGINT` 一般用于关闭前台进程,`SIGTERM` 会要求进程自己正常退出。 当我们在 `shell` 中给进程发送 `SIGTERM` 和 `SIGINT` 信号的时候,这些进程往往都能正确的处理。 但是在 `docker` 中却不灵了。 这是因为在 `docker` 中,只会将 `SIGTERM` 等所有的 `signal` 信号发送给 PID 为 1 的进程,当我们 `docker` 中运行的进程的进程号不是 1 时,就不会收到这样的信号。 > 根据[这篇文章](https://mp.weixin.qq.com/s/vaIBGHmdUT0bHP2O722AQQ)的说法,只有 `alpine` 会出现这个问题,但从我搜到的资料和实验来看,并不是这样,而是所有的镜像都会有这个问题。 ### 为什么是 10s 其实这只是 `docker` 的默认设置,如果你愿意,等十年都可以。 文档链接:[https://docs.docker.com/engine/reference/commandline/stop/](https://docs.docker.com/engine/reference/commandline/stop/) 如果达到上面的时间限制,`docker` 将会通过给内核发送 `SIGKILL` 从而强制结束容器。 ### 验证上面的回答 为了验证上面查到的这些结果,我写了一点 demo。 在 demo 的场景里,我们会在 `ENTRYPOINT` 配置运行一个 `shell` 脚本,在脚本中做一些准备工作后启动进程。 ``` FROM openjdk:8-jre-alpine COPY ./build/libs/app.jar /app/app.jar COPY ./docker/entrypoint.sh /app/entrypoint.sh WORKDIR /app EXPOSE 8080 ENTRYPOINT ["./entrypoint.sh"] ``` entrypoint.sh ``` #!/bin/sh echo 'Do something' java -jar app.jar ``` > 后面还会用到这个例子 我们可以观察一下 `docker stop` 之后,`shell` 给我们的返回值  Figure 1. 注意红色方框 根据这张图,我们可以看到: 1. `docker stop` 命令等待了 10s 才结束 2. 结束的 docker container 返回了 `137`,表示进程是因为内核接收到了 `SIGKILL` 而结束的(`zsh` 给美化成了 KILL) ### Kill 导致的问题 现在我们已经了解了 `ctrl+c` 为什么不生效和 `docker stop` 等待 10s 的原因了。 我们再来看看另一个问题: **强制关闭容器,真的就没问题吗?** 或许你能想到,很多进程在结束阶段会做一些清理工作:比如删除临时目录、执行 shutdown hook 等。 但是当进程被强制关闭时,这些任务就不会被执行,那么我们就可能得到一些并不期望的结果。 以 `Eureka` 为例。 `Eureka client` 在结束进程时,需要向 `Eureka server` 发送 shutdown 信号,以注销 `client`。 这本来没什么问题,因为 `Eureka server` 即使没有收到这样的信息,也会定期清理 `client` 信息。 但是 `Eureka server` 还有一个 `self preservation` 模式,以防止意外的网络事件导致大量的 `client` 下线。 这就有可能导致 `Eureka` 集群的注册表中出现大量的 `client` 信息,但它们其实已经关闭了。 ## 如何优雅的关闭容器 通过前面的内容,我们已经了解了容器没有被优雅关闭的原因和可能导致的问题,接下来,我们来看看如何解决。 > 前面提到的那篇文章中提到可以使用 `tini` 来解决,但我没有成功过。`tini` 的确做到了立即关闭进程,但是进程并没有执行 shutdown hook。 ### 使目标进程成为 PID 1 既然 `docker` 只会将 `sigal` 发送给 PID 1 的进程,那就让我们的进程成为 PID 1 的进程就好了。 #### docker 的 exec 与 shell 模式 `Dockerfile` 的 `ENTRYPOINT` 有两种写法,即 `exec` 和 `shell`: ``` # exec form ENTRYPOINT ["command", "param"] # shell form ENTRYPOINT command param ``` 两者的区别在于: - `exec` 形式的命令会使用 PID 1 的进程 - `shell` 形式的命令会被执行为 `/bin/sh -c <command>`,不会执行在 PID 1 上,也就不会收到 `signal` 所以,我们应该选择 `exec` 模式,让我们的程序成为 PID 1 进程。 ``` ENTRYPOINT ["java", "-jar", "app.jar"] ``` > 更详细的信息,可以查看[官方文档](https://docs.docker.com/engine/reference/builder/#entrypoint)。 #### exec 命令 `exec` 形式的 `ENTRYPOINT` 只能解决 _无需任何准备工作就启动进程_ 的场景,而不能解决一些需要准备工作的复杂场景。 在这样的场景中,我们的 `ENTRYPOINT` 往往需要执行一个 `shell` 脚本: ``` ENTRYPOINT ["./entrypoint.sh"] ``` 然后在这个脚本中执行我们的准备工作,完成后再启动真正的进程。 比如上面的例子,做完准备后,启动 `java` 进程。 这时候,我们的 `java` 进程就无法成为 PID 1 进程。  我们可以看到,`java` 进程的 PID 是 7,也就无法优雅退出了。 为了解决这个问题,我们可以使用 `exec` 命令来解决。这个命令的作用就是**使用新的进程替代原有的进程,并保持 PID 不变**。 这就意味着我们可以在执行 `java` 命令的时候使用它,从而替换掉 PID 1 的 shell 脚本: entrypoint.sh ``` #!/bin/sh echo "Do something" exec java -jar app.jar ``` 我们再来看一下容器中的进程:  使用 `exec` 命令之后,我们无论是使用 `ctrl+c` 还是 `docker stop` 都能让进程接收到信号,执行相应的操作后退出:  这张图我们可以看到很多信息: 1. `docker stop` 命令很快结束,没有等待十秒 2. 容器退出收到的信号是 `SIGTERM`,不是 `SIGKILL` 3. `Spring` 进程的最后一行日志是 shutdown hook 的日志 这些信息表明,`java` 进程收到了 `docker stop` 发送的 `SIGTERM` 信号,并且正确的触发了相关操作,最后退出程序。 ### 使用 trap `exec` 命令在这样的场景下算是一个比较完美的方案。 但如果你还想探索一下其他方式,或者你的容器中需要运行多个进程,那我们可以接着来看看 `trap` 命令。 `trap` 是用来设置陷阱、监听 `signal` 的 `shell` 命令,一般用来处理脚本收到的 `signal`,完成一些操作。 ``` trap [-lp] [[arg] sigspec ...] ``` > 本文不介绍 `lp` 参数的含义 - `arg` 代表接收到某个信号后要执行的操作,是一个 `shell` 命令 - `sigspec` 表示监听的信号,可以是多个 举个🌰: ``` trap 'echo "Shutting Down"' TERM #(1) ``` 1. 表示在接收到 `SIGTERM` 信号时输出 "Shutting Down" #### 添加 trap 简单了解了 `trap` 命令后,我们就可以来改造一下 `entrypoint.sh`: entrypoint.sh ``` #!/bin/sh echo 'Do something' kill_jar() { echo 'Received TERM' kill "$(ps -ef | grep java | grep app | awk '{print $1}')" #(1) } trap 'kill_jar' TERM INT #(2) java -jar app.jar ``` 1. 找到执行的进程,使用 `kill` 命令向其发送 `SIGTERM` 2. 在脚本中监听 `SIGTERM` 和 `SIGINT` 信号,然后执行 `kill_jar` 函数 上面的脚本看起来可以正常工作,但实际上不能。 这是因为在 `bash` 中,即使 `trap` 收到了信号,如果这个时候 `bash` 在等待一个命令结束的话, 那么 `trap` 就会等到这个命令结束才会被执行。 > If Bash is waiting for a command to complete and receives a signal for which a trap has been set, the trap will not be executed until the command completes. — https://www.gnu.org/software/bash/manual/html\_node/Signals.html#Signals 在我们的场景中,`bash` 就在等待 `java` 进程结束,才能执行 `trap` 中的命令。 但是 `java` 进程又需要 `trap` 来关闭才能结束,所以程序陷入了循环依赖,只能 `docker stop` 等待 10s。 #### 后台运行 java 既然前面的问题是 `bash` 在等待 `java` 进程结束,那么我们就让它不等待就好了——后台执行 `java`: entrypoint.sh ``` #!/bin/sh echo 'Do something' kill_jar() { echo 'Received TERM' kill "$(ps -ef | grep java | grep app | awk '{print $1}')" echo 'Process finished' } trap 'kill_jar' TERM INT java -jar app.jar & #(1) wait $! #(2) ``` 1. 后台执行 `java` 2. 使用 `wait` 命令等待 `java` 进程结束,避免 `entrypoint.sh` 执行完成后容器直接退出 是不是觉得这样就 OK 了? Naive,上面的这个脚本能够帮助我们立即结束容器,但并不会等待进程自己正常退出:  我们可以看到,`kill_jar` 方法中的 `echo` 被成功执行,但是却没有看到 `Spring` 的 shutdown hook 日志输出。 这说明容器没有等待程序正常退出就被关闭了。 这里其实有两个问题。 `kill` 的问题 第一个是 `kill` 命令并不会等待进程结束,它只负责向进程发送 `SIG` 信号。 至于程序如何处理、什么时候处理,则与它无瓜。 `wait` 的问题 第二个问题则是 `wait` 命令,在上面 `bash` 对 `trap` 的解释后面,还有一句话: > When Bash is waiting for an asynchronous command via the wait builtin, the reception of a signal for which a trap has been set will cause the wait builtin to return immediately with an exit status greater than 128, immediately after which the trap is executed. — https://www.gnu.org/software/bash/manual/html\_node/Signals.html#Signals 也就是说,虽然 `bash` 在等待 `wait` 结束,但是 `wait` 又被特殊处理了 ——`trap` 收到任何大于 128 的信号都会让 `wait` 命令结束,以执行 `trap` 中的方法。 综合以上两点,我们会发现 `trap` 在执行 `kill_jar` 时,`entrypoint.sh` 中的 `wait` 已经结束,不再等待 `java` 进程结束。 `kill_jar` 仅仅发送了 `SIGTERM` 信号,也不会等待 `java` 进程结束。 由此,我们就可以对脚本进行改进: entrypoint.sh ``` #!/bin/sh echo 'Do something' kill_jar() { echo 'Received TERM' kill "$(ps -ef | grep java | grep app | awk '{print $1}')" wait $! #(1) echo 'Process finished' } trap 'kill_jar' TERM INT java -jar app.jar & wait $! ``` 1. 在 `kill` 后加了一行 `wait`,因为 `kill` 会返回进程号,所以这里也可以使用 `$!`。 这样,我们的 `kill_jar` 就会等到 `java` 进程完全退出后才会结束:  我们可以看到,`Spring` 的 shutdown hook 在 "Process finished" 之前输出,证明新加的 `wait` 命令发挥了作用。 ## 总结 1. `ctrl+c` 与 `docker stop` 都只会向容器中 PID 1 进程发送信号 2. `docker stop` 默认等待 10s 没有关闭容器后,会向内核发送 `SIGKILL` 以强制关闭容器 3. 解决方案: 1. 直接启动进程时,使用 `ENTRYPOINT` 的 `exec form` 2. 启动单一进程,并且需要一点准备工作时,使用 `exec` 命令 3. 启动多个进程时,组合使用 `trap`、`wait`、`kill` 命令 > - **本文链接:** [https://blog.gaoyuexiang.cn/posts/graceful-shutdown-docker-container/](https://blog.gaoyuexiang.cn/posts/graceful-shutdown-docker-container/ "你的 docker stop,它优雅吗?") > - **License:** [CC BY-NC 4.0 CN](https://creativecommons.org/licenses/by-nc/4.0/deed.zh)
幻翼
2021年11月18日 06:10
转发文档
收藏文档
上一篇
下一篇
手机扫码
复制链接
手机扫一扫转发分享
复制链接
Markdown文件
分享
链接
类型
密码
更新密码