JVM安全退出(如何優雅的關閉java服務)

背景

用戶:貨都到了,購物車裏怎麼還有剛買的東西,what?
產品:有用戶反映,提單完成了,怎麼沒清購物車,研發趕忙看看是否是有bug啊?
研發:恩,我看看,!@#¥%……&*()一頓狂查,搜嘎,當時在上線,重啓應用,異步任務丟了……
產品:能不能行,上線你就丟任務,丟不丟人啊!
研發:…………數據庫

 

上線!重啓!你還在爲丟失任務而煩惱麼?看這裏看這裏,今後再也不丟任務,JVM能夠安全退出的windows

在交易流程中,爲了提高服務的性能,咱們作了一些異步化的優化,好比更新用戶最近使用的收貨地址、提單完成後經過MQ去發送各類通知類消息、清理用戶的購物車等等這些操做,異步化加快了應用的響應速度同時也帶來一個隱患,如何保障異步操做的執行?這個場景主要發生在應用重啓時,對於經過線程或線程池進行的異步化,JVM重啓時,後臺執行的異步操做可能還沒有完成。這時,須要經過JVM安全關閉來保證異步操做進行完成後,JVM再執行關閉。
更普遍的說,在Linux上不少應用一般會經過kill -9 pid的方式強制將進程殺掉,這種方式簡單高效,所以不少應用的中止腳本常常會選擇使用kill -9 pid的方式。強制進程退出,會帶來一些反作用,對應用程序而言其效果等同於忽然掉電,可能會致使以下一些問題:緩存

  1. 緩存中的數據還沒有持久化到磁盤中,致使數據丟失;
  2. 正在進行文件的write操做,沒有更新完成,忽然退出,致使文件損壞;
  3. 線程池的任務隊列中尚有接收到的任務還沒來得及處理,致使任務丟失;
  4. 數據庫操做已經完成,例如帳戶餘額更新,準備返回應答消息給客戶端時,消息尚在通訊線程的發送隊列中排隊等待發送,進程強制退出致使應答消息沒有返回給客戶端,客戶端發起超時重試,會帶來重複更新問題;
  5. 其它問題等…

這些問題都有可能對咱們的業務產生影響,形成沒必要要的損失,爲了不這些問題,咱們須要在JVM關閉時作些掃尾的工做,爲此JVM提供了關閉鉤子(shutdown hooks)來作這些事情。本文探討了利用關閉鉤子的相關內容。tomcat

JVM 關閉

首先,咱們瞭解下哪些狀況會致使JVM關閉,以下圖安全

image

對於強制關閉的幾種狀況,系統關機,操做系統會通知JVM進程關閉並等待,一旦等待超時,系統會強制停止JVM進程;kill -九、Runtime.halt()、斷電、系統crash這些種方式會直接無商量停止JVM進程,JVM徹底沒有執行掃尾工做的機會。所以對用應用程序而言,咱們強烈不建議使用kill -9 這種暴力方式退出。
而對於正常關閉、異常關閉的幾種狀況,JVM關閉前,都會調用已註冊的shutdown hooks,基於這種機制,咱們能夠將掃尾的工做放在shutdown hooks中,進而使咱們的應用程序安全的退出。基於平臺通用性的考慮,咱們更推薦應用程序使用System.exit(0)這種方式退出JVM。併發

JVM 與 shutdown hooks 交互流程以下圖所示,能夠對照源碼進一步的學習shutdown hooks工做原理。
image異步

Jvm安全退出

對於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安全退出的流程圖:

image

第一步,應用進程啓動的時候,初始化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拋出 IllegalStateException。
  • 不能在鉤子調用System.exit(),不然卡住JVM的關閉過程,可是能夠調用Runtime.halt()。
  • Hook線程中一樣會拋出異常,對於未捕捉的異常,線程的默認異常處理器處理該異常,不會影響其餘hook線程以及JVM正常退出。

總結

爲了保障應用重啓過程當中異步操做的執行,避免強制退出JVM可能產生的各類問題,咱們能夠採用關閉鉤子、自定義信號的方式,主動的通知JVM退出,並在JVM關閉前,執行應用程序的一些掃尾工做,進一步保證應用程序能夠安全的退出。

相關文章
相關標籤/搜索