SpringBoot实现优雅停机的多种方式
作者:后端出路在何方
优雅停机在微服务架构中的角色
想象一下,你在一家餐厅用餐,突然服务员走过来说:“很抱歉,我得下班了,请您尽快结账离开。”你可能刚刚点了主菜,或许正在等着甜点,结果这个突如其来的通知让你感到非常不满。你的就餐体验被打断了,甚至你的账单也可能出现混乱。与此同时,餐厅的运营也可能因此变得不顺畅,服务员离开后,未完成的订单和客户的需求都可能无法及时得到处理,产生了不必要的麻烦。
在微服务架构中,这种情况的对应场景是应用程序的突发关闭。假设一个用户正在发起一个请求,这个请求正在进行中时,某个微服务却突然被强制关闭,或者在维护过程中突然停止。这就会导致用户的操作被中断,未处理的请求可能丢失,甚至产生数据不一致或错误的状态。
这种场景不仅会让用户体验变差,还可能给后端系统带来极大的问题。例如,用户信息可能未能成功保存到数据库中,或者订单状态没有被正确更新,最终造成数据的“脏”或不一致。为了避免这种问题,优雅停机机制在现代微服务系统中变得至关重要。它确保在服务关闭时,所有进行中的请求能够得到适当的处理,相关资源得以顺利释放,避免因应用程序的突然关闭而导致的问题。
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 Boot
的 shutdown
端点是禁用的。我们需要在 application.properties
或 application.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
容器平台:如果你的应用运行在
Kubernetes
或Docker
等容器平台上,平台通常会在删除容器时发送SIGTERM
信号,并等待一段时间让应用完成工作。
其他方法
在 Spring Boot 中实现优雅停机(Graceful Shutdown)除了通过 spring-boot-actuator
、ApplicationListener<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. 配合自定义线程池实现优雅停机
如果你的应用使用了自定义线程池,想确保所有线程在应用关闭时能够有序停止,可以通过设置线程池的 shutdown
或 shutdownNow
方法来实现。
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 秒
适用场景:
- 当应用部署在
Kubernetes
或Docker 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..."); // 执行清理操作,如关闭连接池、停止后台服务等 } }
适用场景:
- 如果你需要更灵活的优雅停机控制,可以使用自定义的信号处理器。
- 适用于需要处理各种不同信号(如
SIGTERM
、SIGINT
等)的复杂场景,特别是对于非 Spring 的基础设施组件。
9、小结
优雅停机是 Spring Boot
应用的重要特性,它帮助我们确保在关闭应用时能够平稳地释放资源,处理完正在进行的请求,从而提高系统的稳定性和可靠性。通过 Spring Boot Actuator、ApplicationListener
接口和 JVM 钩子函数等多种方式,我们可以确保应用程序能够安全、顺利地关闭,而不会影响用户体验或导致数据丢失。
除了 Spring Boot Actuator 和 ApplicationListener
外,还有多种方式可以实现优雅停机。每种方式有不同的适用场景:
@PreDestroy
和DisposableBean
:适用于简单的资源释放和清理操作。- 自定义线程池清理:适用于需要确保线程池任务完成的场景。
- 容器平台的优雅停机:适用于容器化应用,
Kubernetes
会自动管理服务的优雅停机。 - 自定义信号处理:适用于需要更灵活、底层控制的停机过程。
根据应用的需求,你可以选择合适的方式实现优雅停机,从而提高系统的可靠性和用户体验。
以上就是SpringBoot实现优雅停机的多种方式的详细内容,更多关于SpringBoot优雅停机的资料请关注脚本之家其它相关文章!