SpringBoot实现优雅停机的多种方式

 更新时间:2025年01月21日 11:09:52   作者:后端出路在何方  
优雅停机(Graceful Shutdown)在现代微服务架构中是非常重要的,它帮助我们确保在应用程序停止时,不会中断正在进行的请求或导致数据丢失,让我们以通俗易懂的方式来讲解这个概念以及如何在 Spring Boot 中实现它,需要的朋友可以参考下

优雅停机在微服务架构中的角色

想象一下,你在一家餐厅用餐,突然服务员走过来说:“很抱歉,我得下班了,请您尽快结账离开。”你可能刚刚点了主菜,或许正在等着甜点,结果这个突如其来的通知让你感到非常不满。你的就餐体验被打断了,甚至你的账单也可能出现混乱。与此同时,餐厅的运营也可能因此变得不顺畅,服务员离开后,未完成的订单和客户的需求都可能无法及时得到处理,产生了不必要的麻烦。

在微服务架构中,这种情况的对应场景是应用程序的突发关闭。假设一个用户正在发起一个请求,这个请求正在进行中时,某个微服务却突然被强制关闭,或者在维护过程中突然停止。这就会导致用户的操作被中断,未处理的请求可能丢失,甚至产生数据不一致或错误的状态。

这种场景不仅会让用户体验变差,还可能给后端系统带来极大的问题。例如,用户信息可能未能成功保存到数据库中,或者订单状态没有被正确更新,最终造成数据的“脏”或不一致。为了避免这种问题,优雅停机机制在现代微服务系统中变得至关重要。它确保在服务关闭时,所有进行中的请求能够得到适当的处理,相关资源得以顺利释放,避免因应用程序的突然关闭而导致的问题。

1、什么是优雅停机

简单来说,优雅停机就是指当我们需要关闭一个服务时,服务能够有序地完成当前的工作并停止。 具体来说,优雅停机包括以下几个步骤:

  • 停止接收新请求:当系统开始关闭时,需要通知负载均衡器或网关,告知它不要再将新的请求发送到即将下线的实例。
  • 处理正在进行的请求:对于已经到达并正在处理的请求,系统要给它们完成的机会,不会突然中断。
  • 释放资源:像数据库连接池、线程池等资源需要被安全释放,避免资源泄露。
  • 持久化临时数据:如果有必要,系统会保存当前的状态到数据库或文件,以便下次启动时可以恢复。

2、为什么需要优雅停机

  • 部署新版本时平稳过渡:当我们需要更新应用时,优雅停机可以让旧版本服务平稳关闭,避免突然的停机对用户造成影响。
  • 避免资源泄露:不管是内存、数据库连接,还是线程池资源,都需要在关闭时释放,否则就可能导致内存泄漏等问题。
  • 确保数据一致性:如果有正在处理的事务,优雅停机可以让这些事务有机会完成,避免数据丢失或者不一致。

3、优雅停机的实际应用场景

  • 服务更新: 在系统版本升级时,通过优雅停机完成请求处理和资源释放,避免对用户造成干扰。

  • 流量调控: 在高并发场景下,如果需要暂时下线部分服务节点,优雅停机可以帮助实现“无感”迁移。

  • 订单处理: 如出租车平台,在订单完成后再下线服务,避免出现“中途被抛弃”的情况。

4、优雅停机可能失效的情况

  • 强制关闭:使用 kill -9 强制终止进程将导致优雅停机机制无法触发。
  • 资源耗尽:系统资源不足可能导致清理操作无法完成。
  • 未配置超时:如果未配置超时时间,处理长时间任务可能导致停机时间过长。

5、如何在 Spring Boot 中实现优雅停机

Spring Boot 优雅停机的基础实现

  • Spring Boot 2.3 开始,优雅停机的支持更加简单和强大。通过设置 server.shutdown 配置,可以决定应用停机时的行为。

立即停机模式

  • 在立即停机模式下,应用会立刻中断所有请求和任务。
