1. 程式人生 > >JVM安全退出(如何優雅的關閉java服務)

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

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的方式。強制程序退出,會帶來一些副作用,對應用程式而言其效果等同於突然掉電,可能會導致如下一些問題:

  1. 快取中的資料尚未持久化到磁碟中,導致資料丟失;
  2. 正在進行檔案的write操作,沒有更新完成,突然退出,導致檔案損壞;
  3. 執行緒池的任務佇列中尚有接收到的任務還沒來得及處理,導致任務丟失;
  4. 資料庫操作已經完成,例如賬戶餘額更新,準備返回應答訊息給客戶端時,訊息尚在通訊執行緒的傳送佇列中排隊等待發送,程序強制退出導致應答訊息沒有返回給客戶端,客戶端發起超時重試,會帶來重複更新問題;
  5. 其它問題等…

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

JVM 關閉

首先,我們瞭解下哪些情況會導致JVM關閉,如下圖

image

對於強制關閉的幾種情況,系統關機,作業系統會通知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工作原理。
image

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

image

第一步,應用程序啟動的時候,初始化Signal例項,它的程式碼示例如下:

1
Signal sig = new Signal(getOSSignalType());

其中Signal建構函式的引數為String字串,也就上文介紹的訊號量名稱。

第二步,根據作業系統的名稱來獲取對應的訊號名稱,程式碼如下:

1
2
3
4
5
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)方法,程式碼示例如下:

1
2
3
4
5
6
7
8
9
public class ShutdownHandler implements SignalHandler {
/**
* 處理訊號
*
* @param signal 訊號
*/
public void handle(Signal signal) {
}
}

第四步,在接收到訊號回撥的handle介面中,初始化JVM的ShutdownHook執行緒,並將其註冊到Runtime中,示例程式碼如下:

1
2
3
4
5
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中執行掃尾工作即可,示例程式碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
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...");
}
}

通過以上的幾個步驟,我們可以輕鬆實現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關閉前,執行應用程式的一些掃尾工作,進一步保證應用程式可以安全的退出。