https://tech.imdada.cn/2017/06/18/jvm-safe-exit/?utm_source=tuicool&utm_medium=referral
背景
使用者:貨都到了,購物車裡怎麼還有剛買的東西,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執行個體,它的程式碼範例如下:
1 |
Signal sig = new Signal(getOSSignalType()); |
其中Signal建構函式的參數為String字串,也就上文介紹的訊號量名稱。
第二步,根據作業系統的名稱來擷取對應的訊號名稱,代碼如下:
12345 |
private String getOSSignalType() { return System.getProperties().getProperty("os.name"). toLowerCase().startsWith("win") ? "INT" : "USR2"; } |
判斷是否是windows作業系統,如果是則選擇SIGINT,接收Ctrl+C中斷的指令;否則選擇USR2訊號,接收SIGUSR2(等價於kill -12 pid)指令。
第三步,將執行個體化之後的SignalHandler註冊到JVM的Signal,一旦JVM進程接收到kill -12 或者 Ctrl+C則回調handle介面,程式碼範例如下:
1 |
Signal.handle(sig, shutdownHandler); |
其中shutdownHandler實現了SignalHandler介面的handle(Signal sgin)方法,程式碼範例如下:
123456789 |
public class ShutdownHandler implements SignalHandler { /** * 處理訊號 * * @param signal 訊號 */ public void handle(Signal signal) { }} |
第四步,在接收到訊號回調的handle介面中,初始化JVM的ShutdownHook線程,並將其註冊到Runtime中,範例程式碼如下:
12345 |
private void registerShutdownHook() { Thread t = new Thread(new ShutdownHook(), "ShutdownHook-Thread"); Runtime.getRuntime().addShutdownHook(t); } |
第五步,接收到進程退出訊號後,在回調的handle介面中執行虛擬機器的退出操作,範例程式碼如下:
1 |
Runtime.getRuntime().exit(0); |
JVM退出時,底層會自動檢測使用者是否註冊了ShutdownHook任務,如果有,則會自動執行註冊鉤子的Run方法,應用只需要在ShutdownHook中執行掃尾工作即可,範例程式碼如下:
12345678910111213 |
class ShutdownHook implements Runnable{ @Override public void run() { System.out.println("ShutdownHook execute start..."); try { TimeUnit.SECONDS.sleep(10);//類比應用進程退出前的處理操作 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("ShutdownHook execute end..."); }} |