SpringBoot實(shí)現(xiàn)優(yōu)雅停機(jī)的三種方式
引言
應(yīng)用的啟停是一個(gè)常見(jiàn)操作。然而,突然終止一個(gè)正在運(yùn)行的應(yīng)用可能導(dǎo)致正在處理的請(qǐng)求失敗、數(shù)據(jù)不一致等問(wèn)題。優(yōu)雅停機(jī)(Graceful Shutdown)是指應(yīng)用在接收到停止信號(hào)后,能夠妥善處理現(xiàn)有請(qǐng)求、釋放資源,然后再退出的過(guò)程。本文將詳細(xì)介紹SpringBoot中實(shí)現(xiàn)優(yōu)雅停機(jī)的三種方式。
什么是優(yōu)雅停機(jī)?
優(yōu)雅停機(jī)指的是應(yīng)用程序在收到停止信號(hào)后,不是立即終止,而是遵循以下步驟有序退出:
- 停止接收新的請(qǐng)求
- 等待正在處理的請(qǐng)求完成
- 關(guān)閉各種資源連接(數(shù)據(jù)庫(kù)連接池、線程池、消息隊(duì)列連接等)
- 完成必要的清理工作
- 最后退出應(yīng)用
優(yōu)雅停機(jī)的核心價(jià)值在于:
- 提高用戶(hù)體驗(yàn),避免請(qǐng)求突然中斷
- 保障數(shù)據(jù)一致性,防止數(shù)據(jù)丟失
方式一:SpringBoot內(nèi)置的優(yōu)雅停機(jī)支持
原理與支持版本
從Spring Boot 2.3版本開(kāi)始,框架原生支持優(yōu)雅停機(jī)機(jī)制。這是最簡(jiǎn)單且官方推薦的實(shí)現(xiàn)方式。
當(dāng)應(yīng)用接收到停止信號(hào)(如SIGTERM)時(shí),內(nèi)嵌的Web服務(wù)器(如Tomcat、Jetty或Undertow)會(huì)執(zhí)行以下步驟:
- 停止接收新的連接請(qǐng)求
- 設(shè)置現(xiàn)有連接的keepalive為false
- 等待所有活躍請(qǐng)求處理完成或超時(shí)
- 關(guān)閉應(yīng)用上下文和相關(guān)資源
配置方法
在application.properties或application.yml中添加簡(jiǎn)單配置即可啟用:
server:
shutdown: graceful
spring:
lifecycle:
timeout-per-shutdown-phase: 30s
這里的timeout-per-shutdown-phase指定了等待活躍請(qǐng)求完成的最大時(shí)間,默認(rèn)為30秒。
實(shí)現(xiàn)示例
下面是一個(gè)完整的SpringBoot應(yīng)用示例,啟用了優(yōu)雅停機(jī):
@SpringBootApplication
public class GracefulShutdownApplication {
private static final Logger logger = LoggerFactory.getLogger(GracefulShutdownApplication.class);
public static void main(String[] args) {
SpringApplication.run(GracefulShutdownApplication.class, args);
logger.info("Application started");
}
@RestController
@RequestMapping("/api")
static class SampleController {
@GetMapping("/quick")
public String quickRequest() {
return "Quick response";
}
@GetMapping("/slow")
public String slowRequest() throws InterruptedException {
// 模擬長(zhǎng)時(shí)間處理的請(qǐng)求
logger.info("Start processing slow request");
Thread.sleep(10000); // 10秒
logger.info("Finished processing slow request");
return "Slow response completed";
}
}
@Bean
public ApplicationListener<ContextClosedEvent> contextClosedEventListener() {
return event -> logger.info("Received spring context closed event - shutting down");
}
}
測(cè)試驗(yàn)證
- 啟動(dòng)應(yīng)用
- 發(fā)起一個(gè)長(zhǎng)時(shí)間運(yùn)行的請(qǐng)求:
curl http://localhost:8080/api/slow - 在處理過(guò)程中,向應(yīng)用發(fā)送SIGTERM信號(hào):
kill -15 <進(jìn)程ID>,如果是IDEA開(kāi)發(fā)環(huán)境,可以點(diǎn)擊一次停止服務(wù)按鈕 - 觀察日志輸出:應(yīng)該能看到應(yīng)用等待長(zhǎng)請(qǐng)求處理完成后才關(guān)閉
優(yōu)缺點(diǎn)
優(yōu)點(diǎn):
- 配置簡(jiǎn)單,官方原生支持
- 無(wú)需額外代碼,維護(hù)成本低
- 適用于大多數(shù)Web應(yīng)用場(chǎng)景
- 與Spring生命周期事件完美集成
缺點(diǎn):
- 僅支持Spring Boot 2.3+版本
- 對(duì)于超出HTTP請(qǐng)求的場(chǎng)景(如長(zhǎng)時(shí)間運(yùn)行的作業(yè))需要額外處理
- 靈活性相對(duì)較低,無(wú)法精細(xì)控制停機(jī)流程
- 只能設(shè)置統(tǒng)一的超時(shí)時(shí)間
適用場(chǎng)景
- Spring Boot 2.3+版本的Web應(yīng)用
- 請(qǐng)求處理時(shí)間可預(yù)期,不會(huì)有超長(zhǎng)時(shí)間運(yùn)行的請(qǐng)求
- 微服務(wù)架構(gòu)中的標(biāo)準(zhǔn)服務(wù)
方式二:使用Actuator端點(diǎn)實(shí)現(xiàn)優(yōu)雅停機(jī)
原理與實(shí)現(xiàn)
Spring Boot Actuator提供了豐富的運(yùn)維端點(diǎn),其中包括shutdown端點(diǎn),可用于觸發(fā)應(yīng)用的優(yōu)雅停機(jī)。這種方式的獨(dú)特之處在于它允許通過(guò)HTTP請(qǐng)求觸發(fā)停機(jī)過(guò)程,適合需要遠(yuǎn)程操作的場(chǎng)景。
配置步驟
- 添加Actuator依賴(lài):
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
- 啟用并暴露shutdown端點(diǎn):
management:
endpoint:
shutdown:
enabled: true
endpoints:
web:
exposure:
include: shutdown
base-path: /management
server:
port: 8081 # 可選:為管理端點(diǎn)設(shè)置單獨(dú)端口
使用方法
通過(guò)HTTP POST請(qǐng)求觸發(fā)停機(jī):
curl -X POST http://localhost:8081/management/shutdown
請(qǐng)求成功后,會(huì)返回類(lèi)似如下響應(yīng):
{
"message": "Shutting down, bye..."
}
安全性考慮
由于shutdown是一個(gè)敏感操作,必須考慮安全性:
spring:
security:
user:
name: admin
password: secure_password
roles: ACTUATOR
management:
endpoints:
web:
exposure:
include: shutdown
endpoint:
shutdown:
enabled: true
# 配置端點(diǎn)安全
management.endpoints.web.base-path: /management
使用安全配置后的訪問(wèn)方式:
curl -X POST http://admin:secure_password@localhost:8080/management/shutdown
完整實(shí)現(xiàn)示例
@SpringBootApplication
@EnableWebSecurity
public class ActuatorShutdownApplication {
private static final Logger logger = LoggerFactory.getLogger(ActuatorShutdownApplication.class);
public static void main(String[] args) {
SpringApplication.run(ActuatorShutdownApplication.class, args);
logger.info("Application started with Actuator shutdown enabled");
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeHttpRequests()
.requestMatchers("/management/**").hasRole("ACTUATOR")
.anyRequest().permitAll()
.and()
.httpBasic();
return http.build();
}
@RestController
static class ApiController {
@GetMapping("/api/hello")
public String hello() {
return "Hello, world!";
}
}
@Bean
public ApplicationListener<ContextClosedEvent> shutdownListener() {
return event -> {
logger.info("Received shutdown signal via Actuator");
// 等待活躍請(qǐng)求完成
logger.info("Waiting for active requests to complete...");
try {
Thread.sleep(5000); // 簡(jiǎn)化示例,實(shí)際應(yīng)監(jiān)控活躍請(qǐng)求
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
logger.info("All requests completed, shutting down");
};
}
}
優(yōu)缺點(diǎn)
優(yōu)點(diǎn):
- 可以通過(guò)HTTP請(qǐng)求遠(yuǎn)程觸發(fā)停機(jī)
- 適合管理工具和運(yùn)維腳本集成
- 可以與Spring Security集成實(shí)現(xiàn)訪問(wèn)控制
- 支持所有Spring Boot版本(包括2.3之前的版本)
缺點(diǎn):
- 需要額外配置和依賴(lài)
- 潛在的安全風(fēng)險(xiǎn),需謹(jǐn)慎保護(hù)端點(diǎn)
- 對(duì)于內(nèi)部復(fù)雜資源的關(guān)閉需要額外編碼
適用場(chǎng)景
- 需要通過(guò)HTTP請(qǐng)求觸發(fā)停機(jī)的場(chǎng)景
- 使用運(yùn)維自動(dòng)化工具管理應(yīng)用的部署
- 集群環(huán)境中需要按特定順序停止服務(wù)
- 內(nèi)部管理系統(tǒng)需要直接控制應(yīng)用生命周期
方式三:自定義ShutdownHook實(shí)現(xiàn)優(yōu)雅停機(jī)
原理與實(shí)現(xiàn)
JVM提供了ShutdownHook機(jī)制,允許在JVM關(guān)閉前執(zhí)行自定義邏輯。通過(guò)注冊(cè)自定義的ShutdownHook,我們可以實(shí)現(xiàn)更加精細(xì)和靈活的優(yōu)雅停機(jī)控制。這種方式的優(yōu)勢(shì)在于可以精確控制資源釋放順序,適合有復(fù)雜資源管理需求的應(yīng)用。
基本實(shí)現(xiàn)步驟
- 創(chuàng)建自定義的ShutdownHandler類(lèi)
- 注冊(cè)JVM ShutdownHook
- 在Hook中實(shí)現(xiàn)自定義的優(yōu)雅停機(jī)邏輯
完整實(shí)現(xiàn)示例
以下是一個(gè)包含詳細(xì)注釋的完整示例:
@SpringBootApplication
public class CustomShutdownHookApplication {
private static final Logger logger = LoggerFactory.getLogger(CustomShutdownHookApplication.class);
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(CustomShutdownHookApplication.class, args);
// 注冊(cè)自定義ShutdownHook
registerShutdownHook(context);
logger.info("Application started with custom shutdown hook");
}
private static void registerShutdownHook(ConfigurableApplicationContext context) {
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
logger.info("Executing custom shutdown hook");
try {
// 1. 停止接收新請(qǐng)求(如果是Web應(yīng)用)
if (context.containsBean("tomcatServletWebServerFactory")) {
TomcatServletWebServerFactory server = context.getBean(TomcatServletWebServerFactory.class);
logger.info("Stopping web server to reject new requests");
// 注意: 實(shí)際應(yīng)用中需要找到正確方式停止特定Web服務(wù)器
}
// 2. 等待活躍請(qǐng)求處理完成
logger.info("Waiting for active requests to complete");
// 這里可以添加自定義等待邏輯,如檢查活躍連接數(shù)或線程狀態(tài)
Thread.sleep(5000); // 簡(jiǎn)化示例
// 3. 關(guān)閉自定義線程池
shutdownThreadPools(context);
// 4. 關(guān)閉消息隊(duì)列連接
closeMessageQueueConnections(context);
// 5. 關(guān)閉數(shù)據(jù)庫(kù)連接池
closeDataSourceConnections(context);
// 6. 執(zhí)行其他自定義清理邏輯
performCustomCleanup(context);
// 7. 最后關(guān)閉Spring上下文
logger.info("Closing Spring application context");
context.close();
logger.info("Graceful shutdown completed");
} catch (Exception e) {
logger.error("Error during graceful shutdown", e);
}
}, "GracefulShutdownHook"));
}
private static void shutdownThreadPools(ApplicationContext context) {
logger.info("Shutting down thread pools");
// 獲取所有ExecutorService類(lèi)型的Bean
Map<String, ExecutorService> executors = context.getBeansOfType(ExecutorService.class);
executors.forEach((name, executor) -> {
logger.info("Shutting down executor: {}", name);
executor.shutdown();
try {
// 等待任務(wù)完成
if (!executor.awaitTermination(15, TimeUnit.SECONDS)) {
logger.warn("Executor did not terminate in time, forcing shutdown: {}", name);
executor.shutdownNow();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
logger.warn("Interrupted while waiting for executor shutdown: {}", name);
executor.shutdownNow();
}
});
}
private static void closeMessageQueueConnections(ApplicationContext context) {
logger.info("Closing message queue connections");
// 示例:關(guān)閉RabbitMQ連接
if (context.containsBean("rabbitConnectionFactory")) {
try {
ConnectionFactory rabbitFactory = context.getBean(ConnectionFactory.class);
// 適當(dāng)?shù)仃P(guān)閉連接
logger.info("Closed RabbitMQ connections");
} catch (Exception e) {
logger.error("Error closing RabbitMQ connections", e);
}
}
// 示例:關(guān)閉Kafka連接
if (context.containsBean("kafkaConsumerFactory")) {
try {
// 關(guān)閉Kafka連接的代碼
logger.info("Closed Kafka connections");
} catch (Exception e) {
logger.error("Error closing Kafka connections", e);
}
}
}
private static void closeDataSourceConnections(ApplicationContext context) {
logger.info("Closing datasource connections");
// 獲取所有DataSource類(lèi)型的Bean
Map<String, DataSource> dataSources = context.getBeansOfType(DataSource.class);
dataSources.forEach((name, dataSource) -> {
try {
// 對(duì)于HikariCP連接池
if (dataSource instanceof HikariDataSource) {
((HikariDataSource) dataSource).close();
logger.info("Closed HikariCP datasource: {}", name);
}
// 可以添加其他類(lèi)型連接池的關(guān)閉邏輯
else {
// 嘗試通過(guò)反射調(diào)用close方法
Method closeMethod = dataSource.getClass().getMethod("close");
closeMethod.invoke(dataSource);
logger.info("Closed datasource: {}", name);
}
} catch (Exception e) {
logger.error("Error closing datasource: {}", name, e);
}
});
}
private static void performCustomCleanup(ApplicationContext context) {
// 這里可以添加應(yīng)用特有的清理邏輯
logger.info("Performing custom cleanup tasks");
// 例如:保存應(yīng)用狀態(tài)
// 例如:釋放本地資源
// 例如:發(fā)送關(guān)閉通知給其他系統(tǒng)
}
@Bean
public ExecutorService applicationTaskExecutor() {
return Executors.newFixedThreadPool(10);
}
@RestController
@RequestMapping("/api")
static class ApiController {
@Autowired
private ExecutorService applicationTaskExecutor;
@GetMapping("/task")
public String submitTask() {
applicationTaskExecutor.submit(() -> {
try {
logger.info("Task started, will run for 30 seconds");
Thread.sleep(30000);
logger.info("Task completed");
} catch (InterruptedException e) {
logger.info("Task interrupted");
Thread.currentThread().interrupt();
}
});
return "Task submitted";
}
}
}
優(yōu)缺點(diǎn)
優(yōu)點(diǎn):
- 最大的靈活性和可定制性
- 可以精確控制資源關(guān)閉順序和方式
- 適用于復(fù)雜應(yīng)用場(chǎng)景和所有Spring Boot版本
- 可以處理Spring框架無(wú)法管理的外部資源
缺點(diǎn):
- 實(shí)現(xiàn)復(fù)雜度高,需要詳細(xì)了解應(yīng)用資源
- 維護(hù)成本較高
- 容易出現(xiàn)資源關(guān)閉順序錯(cuò)誤導(dǎo)致的問(wèn)題
適用場(chǎng)景
- 具有復(fù)雜資源管理需求的應(yīng)用
- 需要特定順序關(guān)閉資源的場(chǎng)景
- 使用Spring Boot早期版本(不支持內(nèi)置優(yōu)雅停機(jī))
- 非Web應(yīng)用或混合應(yīng)用架構(gòu)
- 使用了Spring框架不直接管理的資源(如Native資源)
方案對(duì)比和選擇指南
下面是三種方案的對(duì)比表格,幫助您選擇最適合自己場(chǎng)景的實(shí)現(xiàn)方式:
| 特性/方案 | SpringBoot內(nèi)置 | Actuator端點(diǎn) | 自定義ShutdownHook |
|---|---|---|---|
| 實(shí)現(xiàn)復(fù)雜度 | 低 | 中 | 高 |
| 靈活性 | 低 | 中 | 高 |
| 可定制性 | 低 | 中 | 高 |
| 框架依賴(lài) | Spring Boot 2.3+ | 任何Spring Boot版本 | 任何Java應(yīng)用 |
| 額外依賴(lài) | 無(wú) | Actuator | 無(wú) |
| 觸發(fā)方式 | 系統(tǒng)信號(hào)(SIGTERM) | HTTP請(qǐng)求 | 系統(tǒng)信號(hào)或自定義 |
| 安全性考慮 | 低 | 高(需要保護(hù)端點(diǎn)) | 中 |
| 維護(hù)成本 | 低 | 中 | 高 |
| 適用Web應(yīng)用 | 最適合 | 適合 | 適合 |
| 適用非Web應(yīng)用 | 部分適合 | 部分適合 | 最適合 |
選擇SpringBoot內(nèi)置方案,如果:
- 使用Spring Boot 2.3+版本
- 主要是標(biāo)準(zhǔn)Web應(yīng)用
- 沒(méi)有特殊的資源管理需求
- 希望最簡(jiǎn)單的配置
選擇Actuator端點(diǎn)方案,如果:
- 需要通過(guò)HTTP請(qǐng)求觸發(fā)停機(jī)
- 使用早期Spring Boot版本
- 集成了運(yùn)維自動(dòng)化工具
- 已經(jīng)在使用Actuator進(jìn)行監(jiān)控
選擇自定義ShutdownHook方案,如果:
- 有復(fù)雜的資源管理需求
- 需要精確控制停機(jī)流程
- 使用了Spring框架不直接管理的資源
- 混合架構(gòu)應(yīng)用(既有Web又有后臺(tái)作業(yè))
結(jié)論
優(yōu)雅停機(jī)是保障應(yīng)用可靠性和用戶(hù)體驗(yàn)的重要實(shí)踐。SpringBoot提供了多種實(shí)現(xiàn)方式,從簡(jiǎn)單的配置到復(fù)雜的自定義實(shí)現(xiàn),可以滿足不同應(yīng)用場(chǎng)景的需求。
- 對(duì)于簡(jiǎn)單應(yīng)用:SpringBoot內(nèi)置方案是最佳選擇,配置簡(jiǎn)單,足夠滿足大多數(shù)Web應(yīng)用需求
- 對(duì)于需要遠(yuǎn)程觸發(fā)的場(chǎng)景:Actuator端點(diǎn)提供了HTTP接口控制,便于集成運(yùn)維系統(tǒng)
- 對(duì)于復(fù)雜應(yīng)用:自定義ShutdownHook提供了最大的靈活性,可以精確控制資源釋放順序和方式
無(wú)論選擇哪種方式,優(yōu)雅停機(jī)都應(yīng)該成為微服務(wù)設(shè)計(jì)的標(biāo)準(zhǔn)實(shí)踐。正確實(shí)現(xiàn)優(yōu)雅停機(jī),不僅能提升系統(tǒng)穩(wěn)定性,還能改善用戶(hù)體驗(yàn),減少因應(yīng)用重啟或降級(jí)帶來(lái)的業(yè)務(wù)中斷。
以上就是SpringBoot實(shí)現(xiàn)優(yōu)雅停機(jī)的三種方式的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot優(yōu)雅停機(jī)方式的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
最新IntelliJ IDEA 2021版配置 Tomcat 8.5 的詳細(xì)步驟
idea開(kāi)發(fā)工具一直是java環(huán)境最好用,很受廣大開(kāi)發(fā)者喜愛(ài),今天通過(guò)本文給大家分享最新IntelliJ IDEA 2021版配置 Tomcat 8.5 的詳細(xì)步驟,本文通過(guò)圖文并茂的形式給大家介紹的非常詳細(xì),需要的朋友可以參考下2021-06-06
springboot讀取自定義配置文件時(shí)出現(xiàn)亂碼解決方案
這篇文章主要介紹了springboot讀取自定義配置文件時(shí)出現(xiàn)亂碼解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-11-11
MybatisPlus調(diào)用原生SQL的三種方法實(shí)例詳解
這篇文章主要介紹了MybatisPlus調(diào)用原生SQL的三種方法,在有些情況下需要用到MybatisPlus查詢(xún)?cè)鶶QL,MybatisPlus其實(shí)帶有運(yùn)行原生SQL的方法,我這里列舉三種,需要的朋友可以參考下2022-09-09
Java微信公眾平臺(tái)開(kāi)發(fā)(3) 接收消息的分類(lèi)及實(shí)體的創(chuàng)建
這篇文章主要為大家詳細(xì)介紹了Java微信公眾平臺(tái)開(kāi)發(fā)第三步,接收消息的分類(lèi)及實(shí)體的創(chuàng)建,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-04-04
如何使用IntelliJ?IDEA進(jìn)行Java內(nèi)存分析詳解
這篇文章主要介紹了如何使用IntelliJ?IDEA進(jìn)行Java內(nèi)存分析的相關(guān)資料,包括內(nèi)存消耗情況的查看、內(nèi)存泄漏的分析、垃圾回收情況的優(yōu)化等,通過(guò)IntelliJIDEA提供的工具,開(kāi)發(fā)者可以有效地提升應(yīng)用程序的性能和穩(wěn)定性,需要的朋友可以參考下2025-05-05
基于Java實(shí)現(xiàn)簡(jiǎn)單的身材計(jì)算程序
這篇文章主要為大家詳細(xì)介紹了如何利用Java實(shí)現(xiàn)簡(jiǎn)單的身材計(jì)算程序,可以計(jì)算身體的體脂率以及BMI數(shù)值等,感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2022-12-12

