用戶:貨都到了,購物車裏怎麼還有剛買的東西,what?
產品:有用戶反映,提單完成了,怎麼沒清購物車,研發趕忙看看是否是有bug啊?
研發:恩,我看看,!@#¥%……&*()一頓狂查,搜嘎,當時在上線,重啓應用,異步任務丟了……
產品:能不能行,上線你就丟任務,丟不丟人啊!
研發:…………數據庫
上線!重啓!你還在爲丟失任務而煩惱麼?看這裏看這裏,今後再也不丟任務,JVM能夠安全退出的windows
在交易流程中,爲了提高服務的性能,咱們作了一些異步化的優化,好比更新用戶最近使用的收貨地址、提單完成後經過MQ去發送各類通知類消息、清理用戶的購物車等等這些操做,異步化加快了應用的響應速度同時也帶來一個隱患,如何保障異步操做的執行?這個場景主要發生在應用重啓時,對於經過線程或線程池進行的異步化,JVM重啓時,後臺執行的異步操做可能還沒有完成。這時,須要經過JVM安全關閉來保證異步操做進行完成後,JVM再執行關閉。
更普遍的說,在Linux上不少應用一般會經過kill -9 pid的方式強制將進程殺掉,這種方式簡單高效,所以不少應用的中止腳本常常會選擇使用kill -9 pid的方式。強制進程退出,會帶來一些反作用,對應用程序而言其效果等同於忽然掉電,可能會致使以下一些問題:緩存
這些問題都有可能對咱們的業務產生影響,形成沒必要要的損失,爲了不這些問題,咱們須要在JVM關閉時作些掃尾的工做,爲此JVM提供了關閉鉤子(shutdown hooks)來作這些事情。本文探討了利用關閉鉤子的相關內容。tomcat
首先,咱們瞭解下哪些狀況會致使JVM關閉,以下圖安全
對於強制關閉的幾種狀況,系統關機,操做系統會通知JVM進程關閉並等待,一旦等待超時,系統會強制停止JVM進程;kill -九、Runtime.halt()、斷電、系統crash這些種方式會直接無商量停止JVM進程,JVM徹底沒有執行掃尾工做的機會。所以對用應用程序而言,咱們強烈不建議使用kill -9 這種暴力方式退出。
而對於正常關閉、異常關閉的幾種狀況,JVM關閉前,都會調用已註冊的shutdown hooks,基於這種機制,咱們能夠將掃尾的工做放在shutdown hooks中,進而使咱們的應用程序安全的退出。基於平臺通用性的考慮,咱們更推薦應用程序使用System.exit(0)這種方式退出JVM。併發
JVM 與 shutdown hooks 交互流程以下圖所示,能夠對照源碼進一步的學習shutdown hooks工做原理。
異步
對於tomcat類Web應用,咱們能夠直接經過Runtime.addShutdownHook(Thread hook)註冊自定義鉤子,在鉤子中實現資源的清理;而對於worker類應用,咱們能夠採用以下的方式安全的退出應用。ide
信號是在軟件層次上對中斷機制的一種模擬,在原理上,一個進程收到一個信號與處理器收到一箇中斷請求能夠說是同樣的。通俗來說,信號就是進程間的一種異步通訊機制。信號具備平臺相關性,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字符串,也就上文介紹的信號量名稱。
第二步,根據操做系統的名稱來獲取對應的信號名稱,代碼以下:
1 private String getOSSignalType() 2 { 3 return System.getProperties().getProperty("os.name"). 4 toLowerCase().startsWith("win") ? "INT" : "USR2"; 5 }
判斷是不是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)方法,代碼示例以下:
1 public class ShutdownHandler implements SignalHandler { 2 /** 3 * 處理信號 4 * 5 * @param signal 信號 6 */ 7 public void handle(Signal signal) { 8 } 9 }
第四步,在接收到信號回調的handle接口中,初始化JVM的ShutdownHook線程,並將其註冊到Runtime中,示例代碼以下:
1 private void registerShutdownHook() 2 { 3 Thread t = new Thread(new ShutdownHook(), "ShutdownHook-Thread"); 4 Runtime.getRuntime().addShutdownHook(t); 5 }
第五步,接收到進程退出信號後,在回調的handle接口中執行虛擬機的退出操做,示例代碼以下:
1 Runtime.getRuntime().exit(0);
JVM退出時,底層會自動檢測用戶是否註冊了ShutdownHook任務,若是有,則會自動執行註冊鉤子的Run方法,應用只須要在ShutdownHook中執行掃尾工做便可,示例代碼以下:
1 class ShutdownHook implements Runnable 2 { 3 @Override 4 public void run() { 5 System.out.println("ShutdownHook execute start..."); 6 try { 7 TimeUnit.SECONDS.sleep(10);//模擬應用進程退出前的處理操做 8 } catch (InterruptedException e) { 9 e.printStackTrace(); 10 } 11 System.out.println("ShutdownHook execute end..."); 12 } 13 }
經過以上的幾個步驟,咱們能夠輕鬆實現JVM的安全退出,另外,一般安全退出須要有超時控制機制,例如30S,若是到達超時時間仍然沒有完成退出,則由停機腳本直接調用kill -9強制退出。
關閉鉤子本質上是一個線程(也稱爲Hook線程),對於一個JVM中註冊的多個關閉鉤子它們將會併發執行,因此JVM並不保證它們的執行順序;因爲是併發執行的,那麼極可能由於代碼不當致使出現競態條件或死鎖等問題,爲了不該問題,強烈建議在一個鉤子中執行一系列操做。
Hook線程會延遲JVM的關閉時間,這就要求在編寫鉤子過程當中必需要儘量的減小Hook線程的執行時間,避免hook線程中出現耗時的計算、等待用戶I/O等等操做。
爲了保障應用重啓過程當中異步操做的執行,避免強制退出JVM可能產生的各類問題,咱們能夠採用關閉鉤子、自定義信號的方式,主動的通知JVM退出,並在JVM關閉前,執行應用程序的一些掃尾工做,進一步保證應用程序能夠安全的退出。