当前位置: 首页 > news >正文

SpringBoot优雅关机,监听关机事件,docker配置

Spring Boot 提供了多种方法来实现优雅停机(Graceful Shutdown),这意味着在关闭应用程序之前,它会等待当前正在处理的请求完成,并且不再接受新的请求。

一、优雅停机的基本概念

优雅停机的主要步骤如下:

  1. 停止接收新的请求:一旦收到关闭指令,服务器会停止接受新的请求。
  2. 处理当前请求:系统会继续处理已经在处理中的请求,确保这些请求能够正常完成。
  3. 释放资源:在所有请求处理完毕后,系统会释放已分配的资源,比如关闭数据库连接、断开网络连接等。
  4. 关闭服务:当所有资源都被正确释放之后,系统会安全地关闭服务。

二、实现优雅停机的方法

2.1、在 Spring Boot 2.3 及以上版本中启用优雅停机

从 Spring Boot 2.3 开始,默认集成了对优雅停机的支持,你只需要通过配置文件进行简单的设置即可。

application.ymlapplication.properties 文件中添加以下配置:

server:
  shutdown: graceful
spring:
  lifecycle:
    timeout-per-shutdown-phase: 60s # 设置最大等待时间为60秒

上述配置告诉 Spring Boot 使用优雅的方式关闭应用,并设置了最长等待时间。

2.2、 对于 Spring Boot 2.3 之前的版本

如果你使用的是 Spring Boot 2.3 之前的版本,则需要手动引入 spring-boot-starter-actuator 并利用其提供的 /shutdown 端点来实现优雅停机。

首先,在你的 pom.xml 或者 build.gradle 中添加依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

然后,在 application.yml 中开启 /shutdown 端点:

management:
  endpoints:
    web:
      exposure:
        include: "shutdown"
  endpoint:
    shutdown:
      enabled: true

接下来,你可以通过发送 POST 请求到 /actuator/shutdown 来触发优雅停机。

2.3、 自定义优雅停机逻辑

如果默认的优雅停机行为不能满足需求,你还可以自定义优雅停机逻辑。例如,对于不同的 Web 容器(如 Tomcat、Jetty、Undertow),可以编写相应的代码来控制线程池的行为。

以 Tomcat 为例,你可以创建一个类实现 GracefulShutdownListenerApplicationListener<ContextClosedEvent> 接口,来定制化优雅停机过程。

package cn.gxm.multiinstancetest.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.stereotype.Component;

/**
 * @author GXM
 * @version 1.0.0
 * @Description TODO
 * @createTime 2025年03月04日
 */
@Slf4j
@Component
public class GracefulShutdownListener implements ApplicationListener<ContextClosedEvent> {


    /**
     * 优雅关机是否已开始
     */
    private boolean isGracefulShutdownStarted = false;

    @Override
    public void onApplicationEvent(ContextClosedEvent event) {
        // 执行你的业务逻辑
        log.info("Executing custom logic before graceful shutdown...");
        this.isGracefulShutdownStarted = true;
    }


    public boolean isGracefulShutdownStarted() {
        return isGracefulShutdownStarted;
    }
}

2.4、测试

下面的测试中, spring.lifecycle.timeout-per-shutdown-phase 值为30。

在这里插入图片描述

2.4.1、接口测试

1、写一个接口/sleep/{seconds}来测试。

package cn.gxm.multiinstancetest.controller;

import cn.hutool.core.date.DateUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.net.InetAddress;
import java.net.UnknownHostException;

/**
 * @author GXM
 * @version 1.0.0
 * @Description TODO
 * @createTime 2025年01月03日
 */
@RestController
@RequestMapping("/test")
@Slf4j
public class TestController {

    @GetMapping("/say")
    public String say() throws UnknownHostException {
        InetAddress localHost = InetAddress.getLocalHost();
        log.info("localhost: {}", localHost);
        return localHost.getHostAddress() + "_" + localHost.getHostName();
    }

