背景
使用者:貨都到了,購物車裡怎麼還有剛買的東西,what?
産品:有使用者反映,提單完成了,怎麼沒清購物車,研發趕緊看看是不是有bug啊?
研發:恩,我看看,!@#¥%……&*()一頓狂查,搜嘎,當時在上線,重新開機應用,異步任務丢了……
産品:能不能行,上線你就丢任務,丢不丢人啊!
研發:…………
上線!重新開機!你還在為丢失任務而煩惱麼?看這裡看這裡,從此不再丢任務,JVM可以安全退出的
在交易流程中,為了提升服務的性能,我們做了一些異步化的優化,比如更新使用者最近使用的收貨位址、提單完成後通過MQ去發送各種通知類消息、清理使用者的購物車等等這些操作,異步化加快了應用的響應速度同時也帶來一個隐患,如何保障異步操作的執行?這個場景主要發生在應用重新開機時,對于通過線程或線程池進行的異步化,JVM重新開機時,背景執行的異步操作可能尚未完成。這時,需要通過JVM安全關閉來保證異步操作進行完成後,JVM再執行關閉。
更廣泛的說,在Linux上很多應用通常會通過kill -9 pid的方式強制将程序殺掉,這種方式簡單高效,是以很多應用的停止腳本經常會選擇使用kill -9 pid的方式。強制程序退出,會帶來一些副作用,對應用程式而言其效果等同于突然掉電,可能會導緻如下一些問題:
- 緩存中的資料尚未持久化到磁盤中,導緻資料丢失;
- 正在進行檔案的write操作,沒有更新完成,突然退出,導緻檔案損壞;
- 線程池的任務隊列中尚有接收到的任務還沒來得及處理,導緻任務丢失;
- 資料庫操作已經完成,例如賬戶餘額更新,準備傳回應答消息給用戶端時,消息尚在通信線程的發送隊列中排隊等待發送,程序強制退出導緻應答消息沒有傳回給用戶端,用戶端發起逾時重試,會帶來重複更新問題;
- 其它問題等…
這些問題都有可能對我們的業務産生影響,造成不必要的損失,為了避免這些問題,我們需要在JVM關閉時做些掃尾的工作,為此JVM提供了關閉鈎子(shutdown hooks)來做這些事情。本文探讨了利用關閉鈎子的相關内容。
JVM 關閉
首先,我們了解下哪些情況會導緻JVM關閉,如下圖
對于強制關閉的幾種情況,系統關機,作業系統會通知JVM程序關閉并等待,一旦等待逾時,系統會強制中止JVM程序;kill -9、Runtime.halt()、斷電、系統crash這些種方式會直接無商量中止JVM程序,JVM完全沒有執行掃尾工作的機會。是以對用應用程式而言,我們強烈不建議使用kill -9 這種暴力方式退出。
而對于正常關閉、異常關閉的幾種情況,JVM關閉前,都會調用已注冊的shutdown hooks,基于這種機制,我們可以将掃尾的工作放在shutdown hooks中,進而使我們的應用程式安全的退出。基于平台通用性的考慮,我們更推薦應用程式使用System.exit(0)這種方式退出JVM。
JVM 與 shutdown hooks 互動流程如下圖所示,可以對照源碼進一步的學習shutdown hooks工作原理。
Jvm安全退出
對于tomcat類Web應用,我們可以直接通過Runtime.addShutdownHook(Thread hook)注冊自定義鈎子,在鈎子中實作資源的清理;而對于worker類應用,我們可以采用如下的方式安全的退出應用。
基于信号的程序通知機制
信号是在軟體層次上對中斷機制的一種模拟,在原理上,一個程序收到一個信号與處理器收到一個中斷請求可以說是一樣的。通俗來講,信号就是程序間的一種異步通信機制。信号具有平台相關性,Linux平台支援的一些終止程序信号如下所示:
信号名稱 | 用途 |
---|---|
SIGKILL | 終止程序,強制殺死程序 |
SIGTERM | 終止程序,軟體終止信号 |
SIGTSTP | 停止程序,終端來的停止信号 |
SIGPROF | 終止程序,統計分布圖用計時器到時 |
SIGUSR1 | 終止程序,使用者定義信号1 |
SIGUSR2 | 終止程序,使用者定義信号2 |
SIGINT | 終止程序,中斷程序 |
SIGQUIT | 建立CORE檔案終止程序,并且生成core檔案 |
Windows平台存在一些差異,它的一些信号舉例如下所示:
信号名稱 | 用途 |
---|---|
SIGINT | Ctrl+C中斷 |
SIGTERM | kill發出的軟體終止 |
SIGBREAK | Ctrl+Break中斷 |
信号選擇:為了不幹擾正常信号的運作,又能模拟Java異步通知,在Linux上我們需要先標明一種特殊的信号。通過檢視信号清單上的描述,發現 SIGUSR1 和 SIGUSR2 是允許使用者自定義的信号,我們可以選擇SIGUSR2,在Windows上我們可以選擇SIGINT。
通過這種信号機制,對應用程式JVM發送特定信号,JVM可以感覺并處理該信号,進而可以接受程式退出指令。
安全退出實作
首先看下通用的JVM安全退出的流程圖:
第一步,應用程序啟動的時候,初始化Signal執行個體,它的代碼示例如下:
| |
其中Signal構造函數的參數為String字元串,也就上文介紹的信号量名稱。
第二步,根據作業系統的名稱來擷取對應的信号名稱,代碼如下:
| |
判斷是否是windows作業系統,如果是則選擇SIGINT,接收Ctrl+C中斷的指令;否則選擇USR2信号,接收SIGUSR2(等價于kill -12 pid)指令。
第三步,将執行個體化之後的SignalHandler注冊到JVM的Signal,一旦JVM程序接收到kill -12 或者 Ctrl+C則回調handle接口,代碼示例如下:
| |
其中shutdownHandler實作了SignalHandler接口的handle(Signal sgin)方法,代碼示例如下:
| |
第四步,在接收到信号回調的handle接口中,初始化JVM的ShutdownHook線程,并将其注冊到Runtime中,示例代碼如下:
| |
第五步,接收到程序退出信号後,在回調的handle接口中執行虛拟機的退出操作,示例代碼如下:
| |
JVM退出時,底層會自動檢測使用者是否注冊了ShutdownHook任務,如果有,則會自動執行注冊鈎子的Run方法,應用隻需要在ShutdownHook中執行掃尾工作即可,示例代碼如下:
| |
通過以上的幾個步驟,我們可以輕松實作JVM的安全退出,另外,通常安全退出需要有逾時控制機制,例如30S,如果到達逾時時間仍然沒有完成退出,則由停機腳本直接調用kill -9強制退出。
使用關閉鈎子的注意事項
- 關閉鈎子本質上是一個線程(也稱為Hook線程),對于一個JVM中注冊的多個關閉鈎子它們将會并發執行,是以JVM并不保證它們的執行順序;由于是并發執行的,那麼很可能因為代碼不當導緻出現競态條件或死鎖等問題,為了避免該問題,強烈建議在一個鈎子中執行一系列操作。
- Hook線程會延遲JVM的關閉時間,這就要求在編寫鈎子過程中必須要盡可能的減少Hook線程的執行時間,避免hook線程中出現耗時的計算、等待使用者I/O等等操作。
- 關閉鈎子執行過程中可能被強制打斷,比如在作業系統關機時,作業系統會等待程序停止,等待逾時,程序仍未停止,作業系統會強制的殺死該程序,在這類情況下,關閉鈎子在執行過程中被強制中止。
- 在關閉鈎子中,不能執行注冊、移除鈎子的操作,JVM将關閉鈎子序列初始化完畢後,不允許再次添加或者移除已經存在的鈎子,否則JVM抛出 IllegalStateException。
- 不能在鈎子調用System.exit(),否則卡住JVM的關閉過程,但是可以調用Runtime.halt()。
- Hook線程中同樣會抛出異常,對于未捕捉的異常,線程的預設異常處理器處理該異常,不會影響其他hook線程以及JVM正常退出。
總結
為了保障應用重新開機過程中異步操作的執行,避免強制退出JVM可能産生的各種問題,我們可以采用關閉鈎子、自定義信号的方式,主動的通知JVM退出,并在JVM關閉前,執行應用程式的一些掃尾工作,進一步保證應用程式可以安全的退出。
文章來源:https://www.cnblogs.com/panchanggui/p/9806266.html