说明
- 环境
工具 | 版本 |
---|---|
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: graceful
spring:
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 中引入以下插件
<!-- Docker 部署镜像相关 -->
<plugin>
<groupId>com.spotify</groupId>
<artifactId>dockerfile-maven-plugin</artifactId>
<version>1.4.10</version>
<configuration>
<!-- 镜像名称 -->
<repository>${docker.image.prefix}/${project.artifactId}</repository>
<!-- 镜像 tags -->
<tag>${project.version}</tag>
<!--<tag>latest</tag>-->
<!-- 变量 -->
<buildArgs>
<JAR_FILE>${project.build.finalName}.jar</JAR_FILE>
</buildArgs>
<!-- 启用 Maven Setting 配置 服务用户 -->
<useMavenSettingsForAuth>true</useMavenSettingsForAuth>
</configuration>
<executions>
<!-- 打包时构建镜像到本地 -->
<execution>
<id>build-image</id>
<phase>package</phase>
<goals>
<goal>build</goal>
</goals>
</execution>
<!-- 安装到本地时 push 到远程仓库 -->
<execution>
<id>push-image</id>
<phase>install</phase>
<goals>
<goal>push</goal>
</goals>
</execution>
</executions>
</plugin>
- 项目根目录增加
Dockerfile
文件
# 这里可以使用你自己的镜像
FROM ******/centos8:jdk14-base
ARG JAR_FILE
COPY target/$JAR_FILE app.jar
RUN echo 'export LANG=zh_CN.utf8 && exec java $JAVA_OPTS -jar /app.jar' > docker-entrypoint.sh \
&& chmod +x /docker-entrypoint.sh
ENTRYPOINT ["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"]
ENTRYPOINT ["sh", "-c", "/docker-entrypoint.sh"]
前者不能加动态参数,后者倒是可以,但是如果直接在脚本中这样调用: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
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: v1
kind: Pod
metadata:
name: my-pod
spec:
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: v1
kind: Pod
metadata:
name: my-pod
spec:
containers:
- name: my-container
image: busybox
terminationGracePeriodSeconds: 60
文章评论