    @GetMapping("/sleep/{seconds}")
    public String sleep(@PathVariable(value = "seconds") Integer seconds) {
        String startTime = DateUtil.now();
        try {
            Thread.sleep(seconds * 1000);
        } catch (Exception e) {
            log.error("sleep:", e);
        }
        String endTime = DateUtil.now();
        return startTime + "      " + endTime;
    }
}

2、第一种情况:在关机之前,请求接口http://127.0.0.1:9600/multi-instance-test/test/sleep/20,会导致睡眠20秒,接着马上在Idea关机程序,触发优雅关机。接口正常返回,并相差20秒,并且查看程序日志也是发现等待接口完成后才关机

在这里插入图片描述
在这里插入图片描述

3、第二种情况:在关机之前,请求接口http://127.0.0.1:9600/multi-instance-test/test/sleep/50,会导致睡眠50秒,接着马上在Idea关机程序,触发优雅关机。接口不会正常返回,并且查看程序日志发现关机就是相差30秒,所以如果你的接口处理的逻辑超过了设置的优雅关机的时间,它是不会管你的,任然会直接关机,会导致你的接口业务没有处理完成。

在这里插入图片描述

2.4.2、GracefulShutdownListener 测试

1、在 GracefulShutdownListener 类里通过sleep来模拟业务

    @Slf4j
@Component
public class GracefulShutdownListener implements ApplicationListener<ContextClosedEvent> {


    /**
     * 优雅关机是否已开始
     */
    private boolean isGracefulShutdownStarted = false;

    @Override
    public void onApplicationEvent(ContextClosedEvent event) {
        // 执行你的业务逻辑
        log.info("Executing custom logic before graceful shutdown...");
        this.isGracefulShutdownStarted = true;

        try {
            Thread.sleep(60 * 1000);
        } catch (Exception e) {
            log.error("sleep:", e);
        }
        log.info("Executing custom logic before graceful shutdown,end!");
    }


    public boolean isGracefulShutdownStarted() {
        return isGracefulShutdownStarted;
    }
}

2、第一种情况:设置上述代码的睡眠时间是20秒后启动程序,接着马上在Idea关机程序,触发优雅关机。日志正常打印,并相差20秒,并且查看程序日志也是发现等待睡眠完成后才关机

在这里插入图片描述

3、第二种情况:设置上述代码的睡眠时间是50秒后启动程序,接着马上在Idea关机程序,触发优雅关机。日志正常打印,并相差50秒,并且查看程序日志也是发现等待睡眠完成后才关机,所以如果你的逻辑超过了设置的优雅关机的时间,它是会等你完成,再关机。

在这里插入图片描述

3、GracefulShutdownListener 的逻辑是,要关机了,发出信号给你,你可以先处理,比如, 你在 GracefulShutdownListener 代码里面业务处理了50秒,那它就等你50秒,直到你自己的业务处理完成即50秒之后,然后再开始spring.lifecycletimeout-per-shutdown-phase30秒的计时,去执行自己的关机业务逻辑,为什么那么说呢,因为,你可以在GracefulShutdownListener 种处理的50秒内,请求接口,是可以接受返回的,如下

在这里插入图片描述

三、docker 镜像中优雅关机的配置

默认情况下,docker stop 命令会发送一个 SIGTERM 信号给容器中的主进程(PID 1),并等待一段时间(默认为 10 秒)以允许该进程正常关闭。如果这段时间内进程没有退出,Docker 会发送一个 SIGKILL 信号强制终止进程

3.1、错误示例

1、❌ 先给出一个原始的简单的"sh", "-c",这种方式,是收不到关机信号的,这是因为docker stop命令默认发送的是SIGTERM信号给容器的PID 1进程(在这个例子中是sh脚本),而不是直接给Java进程。由于sh脚本并不具备转发信号的能力,因此Java应用无法接收到终止信号,从而不能触发优雅关闭流程