server:
  shutdown: immediate

虽然简单高效,但这种方式通常只适用于测试或无状态服务。

优雅停机模式

  • 在优雅停机模式下,Spring Boot 会等待当前的处理任务完成,再进行停机操作。
server:
  shutdown: graceful

注意: 该模式下的默认等待时间为 30 秒,可通过 spring.lifecycle.timeout-per-shutdown-phase 进行配置。

添加 spring-boot-starter-actuator 依赖

首先,需要确保你的项目中包含了 spring-boot-starter-actuator 依赖,这是启用 Spring Boot 内置监控和管理端点的工具包。

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

启用 shutdown 端点

默认情况下,Spring Bootshutdown 端点是禁用的。我们需要在 application.propertiesapplication.yml 中显式启用它。

对于 application.properties

management.endpoint.shutdown.enabled=true
management.endpoints.web.exposure.include=shutdown

或者,如果你使用 application.yml

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

触发优雅停机

配置好后,你可以通过发送 HTTP 请求来触发优雅停机。例如,使用 curl 命令:

curl -X POST http://localhost:8080/actuator/shutdown

当你调用这个端点时,Spring Boot 应用会停止接收新的请求,继续处理已经收到的请求,直到所有请求处理完毕后,应用才会退出。

6、通过 ApplicationListener 接口实现清理逻辑

为了在应用关闭时执行特定的清理操作(例如关闭数据库连接、释放资源等),你可以实现 ApplicationListener<ContextClosedEvent> 接口。

import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.stereotype.Component;

@Component
public class GracefulShutdownListener implements ApplicationListener<ContextClosedEvent> {
    @Override
    public void onApplicationEvent(ContextClosedEvent event) {
        // 执行必要的清理操作
        System.out.println("Starting graceful shutdown...");
        try {
            Thread.sleep(5000); // 模拟清理任务
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        System.out.println("Graceful shutdown completed.");
    }
}

在这个例子中,我们模拟了一个清理操作的过程(通过 Thread.sleep() 来等待)。实际上,你可以替换这部分逻辑,比如关闭数据库连接、释放文件句柄等。

7、使用 JVM 的钩子函数

Java 提供了 Runtime.addShutdownHook() 方法,可以注册一个线程,在 JVM 终止时执行清理任务。这个方法适用于一些更底层的清理操作,尤其是在某些情况下,ApplicationListener 可能没有机会被触发时。

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class MainApp {

    public static void main(String[] args) {
        // 注册关闭钩子
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            System.out.println("JVM is shutting down, executing cleanup...");
            try {
                Thread.sleep(5000); // 模拟清理任务
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            System.out.println("Cleanup completed.");
        }));

        SpringApplication.run(MainApp.class, args);
    }
}

需要注意的是,JVM 关闭钩子并不总是能执行,尤其是在遇到强制停止(如 kill -9)时。因此,它应该作为一种补充机制,而不是唯一的保证。

8、触发优雅停机的方式

除了通过 /actuator/shutdown 端点来触发优雅停机外,还有一些常见的方法:

  • SIGTERM 信号:通过发送 SIGTERM 信号(例如使用 kill 命令)可以触发 JVM 的正常退出流程。

kill <pid>

这里的 <pid> 是应用的进程 ID。

kill -9 pid可以模拟了一次系统宕机,系统断电等极端情况,而kill -15 pid则是等待应用关闭,执行阻塞操作,有时候也会出现无法关闭应用的情况

#查看jvm进程pid
jps
#列出所有信号名称
kill -l

# Windows下信号常量值
# 简称  全称    数值 
# INT   SIGINT     2       Ctrl+C中断
# ILL   SIGILL     4       非法指令
# FPE   SIGFPE     8       floating point exception(浮点异常)
# SEGV  SIGSEGV    11      segment violation(段错误)
# TERM  SIGTERM    5       Software termination signal from kill(Kill发出的软件终止)
# BREAK SIGBREAK   21      Ctrl-Break sequence(Ctrl+Break中断)
# ABRT  SIGABRT    22      abnormal termination triggered by abort call(Abort)

