SpringBoot實(shí)現(xiàn)優(yōu)雅停機(jī)的三種方式
引言
應(yīng)用的啟停是一個(gè)常見操作。然而,突然終止一個(gè)正在運(yùn)行的應(yīng)用可能導(dǎo)致正在處理的請(qǐng)求失敗、數(shù)據(jù)不一致等問題。優(yōu)雅停機(jī)(Graceful Shutdown)是指應(yīng)用在接收到停止信號(hào)后,能夠妥善處理現(xiàn)有請(qǐng)求、釋放資源,然后再退出的過程。本文將詳細(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ù)庫連接池、線程池、消息隊(duì)列連接等)
- 完成必要的清理工作
- 最后退出應(yīng)用
優(yōu)雅停機(jī)的核心價(jià)值在于:
- 提高用戶體驗(yàn),避免請(qǐng)求突然中斷
- 保障數(shù)據(jù)一致性,防止數(shù)據(jù)丟失
方式一:SpringBoot內(nèi)置的優(yōu)雅停機(jī)支持
原理與支持版本
從Spring Boot 2.3版本開始,框架原生支持優(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
- 在處理過程中,向應(yīng)用發(fā)送SIGTERM信號(hào):
kill -15 <進(jìn)程ID>
,如果是IDEA開發(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)單,官方原生支持
- 無需額外代碼,維護(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ì)較低,無法精細(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ú)特之處在于它允許通過HTTP請(qǐng)求觸發(fā)停機(jī)過程,適合需要遠(yuǎn)程操作的場(chǎng)景。
配置步驟
- 添加Actuator依賴:
<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ú)端口
使用方法
通過HTTP POST請(qǐng)求觸發(fā)停機(jī):
curl -X POST http://localhost:8081/management/shutdown
請(qǐng)求成功后,會(huì)返回類似如下響應(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
使用安全配置后的訪問方式:
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):
- 可以通過HTTP請(qǐng)求遠(yuǎn)程觸發(fā)停機(jī)
- 適合管理工具和運(yùn)維腳本集成
- 可以與Spring Security集成實(shí)現(xiàn)訪問控制
- 支持所有Spring Boot版本(包括2.3之前的版本)
缺點(diǎn):
- 需要額外配置和依賴
- 潛在的安全風(fēng)險(xiǎn),需謹(jǐn)慎保護(hù)端點(diǎn)
- 對(duì)于內(nèi)部復(fù)雜資源的關(guān)閉需要額外編碼
適用場(chǎng)景
- 需要通過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í)行自定義邏輯。通過注冊(cè)自定義的ShutdownHook,我們可以實(shí)現(xiàn)更加精細(xì)和靈活的優(yōu)雅停機(jī)控制。這種方式的優(yōu)勢(shì)在于可以精確控制資源釋放順序,適合有復(fù)雜資源管理需求的應(yīng)用。
基本實(shí)現(xiàn)步驟
- 創(chuàng)建自定義的ShutdownHandler類
- 注冊(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ù)庫連接池 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類型的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類型的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); } // 可以添加其他類型連接池的關(guān)閉邏輯 else { // 嘗試通過反射調(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框架無法管理的外部資源
缺點(diǎn):
- 實(shí)現(xiàn)復(fù)雜度高,需要詳細(xì)了解應(yīng)用資源
- 維護(hù)成本較高
- 容易出現(xiàn)資源關(guān)閉順序錯(cuò)誤導(dǎo)致的問題
適用場(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ù)雜度 | 低 | 中 | 高 |
靈活性 | 低 | 中 | 高 |
可定制性 | 低 | 中 | 高 |
框架依賴 | Spring Boot 2.3+ | 任何Spring Boot版本 | 任何Java應(yīng)用 |
額外依賴 | 無 | Actuator | 無 |
觸發(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)用
- 沒有特殊的資源管理需求
- 希望最簡(jiǎn)單的配置
選擇Actuator端點(diǎn)方案,如果:
- 需要通過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)用可靠性和用戶體驗(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提供了最大的靈活性,可以精確控制資源釋放順序和方式
無論選擇哪種方式,優(yōu)雅停機(jī)都應(yīng)該成為微服務(wù)設(shè)計(jì)的標(biāo)準(zhǔn)實(shí)踐。正確實(shí)現(xiàn)優(yōu)雅停機(jī),不僅能提升系統(tǒng)穩(wěn)定性,還能改善用戶體驗(yàn),減少因應(yīng)用重啟或降級(jí)帶來的業(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)文章
基于HttpServletResponse 相關(guān)常用方法的應(yīng)用
本篇文章小編為大家介紹,基于HttpServletResponse 相關(guān)常用方法的應(yīng)用,需要的朋友參考下2013-04-04Spring Boot 實(shí)現(xiàn)https ssl免密登錄(X.509 pki登錄)
這篇文章主要介紹了Spring Boot 實(shí)現(xiàn)https ssl免密登錄(X.509 pki登錄),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-01-01Java concurrency之公平鎖(二)_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
這篇文章主要為大家詳細(xì)介紹了Java concurrency之公平鎖的第二篇內(nèi)容,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-06-06Java中RabbitMQ延遲隊(duì)列實(shí)現(xiàn)詳解
這篇文章主要介紹了Java中RabbitMQ延遲隊(duì)列實(shí)現(xiàn)詳解,消息過期后,根據(jù)routing-key的不同,又會(huì)被死信交換機(jī)路由到不同的死信隊(duì)列中,消費(fèi)者只需要監(jiān)聽對(duì)應(yīng)的死信隊(duì)列進(jìn)行消費(fèi)即可,需要的朋友可以參考下2023-09-09