FROM openjdk:17-jdk-alpine
VOLUME /tmp
ADD multi-instance-test-0.0.1-SNAPSHOT.jar run.jar
RUN sh -c 'touch /run.jar'
ENV JAVA_OPTS="-Xms512m -Xmx512m -server"
ENV PROFILE="test"
ENV APP_NAME="run.jar"

ENTRYPOINT [ "sh", "-c", "java $JAVA_OPTS  -Djava.awt.headless=true -Duser.timezone=GMT+08 -Djava.security.egd=file:/dev/./urandom -jar /$APP_NAME --spring.profiles.active=test" ]

3.2、ENTRYPOINT 示例

2、修改上述的 ENTRYPOINT ,使用exec命令来替换当前进程(sh)为Java进程,这样Java进程就会成为PID 1,如下就可以收到信号了。

FROM openjdk:17-jdk-alpine
VOLUME /tmp
ADD multi-instance-test-0.0.1-SNAPSHOT.jar run.jar
RUN sh -c 'touch /run.jar'
ENV JAVA_OPTS="-Xms512m -Xmx512m -server"
ENV PROFILE="test"
ENV APP_NAME="run.jar"

# 使用exec命令来替换当前进程(sh)为Java进程,这样Java进程就会成为PID 1
ENTRYPOINT ["sh", "-c", "exec java $JAVA_OPTS -Djava.awt.headless=true -Duser.timezone=GMT+08 -Djava.security.egd=file:/dev/./urandom -jar /$APP_NAME --spring.profiles.active=$PROFILE"]

3.3、使用tini 作为 init 系统(推荐)

推荐理由:

  1. 信号转发与僵尸进程管理tini 不仅能确保信号(如 SIGTERM)被正确转发给子进程,还能处理僵尸进程,这使得它非常适合用于容器化应用。
  2. 简化配置:相比于直接操作 ENTRYPOINT 或编写复杂的启动脚本,使用 tini 更加简洁和易于维护。
  3. 兼容性和稳定性tini 是一个广泛使用的轻量级 init 系统,在许多 Docker 容器中都有成功应用的案例。

首先,在 Dockerfile 中添加安装 tini 的步骤,并调整 ENTRYPOINT 和 CMD 以使用 tini 启动 Java 应用。

FROM openjdk:17-jdk-alpine
VOLUME /tmp

# 安装 tini
RUN apk add --no-cache tini

ADD multi-instance-test-0.0.1-SNAPSHOT.jar run.jar
RUN sh -c 'touch /run.jar'
ENV JAVA_OPTS="-Xms512m -Xmx512m -server"
ENV PROFILE="test"
ENV APP_NAME="run.jar"

# 使用 tini 启动 Java 应用
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["sh", "-c", "java $JAVA_OPTS -Djava.awt.headless=true -Duser.timezone=GMT+08 -Djava.security.egd=file:/dev/./urandom -jar /$APP_NAME --spring.profiles.active=$PROFILE"]

3.4、其他注意事项

  • Spring Boot 版本:确保你使用的 Spring Boot 版本支持优雅停机功能(2.3 及以上版本默认支持)。如果你使用的是较早版本,请参考之前的建议来启用或自定义优雅停机逻辑。
  • 超时设置:默认情况下,docker stop 命令会等待 10 秒让应用程序关闭。如果需要更长的时间,可以通过 -t 参数指定等待时间。例如,docker stop -t 30 my-running-app 将等待 30 秒。

其中超时设置,我再详细说一下,如果你在Spring Boot应用中设置了spring.lifecycle.timeout-per-shutdown-phase: 30s,那么为了确保Docker有足够的时间让Spring Boot完成其优雅关闭过程,在使用docker stop命令时,你应该设置一个至少为30秒的超时时间(通过-t参数)。