#linux信号常量值
# 简称  全称  数值  
# HUP   SIGHUP      1    终端断线  
# INT   SIGINT      2    中断(同 Ctrl + C)        
# QUIT  SIGQUIT     3    退出(同 Ctrl + \)         
# KILL  SIGKILL     9    强制终止         
# TERM  SIGTERM     15    终止         
# CONT  SIGCONT     18    继续(与STOP相反, fg/bg命令)         
# STOP  SIGSTOP     19    暂停(同 Ctrl + Z)        
#....

#可以理解为操作系统从内核级别强行杀死某个进程
kill -9 pid 
#理解为发送一个通知,等待应用主动关闭
kill -15 pid
#也支持信号常量值全称或简写(就是去掉SIG后)
kill -l KILL

JVM 工具:Java 提供了一些工具(如 jcmd 和 jconsole)可以用来控制 JVM 的生命周期。例如,使用 jcmd 来发送退出命令:

jcmd <pid> VM.exit
  • 容器平台:如果你的应用运行在 KubernetesDocker 等容器平台上,平台通常会在删除容器时发送 SIGTERM 信号,并等待一段时间让应用完成工作。

其他方法

在 Spring Boot 中实现优雅停机(Graceful Shutdown)除了通过 spring-boot-actuatorApplicationListener<ContextClosedEvent>JVM 钩子等方式外,实际上还有一些其他方法可以帮助我们实现优雅停机。以下是几种不同的实现方式,并配合实际应用场景和代码示例。

1. 使用 @PreDestroy 注解

@PreDestroy 注解是 Java EE 中的一种注解,用来在 Bean 销毁之前执行清理任务。Spring 也支持这个注解,当 Spring 容器关闭时,所有带有 @PreDestroy 注解的方法都会被调用。它通常用于执行资源的释放操作,如关闭数据库连接池、清理缓存等。

代码示例:

import javax.annotation.PreDestroy;
import org.springframework.stereotype.Component;

@Component
public class GracefulShutdownService {

    @PreDestroy
    public void onShutdown() {
        System.out.println("Performing cleanup before shutdown...");
        // 在此处执行资源清理,如关闭数据库连接、释放线程池等
    }
}

适用场景:

  • 适用于需要在应用关闭时清理资源的场景,尤其是在资源管理方面(例如关闭连接池、清理缓存等)。
  • Web 应用中,通常用来释放与外部系统(如数据库、消息队列等)的连接。

2. 使用 DisposableBean 接口

Spring 提供了 DisposableBean 接口,用于定义 Bean 在销毁时需要执行的清理操作。它的 destroy() 方法会在 Spring 容器关闭时被自动调用。

代码示例:

启用 Shutdown Hook

Spring Boot 默认会通过 JVM 的 Shutdown Hook 触发优雅停机。确保以下配置启用:

spring:
  main:
    register-shutdown-hook: true

自定义资源释放逻辑

如果需要在停机时执行特定的清理操作,比如关闭数据库连接或停止线程池,可以通过添加 Shutdown Hook 或实现 DisposableBean 接口。

import org.springframework.beans.factory.DisposableBean;
import org.springframework.stereotype.Component;

@Component
public class GracefulShutdownService implements DisposableBean {

    @Override
    public void destroy() throws Exception {
        System.out.println("Graceful shutdown - Cleaning up resources...");
        // 执行清理操作
    }
}

或者直接通过 JVM 钩子实现:

Runtime.getRuntime().addShutdownHook(new Thread(() -> {
    System.out.println("执行自定义的资源清理逻辑");
}));

超时机制

避免因某些请求耗时过长导致系统停机过程被阻塞,可以通过以下配置设置超时时间:

spring:
  lifecycle:
    timeout-per-shutdown-phase: 20s # 默认30秒

