SpringBoot 優(yōu)雅停機的正確姿勢
?一、介紹
什么叫優(yōu)雅停機?
簡單的說,就是向應(yīng)用進程發(fā)出停止指令之后,能保證正在執(zhí)行的業(yè)務(wù)操作不受影響,直到操作運行完畢之后再停止服務(wù)。應(yīng)用程序接收到停止指令之后,會進行如下操作:
- 1.停止接收新的訪問請求
- 2.正在處理的請求,等待請求處理完畢;對于內(nèi)部正在執(zhí)行的其他任務(wù),比如定時任務(wù)、mq 消費等等,也要等當(dāng)前正在執(zhí)行的任務(wù)執(zhí)行完畢,并且不再啟動新的任務(wù)
- 3.當(dāng)應(yīng)用準(zhǔn)備關(guān)閉的時候,按需向外發(fā)出信號,告知其他應(yīng)用服務(wù)準(zhǔn)備接手,以保證服務(wù)高可用
如果暴力的關(guān)閉應(yīng)用程序,比如通過kill -9 <pid>命令強制直接關(guān)閉應(yīng)用程序進程,可能會導(dǎo)致正在執(zhí)行的任務(wù)數(shù)據(jù)丟失或者錯亂,也可能會導(dǎo)致任務(wù)所持有的全局資源等不到釋放,比如當(dāng)前任務(wù)持有 redis 的鎖,并且沒有設(shè)置過期時間,當(dāng)任務(wù)突然被終止并且沒有主動釋放鎖,會導(dǎo)致其他進程因無法獲取鎖而不能處理業(yè)務(wù)。
那么如何在不影響正在執(zhí)行的業(yè)務(wù)的情況下,將應(yīng)用程序安全的進行關(guān)閉呢?
二、方案實踐
SpringBoot 官方文檔上,已經(jīng)告訴開發(fā)者只需要實現(xiàn)特定接口即可監(jiān)聽到項目啟動成功與關(guān)閉時的事件,相關(guān)接口如下:
- CommandLineRunner?接口:當(dāng)應(yīng)用啟動成功后但在開始接受流量之前,會回調(diào)此接口的實現(xiàn)類,也可以實現(xiàn)ApplicationRunner接口,工作的方式與CommandLineRunner與之類似
- DisposableBean?接口:當(dāng)應(yīng)用正要被銷毀前,會回調(diào)此接口的實現(xiàn)類,也可以使用@PreDestroy注解,被標(biāo)記的方法也會被調(diào)用
基于此流程,我們可以創(chuàng)建一個服務(wù)監(jiān)聽類,用于監(jiān)聽到項目啟動成功與關(guān)閉時的回調(diào)服務(wù),示例代碼如下:
每一個SpringApplication?在啟用的時候,都會向 JVM 注冊一個關(guān)閉鉤子shutdown hook?,以確保ApplicationContext在退出的時候,通過這個勾子通知 JVM,實現(xiàn)服務(wù)正常的關(guān)閉,以下介紹的所有關(guān)閉服務(wù)的方法,都是基于這一原理進行實現(xiàn)的。
2.1、方法一:通過Actuator的Endpoint機制關(guān)閉服務(wù)
使用此方法,需要先添加spring-boot-starter-actuator監(jiān)控服務(wù)依賴包,
默認配置下,shutdown?端點是關(guān)閉的,需要在application.properties里配置里面開啟:
雖然Actuator?的端點,支持通過JMX或HTTP?進行遠程訪問。而shutdown?默認配置下是不支持HTTP?進行Web?訪問的,所以使用HTTP請求進行關(guān)閉時的配置,也需要開啟:
最后將SpringBoot?服務(wù)啟動之后,使用POST請求類型,調(diào)用以下接口,即可實現(xiàn)關(guān)閉服務(wù)!
2.2、方法二:使用ApplicationContext的close方法關(guān)閉服務(wù)
如果你不想添加spring-boot-starter-actuator?監(jiān)控服務(wù)依賴包來關(guān)停服務(wù),也可以使用ApplicationContext的close?方法來關(guān)停服務(wù),他會自動銷毀bean對象并關(guān)停服務(wù)。
只需要在應(yīng)用啟用的時候,獲取ApplicationContext?對象,然后在相關(guān)的位置調(diào)用close方法,就可以關(guān)閉服務(wù)。
示例代碼如下:
當(dāng)然我們也可以自己寫一個Controller?,獲取對應(yīng)的ApplicationContext?對象,通過api?操作調(diào)用close方法關(guān)停服務(wù),示例代碼如下:
2.3、方法三:監(jiān)聽服務(wù)pid,通過kill方式關(guān)閉服務(wù)
通過api方式來關(guān)停服務(wù),在很多人看來并不安全,因為一旦接口泄漏了,意味著用戶可以隨便請求這個接口來關(guān)閉服務(wù),其影響不言而喻,因此很多人建議在服務(wù)端,通過其他的方式來關(guān)閉服務(wù),比如通過進程命令方式來關(guān)停。
在springboot?啟動的時候?qū)?yīng)用進程 ID 寫入一個app.pid文件,生成的路徑可以指定,然后通過腳本命令方式來關(guān)閉服務(wù)。
啟動示例代碼如下:
通過如下命令方式,可以安全的關(guān)閉服務(wù)。
這種方式,也是目前在linux操作系統(tǒng)中,使用較為普遍的一種解決方案,區(qū)別在于實現(xiàn)的方式可能不同,有的不用寫文件,通過其他方式來獲取應(yīng)用進程 ID。
如果使用?kill -9 <pid>的方式關(guān)閉服務(wù),服務(wù)的監(jiān)聽器不會收到任何消息,類似于直接強殺應(yīng)用進程,此方法不可?。?/p>
2.4、方法四:使用SpringApplication的exit方法關(guān)閉服務(wù)
通過調(diào)用一個SpringApplication.exit()?方法也可以安全的退出程序,同時會返回一個退出碼,這個退出碼可以傳遞給所有的context?,最后通過調(diào)用System.exit()?可以將這個錯誤碼也傳給JVM。
示例代碼如下:
三、其他監(jiān)聽介紹
3.1、ApplicationListener
如果有些服務(wù),比如定時任務(wù),我們想在SpringBoot?關(guān)閉數(shù)據(jù)源連接池之前,將其關(guān)閉,可以通過實現(xiàn)ApplicationListener?接口,監(jiān)聽bean?對象的變化情況,在bean對象銷毀之前,執(zhí)行相關(guān)的關(guān)閉任務(wù)。
示例代碼如下:
3.2、PreDestroy
上文中,我們提到了實現(xiàn)DisposableBean?接口,可以監(jiān)聽?wèi)?yīng)用關(guān)閉前的回調(diào)處理,其實在自定義的方法上加@PreDestroy注解,也可以實現(xiàn)相同的效果。
示例代碼如下:
四、小結(jié)
本位主要圍繞如何安全的關(guān)閉SpringBoot服務(wù),進行了一些方案操作的介紹,如果有疏漏的地方,歡迎網(wǎng)友批評指出!