3.4.1 、具体原因

  1. Spring Boot优雅关闭机制:当Spring Boot接收到SIGTERM信号后,它会开始执行优雅关闭流程。这个过程包括但不限于停止监听HTTP请求、等待当前正在处理的请求完成、释放资源等。你配置的timeout-per-shutdown-phase: 30s意味着Spring Boot希望在这30秒内完成所有必要的关闭操作。

  2. Docker的SIGTERM和SIGKILL行为

    • 当你执行docker stop <container_id>时,Docker会向容器内的主进程发送SIGTERM信号。
    • 默认情况下,Docker会等待10秒(可以通过-t参数自定义)来允许容器内的进程自行终止。
    • 如果在这个时间内进程没有退出,Docker将发送SIGKILL信号强制终止该进程。

因此,如果Docker的超时时间(默认或通过-t指定)小于Spring Boot所需的关闭时间(例如小于30秒),那么Spring Boot可能无法在Docker强制终止之前完成所有的关闭操作,导致部分关闭逻辑未被执行。

3.4.2、实践建议

为了确保Spring Boot应用能够顺利完成其优雅关闭流程,可以采取以下措施之一:

3.4.2.1、方法1:增加Docker的停止超时时间

当你使用docker stop命令时,指定一个至少为30秒的超时时间:

docker stop -t 30 <container_id>

这将给Spring Boot足够的时间来完成其关闭流程。

3.4.2.2、方法2:在启动容器时设置停止超时时间

你也可以在启动容器时通过--stop-timeout选项来设置超时时间:

docker run --stop-timeout 30 ...
3.4.2.3、方法3:调整Spring Boot的关闭超时时间

如果你发现30秒对于你的应用来说过长,可以考虑缩短Spring Boot的关闭超时时间。但是请注意,这需要确保在较短的时间内能够完成所有必要的关闭操作,否则可能会导致资源泄漏或其他问题。

3.4.2.4 示例

假设你已经配置了Spring Boot的关闭超时时间为30秒:

spring:
  lifecycle:
    timeout-per-shutdown-phase: 30s

那么在停止容器时,你应该使用如下命令:

docker stop -t 30 my-spring-boot-container

这样,Docker会给Spring Boot足够的时间(30秒)来完成其优雅关闭流程,避免因超时导致的强制终止。

只有日志显示了完整的优雅关闭的日志才是真的没问题

在这里插入图片描述

相关文章:

  • gbase8s数据库检测非法字符
  • VSCode 配置优化指南:打造高效的 uni-app、Vue2/3、JS/TS 开发环境
  • JS—组成:2分钟掌握什么是ECMAScript操作,什么是DOM操作,什么是BOM操作
  • 在Blender中给SP分纹理组
  • GStreamer —— 2.6、Windows下Qt加载GStreamer库后运行 - “教程6:媒体格式和Pad功能“(附:完整源码)
  • 基于Spring Boot的多级缓存架构实现
  • C++学习(十四)(使用库)
  • JVM详解
  • C# 开发工具Visual Studio下载和安装
  • vue3中子组件获取父组件的name,父组件不做修改动作
  • JavaScript中的主要知识点
  • Windows 图形显示驱动开发-WDDM 3.2-GPU-P 设备上的实时迁移(一)
  • 【vscode】一键编译运行c/c++程序
  • 深度解析 slabtop:实时监控内核缓存的利器
  • 短分享-Flink图构建
  • python里调用外部控制台应用程序的方法~
  • 【人工智能学习之局部极小值与鞍点】
  • 测试用例详解
  • BetaFlight源码解读01
  • MacOS Big Sur 11 新机安装brew wget python3.12 exo
  • 九江银行落地首单畜牧业转型金融业务,助推传统农业绿色智能
  • 盐城经济技术开发区党工委书记王旭东接受纪律审查和监察调查
  • 长三角体育节回归“上海时间”,首次发布赛事旅游推荐线路
  • 词条数量大幅扩充,《辞海》第八版启动编纂
  • 海昏侯博物馆展览上新,“西汉帝陵文化展”将持续展出3个月
  • 探秘多维魅力,长江经济带、珠三角媒体总编辑岳阳行启动