最近在研究微服务,研发有需求,就是在Pod消亡前,nacos是能实现服务优雅下线。找到这篇文章,分享下:
说明
- 环境
工具版本
SpringBoot 2.3.3
Docker 19.03.12
Kubernetes 1.14
- 背景
服务端要支持 N 多个 Tcp Client 连接,所以做了负载,Tcp Client 会根据负载策略连接到不同的后端 Pod 上,这样就需要维护一个路由表:内部 ip <<==>> Tcp Client 的映射关系。所以在项目关闭的时候要有个关闭前处理(把当前 Pod 路由从路由表中去掉)的过程,也就是优雅关闭①。
SpringBoot 优雅关闭
- 配置
server: # 开启优雅关闭,默认:IMMEDIATE,立即关闭 shutdown: gracefulspring: lifecycle: # 配置优雅关闭宽限时间 timeout-per-shutdown-phase: 30s
- **server.shutdown:**配置是否开启优雅关闭。GRACEFUL:支持优雅关闭。IMMEDIATE:不支持优雅关闭,直接关闭。
- **spring.lifecycle.timeout-per-shutdown-phase:**优雅关闭最大容忍时间。比如这里设置 30s,那么就是在发起关闭请求②到真正关闭③项目之间,允许项目做未做完、关闭之前要做的事的最长时间为 30s,如果 30s 都没处理完,那么久强制关闭掉。
支持优雅关闭的服务器
Web Server开始优雅关闭之后新的接口访问行为说明tomcat 9.0.33+停止接收请求,客户端新请求等待超时。Reactor Netty停止接收请求,客户端新请求等待超时。Undertow停止接收请求,客户端新请求直接返回 503。
SpringBoot 结合 Docker 的优雅关闭
以下操作需本地安装好 Docker 并配置好 Idea。
- SpringBoot 中引入以下插件
com.spotify dockerfile-maven-plugin 1.4.10${docker.image.prefix}/${project.artifactId}${project.version}${project.build.finalName}.jartruebuild-imagepackagebuildpush-imageinstallpush
- 项目根目录增加 Dockerfile 文件
# 这里可以使用你自己的镜像FROM ******/centos8:jdk14-baseARG JAR_FILECOPY target/$JAR_FILE app.jarRUN echo 'export LANG=zh_CN.utf8 && exec java $JAVA_OPTS -jar /app.jar' > docker-entrypoint.sh && chmod +x /docker-entrypoint.shENTRYPOINT ["sh", "-c", "/docker-entrypoint.sh"]
- 之后就可以 mvn clean package 即可
docker stop + -t 参数可以指定优雅关闭容忍时间。
问题
这里有个问题要说一下,SpringBoot 项目开启了优雅关闭,这里 Docker 容器使用了 CentOS8,也就是说想要 SpringBoot 项目想要让自己的优雅关闭生效,其进程就需要接收到系统发送的 SIGINT(kill -2)/SIGTERM(kill -15) 信号,然后执行优雅关闭流程。
一般我们配置 ENTRYPOINT、CMD 时会有这样几种配置方式。
ENTRYPOINT ["java", "-jar", "/app.jar"]1
ENTRYPOINT ["sh", "-c", "/docker-entrypoint.sh"]1
前者不能加动态参数,后者倒是可以,但是如果直接在脚本中这样调用:java -jar $OPTION /app.jar 是可以加动态参数,但是无法达到优雅关闭的目的。总结来说:前者可以不能加动态参数但是可以优雅关闭,后者可以加动态参数但是不能优雅关闭
究其原因是在使用 docker stop container_id 命令时,Docker 会把 SIGTERM 信号传递给容器内 pid 为 1 的进程。前者启动方式,java 进程的 pid 是为 1 的,后者启动 sh 的进程是为 1 的。也在网上找了不少资料。看了如下这篇:
https://my.oschina.net/u/2552286/blog/3039592
结果并不好,没有效果,也可能没配置到位。后又在官方找到了这种解决方法,完美解决:
https://spring.io/guides/topicals/spring-boot-docker
![20290e43bf93b6c4cad2fad3fa22df02.png](https://i-blog.csdnimg.cn/blog_migrate/7d8c5c00dd09714fed77c4333bf7f1c2.jpeg)
SpringBoot 结合 Kubernetes 的优雅关闭
terminationGracePeriodSeconds
一旦 Kubernetes 决定终止您的 Pod,就会发生一系列事件。让我们看看 Kubernetes 终止生命周期的每一步。
- Pod 设置为 Terminating 状态,并从所有服务的 Endpoints 列表中删除。此时,Pod 停止获得新的流量。但在 Pod中 运行的容器不会受到影响。
- preStop Hook 被执行(如果设置了)。preStop Hook 是一个发送到 Pod 中的容器特殊命令或 Http 请求。如果您的应用程序在接收 SIGTERM 时没有正常关闭,您可以使用 preStop Hook 来触发正常关闭。接收 SIGTERM 时大多数程序都会正常关闭,但如果您使用的是第三方代码或管理的系统无法控制,则 preStop Hook 是在不修改应用程序的情况下触发正常关闭的好方法。
- SIGTERM 信号被发送到 Pod,此时,Kubernetes 将向 pod 中的容器发送 SIGTERM 信号。这个信号让容器知道它们很快就会关闭。您的代码应该监听此事件并在此时开始干净利落关闭。这可能包括停止任何长期连接(如数据库连接或 WebSocket 流),保存当前状态或其它类似的事情。即使您使用 preStop Hook,如果您发送 SIGTERM 信号,测试应用程序会发生什么情况也很重要,以确保您对生产环境并不感到惊讶!
- Kubernetes 等待优雅的终止,Kubernetes 等待指定的时间称为优雅终止宽限期。默认情况下,这是 30 秒。值得注意的是,这与 preStop Hook 和 SIGTERM 信号并行发生。Kubernetes 不会等待 preStop Hook 完成。如果你的应用程序完成关闭并在 terminationGracePeriod 完成之前退出,Kubernetes 会立即进入下一步。如果您的 Pod 通常需要超过 30 秒才能关闭,请确保增加优雅终止宽限期。您可以通过在 Pod YAML 中设置 terminationGracePeriodSeconds ④选项来实现。
- SIGKILL 信号被发送到 Pod,并删除 Pod 如果容器在优雅终止宽限期后仍在运行,则会发送 SIGKILL 信号并强制删除。与此同时,所有的 Kubernetes 对象也会被清除。
- 结论:Kubernetes 可以出于各种原因终止 Pod,并确保您的应用程序优雅地处理这些终止,这是创建稳定系统和提供出色用户体验的核心。
- 注意 1:Kubernetes 文档指出,有些步骤是同时执行的(步骤 1,2,3)。因此有可能会导致该 Pod 仍然列在服务的 Endpoints 中并仍然接收流量,而它已经收到 SIGTERM 并且已经停止,因此负载均衡器上可能会有一些 Http 504。目前解决这个问题可以使用 preStop Hook 在容器收到 SIGTERM 时 sleep 一段时间,以确终止期间的流量可以正确处理。设置方式:
apiVersion: v1kind: Podmetadata: name: my-podspec: containers: - name: my-container image: busybox lifecycle: preStop: exec: command: - sleep - 30 terminationGracePeriodSeconds: 60
- 注意 2:preStop Hook 并不会影响 SIGTERM 的处理,因此有可能 preStopHook 还没有执行完就收到 SIGKILL 导致容器强制退出。因此如果 preStop Hook 设置了 n 秒,需要设置 terminationGracePeriodSeconds 为 terminationGracePeriodSeconds + n 秒。
注解
①:**优雅关闭:**就是在真正关闭项目之前,把未做完、关闭之前要做的事执行完毕。
②:**发起关闭请求:**Idea 中点击停止项目请求、Docker 中使用 docker stop container_id、Linux 中使用 kill -2 pid 或者 kill -15 pid、Kubernetes 中删除旧 Pod(滚动更新、删除 Pod等)。
③:**真正关闭:**项目进程已经被被关闭掉了,没了。比如:② 中的执行结果、kill -9 pid。
④:**在 Pod YAML 中设置 terminationGracePeriodSeconds 选项:**比如你可以设置 60 秒。
apiVersion: v1kind: Podmetadata: name: my-podspec: containers: - name: my-container image: busybox terminationGracePeriodSeconds: 60