SpringBoot 项目在容器中使用优雅关闭

说明

  • 环境
工具 版本
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
  1. server.shutdown:配置是否开启优雅关闭。GRACEFUL:支持优雅关闭。IMMEDIATE:不支持优雅关闭,直接关闭。
  2. 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) 信号,然后执行优雅关闭流程。
  
  一般我们配置 ENTRYPOINTCMD 时会有这样几种配置方式。
  

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 终止生命周期的每一步。

  1. Pod 设置为 Terminating 状态,并从所有服务的 Endpoints 列表中删除。此时,Pod 停止获得新的流量。但在 Pod中 运行的容器不会受到影响。
  2. preStop Hook 被执行(如果设置了)。preStop Hook 是一个发送到 Pod 中的容器特殊命令或 Http 请求。如果您的应用程序在接收 SIGTERM 时没有正常关闭,您可以使用 preStop Hook 来触发正常关闭。接收 SIGTERM 时大多数程序都会正常关闭,但如果您使用的是第三方代码或管理的系统无法控制,则 preStop Hook 是在不修改应用程序的情况下触发正常关闭的好方法。
  3. SIGTERM 信号被发送到 Pod,此时,Kubernetes 将向 pod 中的容器发送 SIGTERM 信号。这个信号让容器知道它们很快就会关闭。您的代码应该监听此事件并在此时开始干净利落关闭。这可能包括停止任何长期连接(如数据库连接或 WebSocket 流),保存当前状态或其它类似的事情。即使您使用 preStop Hook,如果您发送 SIGTERM 信号,测试应用程序会发生什么情况也很重要,以确保您对生产环境并不感到惊讶!
  4. Kubernetes 等待优雅的终止,Kubernetes 等待指定的时间称为优雅终止宽限期。默认情况下,这是 30 秒。值得注意的是,这与 preStop Hook 和 SIGTERM 信号并行发生。Kubernetes 不会等待 preStop Hook 完成。如果你的应用程序完成关闭并在 terminationGracePeriod 完成之前退出,Kubernetes 会立即进入下一步。如果您的 Pod 通常需要超过 30 秒才能关闭,请确保增加优雅终止宽限期。您可以通过在 Pod YAML 中设置 terminationGracePeriodSeconds 选项来实现。
  5. 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

发表评论

电子邮件地址不会被公开。 必填项已用*标注