Spring Boot 實現(xiàn)優(yōu)雅停機的正確姿勢!
一、背景介紹
什么叫優(yōu)雅停機?
簡單的說,就是向應用進程發(fā)出停止指令之后,能保證正在執(zhí)行的業(yè)務操作不受影響,直到操作運行完畢之后再停止服務。應用程序接收到停止指令之后,會進行如下操作:
- 1.停止接收新的訪問請求
- 2.正在處理的請求,等待請求處理完畢;對于內(nèi)部正在執(zhí)行的其他任務,比如定時任務、mq 消費等等,也要等當前正在執(zhí)行的任務執(zhí)行完畢,并且不再啟動新的任務
- 3.當應用準備關閉的時候,按需向外發(fā)出信號,告知其他應用服務準備接手,以保證服務高可用
如果暴力的關閉應用程序,比如通過kill -9 <pid>命令強制直接關閉應用程序進程,可能會導致正在執(zhí)行的任務數(shù)據(jù)丟失或者錯亂,也可能會導致任務所持有的全局資源等不到釋放,比如當前任務持有 redis 的鎖,并且沒有設置過期時間,當任務突然被終止并且沒有主動釋放鎖,會導致其他進程因無法獲取鎖而不能處理業(yè)務。
那么如何在不影響正在執(zhí)行的業(yè)務的情況下,將應用程序安全的進行關閉呢?
二、方案實踐
SpringBoot 官方文檔上,已經(jīng)告訴開發(fā)者只需要實現(xiàn)特定接口即可監(jiān)聽到項目啟動成功與關閉時的事件,相關接口如下:
- CommandLineRunner接口:當應用啟動成功后但在開始接受流量之前,會回調(diào)此接口的實現(xiàn)類,也可以實現(xiàn)ApplicationRunner接口,工作的方式與CommandLineRunner與之類似
- DisposableBean接口:當應用正要被銷毀前,會回調(diào)此接口的實現(xiàn)類,也可以使用@PreDestroy注解,被標記的方法也會被調(diào)用
基于此流程,我們可以創(chuàng)建一個服務監(jiān)聽類,用于監(jiān)聽到項目啟動成功與關閉時的回調(diào)服務,示例代碼如下:
@Component
public class AppListener implements CommandLineRunner, DisposableBean {
@Override
public void run(String... args) throws Exception {
System.out.println("應用啟動成功,預加載相關數(shù)據(jù)");
}
@Override
public void destroy() throws Exception {
System.out.println("應用正在關閉,清理相關數(shù)據(jù)");
}
}
每一個SpringApplication在啟用的時候,都會向 JVM 注冊一個關閉鉤子shutdown hook,以確保ApplicationContext在退出的時候,通過這個勾子通知 JVM,實現(xiàn)服務正常的關閉,以下介紹的所有關閉服務的方法,都是基于這一原理進行實現(xiàn)的。
2.1方法一:通過Actuator的Endpoint機制關閉服務
使用此方法,需要先添加spring-boot-starter-actuator監(jiān)控服務依賴包,
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
默認配置下,shutdown端點是關閉的,需要在application.properties里配置里面開啟:
management.endpoint.shutdown.enabled=true
雖然Actuator的端點,支持通過JMX或HTTP進行遠程訪問。而shutdown默認配置下是不支持HTTP進行Web訪問的,所以使用HTTP請求進行關閉時的配置,也需要開啟:
management.endpoints.web.exposure.include=shutdown
最后將SpringBoot服務啟動之后,使用POST請求類型,調(diào)用以下接口,即可實現(xiàn)關閉服務!
http://127.0.0.1:8080/actuator/shutdown
圖片
2.2方法二:使用ApplicationContext的close方法關閉服務
如果你不想添加spring-boot-starter-actuator監(jiān)控服務依賴包來關停服務,也可以使用ApplicationContext的close方法來關停服務,他會自動銷毀bean對象并關停服務。
只需要在應用啟用的時候,獲取ApplicationContext對象,然后在相關的位置調(diào)用close方法,就可以關閉服務。
示例代碼如下:
@SpringBootApplication
publicclass Application {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(Application.class, args);
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//啟動10秒以后,自動關閉
context.close();
}
}
當然我們也可以自己寫一個Controller,獲取對應的ApplicationContext對象,通過api操作調(diào)用close方法關停服務,示例代碼如下:
@RestController
publicclass ShutdownController implements ApplicationContextAware {
private ApplicationContext context;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.context = applicationContext;
}
/**
* 關閉服務
*/
@GetMapping("/shutdown")
public void shutdownContext() {
((ConfigurableApplicationContext) context).close();
}
}
2.3方法三:監(jiān)聽服務pid,通過kill方式關閉服務
通過api方式來關停服務,在很多人看來并不安全,因為一旦接口泄漏了,意味著用戶可以隨便請求這個接口來關閉服務,其影響不言而喻,因此很多人建議在服務端,通過其他的方式來關閉服務,比如通過進程命令方式來關停。
在springboot啟動的時候?qū)眠M程 ID 寫入一個app.pid文件,生成的路徑可以指定,然后通過腳本命令方式來關閉服務。
啟動示例代碼如下:
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication application = new SpringApplication(Application.class);
application.addListeners(new ApplicationPidFileWriter("/home/app/project1/app.pid"));
application.run();
}
}
通過如下命令方式,可以安全的關閉服務。
cat /home/app/project1/app.pid | xargs kill
這種方式,也是目前在linux操作系統(tǒng)中,使用較為普遍的一種解決方案,區(qū)別在于實現(xiàn)的方式可能不同,有的不用寫文件,通過其他方式來獲取應用進程 ID。
如果使用kill -9 <pid>的方式關閉服務,服務的監(jiān)聽器不會收到任何消息,類似于直接強殺應用進程,此方法不可??!
2.4方法四:使用SpringApplication的exit方法關閉服務
通過調(diào)用一個SpringApplication.exit()方法也可以安全的退出程序,同時會返回一個退出碼,這個退出碼可以傳遞給所有的context,最后通過調(diào)用System.exit()可以將這個錯誤碼也傳給JVM。
示例代碼如下:
@SpringBootApplication
publicclass Application {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(Application.class, args);
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
//5秒后,關閉服務
exitApplication(context);
}
public static void exitApplication(ConfigurableApplicationContext context) {
//獲取退出碼
int exitCode = SpringApplication.exit(context, (ExitCodeGenerator) () -> 0);
//退出碼傳遞給jvm,安全退出程序
System.exit(exitCode);
}
}
三、其他監(jiān)聽介紹
3.1ApplicationListener
如果有些服務,比如定時任務,我們想在SpringBoot關閉數(shù)據(jù)源連接池之前,將其關閉,可以通過實現(xiàn)ApplicationListener接口,監(jiān)聽bean對象的變化情況,在bean對象銷毀之前,執(zhí)行相關的關閉任務。
示例代碼如下:
@Component
public class JobTaskListener implements ApplicationListener {
@Override
public void onApplicationEvent(ApplicationEvent applicationEvent) {
// 在spring bean容器銷毀之前執(zhí)行的事件,防止數(shù)據(jù)庫連接池在任務終止前銷毀
if (applicationEvent instanceof ContextClosedEvent) {
System.out.println("關閉相關的定時任務");
}
}
}
3.2PreDestroy
上文中,我們提到了實現(xiàn)DisposableBean接口,可以監(jiān)聽應用關閉前的回調(diào)處理,其實在自定義的方法上加@PreDestroy注解,也可以實現(xiàn)相同的效果。
示例代碼如下:
@Component
public class AppDestroyConfig {
@PreDestroy
public void PreDestroy(){
System.out.println("應用程序正在關閉。。。");
}
}