适用场景:

  • @PreDestroy 类似,但 DisposableBean 提供了一种更明确的方式来处理 Spring 容器中的 Bean 销毁。
  • 适用于需要进行自定义清理操作(如关闭连接池、停止后台线程等)的场景。

3. 配合自定义线程池实现优雅停机

如果你的应用使用了自定义线程池,想确保所有线程在应用关闭时能够有序停止,可以通过设置线程池的 shutdownshutdownNow 方法来实现。

Spring Boot 提供了多种方式来创建和配置线程池,如 TaskExecutor@Async 等。如果应用在停止时需要等待线程池中的任务完成,可以通过以下方式进行配置。

代码示例:

import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Component;

@Component
public class GracefulShutdownListener implements ApplicationListener<ContextClosedEvent> {

    private final ThreadPoolTaskExecutor taskExecutor;

    public GracefulShutdownListener(ThreadPoolTaskExecutor taskExecutor) {
        this.taskExecutor = taskExecutor;
    }

    @Override
    public void onApplicationEvent(ContextClosedEvent event) {
        System.out.println("Shutting down thread pool...");
        
        // 给当前线程池中的任务一些时间来完成
        taskExecutor.shutdown();
        
        try {
            // 等待任务完成
            if (!taskExecutor.getThreadPoolExecutor().awaitTermination(60, TimeUnit.SECONDS)) {
                System.out.println("Timeout reached, forcing shutdown...");
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

适用场景:

  • 适用于你的应用需要执行异步任务时,并希望确保这些任务能够有序地完成,防止强制中断。
  • 适用于后台线程池或异步任务系统,确保应用关闭时,任务能够平稳终止。

4. 在 Kubernetes 中实现优雅停机

如果你将 Spring Boot 应用部署在容器编排平台(如 Kubernetes)上,Kubernetes 会自动帮助你实现优雅停机。Kubernetes 发送 SIGTERM 信号,并等待容器停止一定时间(通常是 30 秒),在这段时间内,应用应当完成正在进行的请求并清理资源。

你可以通过设置 terminationGracePeriodSeconds 来配置应用在收到 SIGTERM 信号后的最大优雅停机时间。

配置示例(Kubernetes):

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
spec:
  replicas: 1
  template:
    spec:
      containers:
        - name: my-app
          image: my-app-image
          ports:
            - containerPort: 8080
      terminationGracePeriodSeconds: 60  # 等待 60 秒

适用场景:

  • 当应用部署在 KubernetesDocker Swarm 等容器平台时,这种方式能够自动触发优雅停机,配合应用的优雅停机策略(如通过 Actuator 触发 shutdown)。
  • 适用于容器化部署和云原生应用,能够与平台的生命周期管理机制配合。

5. 使用自定义 shutdown 信号处理器

如果你不想完全依赖 Spring 提供的机制,可以实现一个自定义的信号处理器来捕获和响应关闭信号。通过这种方式,你可以更精细地控制停机流程。

代码示例:

import org.springframework.stereotype.Component;

import java.io.IOException;

@Component
public class ShutdownSignalHandler {

    public ShutdownSignalHandler() throws IOException {
        // 注册 SIGTERM 信号处理器
        Runtime.getRuntime().addShutdownHook(new Thread(this::shutdown));
    }

    private void shutdown() {
        System.out.println("Received shutdown signal. Performing graceful shutdown...");
        // 执行清理操作,如关闭连接池、停止后台服务等
    }
}

适用场景:

  • 如果你需要更灵活的优雅停机控制,可以使用自定义的信号处理器。
  • 适用于需要处理各种不同信号(如 SIGTERMSIGINT 等)的复杂场景,特别是对于非 Spring 的基础设施组件。

9、小结

优雅停机是 Spring Boot 应用的重要特性,它帮助我们确保在关闭应用时能够平稳地释放资源,处理完正在进行的请求,从而提高系统的稳定性和可靠性。通过 Spring Boot Actuator、ApplicationListener 接口和 JVM 钩子函数等多种方式,我们可以确保应用程序能够安全、顺利地关闭,而不会影响用户体验或导致数据丢失。

除了 Spring Boot Actuator 和 ApplicationListener 外,还有多种方式可以实现优雅停机。每种方式有不同的适用场景:

  • @PreDestroyDisposableBean:适用于简单的资源释放和清理操作。
  • 自定义线程池清理:适用于需要确保线程池任务完成的场景。
  • 容器平台的优雅停机:适用于容器化应用,Kubernetes 会自动管理服务的优雅停机。
  • 自定义信号处理:适用于需要更灵活、底层控制的停机过程。

根据应用的需求,你可以选择合适的方式实现优雅停机,从而提高系统的可靠性和用户体验。

以上就是SpringBoot实现优雅停机的多种方式的详细内容,更多关于SpringBoot优雅停机的资料请关注脚本之家其它相关文章!

相关文章

  • Java面试题冲刺第二十六天--实战编程2

    Java面试题冲刺第二十六天--实战编程2

    这篇文章主要为大家分享了最有价值的三道java实战编程的面试题,涵盖内容全面,包括数据结构和算法相关的题目、经典面试编程题等,感兴趣的小伙伴们可以参考一下
    2021-08-08
  • 解决Mybatis中foreach嵌套使用if标签对象取值的问题

    解决Mybatis中foreach嵌套使用if标签对象取值的问题

    这篇文章主要介绍了解决Mybatis中foreach嵌套使用if标签对象取值的问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-02-02
  • 全面了解servlet中cookie的使用方法

    全面了解servlet中cookie的使用方法

    下面小编就为大家带来一篇全面了解servlet中cookie的使用方法。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2016-06-06
  • 在CentOS上配置Java环境变量的教程

    在CentOS上配置Java环境变量的教程

    这篇文章主要介绍了在CentOS上配置Java环境变量的教程,同时适用于Fedora等其他RedHat系的Linux系统,需要的朋友可以参考下
    2015-06-06
  • Java如何判断一个空对象的常见方法

    Java如何判断一个空对象的常见方法

    在Java中判断对象是否为空是一项重要的编程技巧,可以有效防止空指针异常的发生,下面这篇文章主要给大家介绍了关于利用Java如何判断一个空对象的相关资料,需要的朋友可以参考下
    2024-01-01
  • Mybatis-Plus根据自定义注解实现自动加解密的示例代码

    Mybatis-Plus根据自定义注解实现自动加解密的示例代码

    我们把数据存到数据库的时候,有些敏感字段是需要加密的,从数据库查出来再进行解密,如果我们使用的是Mybatis框架,那就跟着一起探索下如何使用框架的拦截器功能实现自动加解密吧,需要的朋友可以参考下
    2024-06-06
  • 解决springboot项目找不到resources目录下的资源问题

    解决springboot项目找不到resources目录下的资源问题

    这篇文章主要介绍了解决springboot项目找不到resources目录下的资源问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-08-08
  • idea使用带provide修饰依赖导致ClassNotFound

    idea使用带provide修饰依赖导致ClassNotFound

    程序打包到Linux上运行时,若Linux上也有这些依赖,为了在Linux上运行时避免依赖冲突,可以使用provide修饰,本文主要介绍了idea使用带provide修饰依赖导致ClassNotFound,下面就来介绍一下解决方法,感兴趣的可以了解一下
    2024-01-01
  • Java实现PDF转Word的示例代码(无水印无页数限制)

    Java实现PDF转Word的示例代码(无水印无页数限制)

    这篇文章主要为大家详细介绍了如何利用Java语言实现PDF转Word文件的效果,并可以无水印、无页数限制。文中的示例代码讲解详细,需要的可以参考一下
    2022-05-05
  • 下载远程maven仓库的jar 手动放到本地仓库详细操作

    下载远程maven仓库的jar 手动放到本地仓库详细操作

    这篇文章主要介绍了如何下载远程maven仓库的jar 手动放到本地仓库,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2023-03-03

最新评论