JVM應(yīng)用優(yōu)雅上下線,再也不擔(dān)心抖動(dòng)了
一、前言
JVM的關(guān)閉方式可以分為三種:
1.正常關(guān)閉:當(dāng)最后一個(gè)非守護(hù)線程結(jié)束、或者調(diào)用了System.exit、或者通過其他特定平臺(tái)的方法關(guān)閉(發(fā)送SIGINT,SIGTERM信號(hào)等)
2.強(qiáng)制關(guān)閉:通過調(diào)用Runtime.halt方法、或者是在操作系統(tǒng)中直接kill(發(fā)送SIGKILL信號(hào))掉JVM進(jìn)程
3.異常關(guān)閉:運(yùn)行中遇到RuntimeException異常、OOM錯(cuò)誤等。
二、ShutdownHook
通常JVM可使用runtime.addShutdownHook()對(duì)退出信號(hào)做處理,它讓我們?cè)诔绦蛘M顺龌蛘甙l(fā)生異常時(shí)能有機(jī)會(huì)做一些清場(chǎng)工作。關(guān)閉鉤子其實(shí)可以看成是一個(gè)已經(jīng)初始化了的但還沒啟動(dòng)的線程,當(dāng)JVM關(guān)閉時(shí)會(huì)并發(fā)無序地執(zhí)行注冊(cè)的所有關(guān)閉鉤子。
Runtime.getRuntime().addShutdownHook(handleThread); //handleThread是信號(hào)處理線程。
ShutdownHook響應(yīng)的信號(hào)如下:
- 1(SIGHUP):如果使用了nohup則不響應(yīng);
- 2(SIGINT):如果使用了后臺(tái)&則不響應(yīng);
- 15(SIGTERM):都響應(yīng)。
注意事項(xiàng):
- 不要使用kill -9來結(jié)束進(jìn)程,這樣ShutdownHook得不到執(zhí)行;
- ShutdownHook要盡量短。計(jì)算機(jī)在關(guān)機(jī)前,會(huì)給所有的進(jìn)程發(fā)送一個(gè)SIGTERM信號(hào),等若干秒后就直接發(fā)送SIGKILL了;
- ShutdownHook要保證線程安全。如果多次發(fā)送信號(hào),那么ShutdownHook被不同的線程多次執(zhí)行。
三、SignalHandler
用戶可以自定義SignalHander對(duì)特定信號(hào)進(jìn)行處理。
class MySignalHandler implements SignalHandler
{
public static void listenTo(String name) {
Signal signal = new Signal(name);
Signal.handle(signal, new MySignalHandler());
}
public void handle(Signal signal) {
System.out.println("Signal: " + signal);
if (signal.toString().trim().equals("SIGTERM")) {
System.out.println("SIGTERM raised, terminating...");
System.exit(1);
}
}
}
Java對(duì)每個(gè)信號(hào)都啟動(dòng)一個(gè)線程進(jìn)行處理。注冊(cè)TERM信號(hào),就啟動(dòng)"SIGTERM handler" 線程。即便主線程被阻塞,信號(hào)依然可以得到處理。由于對(duì)信號(hào)的處理是多線程的,所以應(yīng)保證信號(hào)處理實(shí)例SignalHandler應(yīng)該是線程安全的。
四、總結(jié)
- ShutdownHook只響應(yīng)1(SIGHUP)、2(SIGINT)、15(SIGTERM)三種信號(hào),而JVM一般用nohup...&的方式啟動(dòng),所以會(huì)忽略1、2兩種信號(hào);
- ShutdownHook觸發(fā)時(shí),多個(gè)鉤子會(huì)并發(fā)無序執(zhí)行。如果資源關(guān)閉上有先后依賴則會(huì)有問題;
4.1 優(yōu)雅關(guān)閉
由于ShutdownHook的并發(fā)無序執(zhí)行,所以我們?cè)趦?yōu)雅關(guān)閉時(shí)不能直接kill -15,比如有殘留請(qǐng)求的情況,如果部分資源已關(guān)閉,那么殘留請(qǐng)求的執(zhí)行會(huì)有異常。 正確流程如下:
- kill -12:等待10s。用戶自定義SignalHandler處理12信號(hào),而且此時(shí)所有的資源都是正常狀態(tài)。1)告知上游該服務(wù)已關(guān)閉,不要再發(fā)請(qǐng)求;2)處理殘留的請(qǐng)求;3)其他需要正常關(guān)閉的操作。
- kill -15:等待10s。這時(shí)會(huì)并發(fā)無序執(zhí)行注冊(cè)的ShutdownHook,進(jìn)行一些資源的釋放,很有可能不需要10sJVM就退出了。
- kill -9:如果kill -15還沒有終止JVM,則直接強(qiáng)制退出。
這里的優(yōu)雅就體現(xiàn)在第一步的10秒kill -12,在資源都正常的情況下給業(yè)務(wù)一些時(shí)間來正常關(guān)閉服務(wù)。
4.2 示例
我們以轉(zhuǎn)轉(zhuǎn)的RPC框架ZZSCF為例,來看其是如何實(shí)現(xiàn)優(yōu)雅關(guān)閉的。
4.2.1 kill -12
首先,我們進(jìn)行kil -12并等待10秒,用戶自定義SignalHandler來處理12信號(hào),而且此時(shí)所有的資源都是正常狀態(tài)。
圖片
圖片
圖片
圖片
圖片
圖片
4.2.2 kill -15
接著,我們進(jìn)行kil -15并等待10秒。這時(shí)會(huì)并發(fā)無序執(zhí)行注冊(cè)的ShutdownHook,進(jìn)行一些資源的釋放,很有可能不需要10sJVM就退出了。
4.2.3 kill -9
最后,如果kill -15還沒有終止JVM,則直接強(qiáng)制退出。
五、啟動(dòng)腳本DEMO
這里附贈(zèng)常用JVM的重啟腳本。
用法:./main.sh start|stop|restart|kill|status
查看源碼鏈接:https://github.com/waterystone/shell_test/blob/main/jvm/main.sh
六、參考
- How to gracefully handle the SIGKILL signal in Java:https://stackoverflow.com/questions/2541597/how-to-gracefully-handle-the-sigkill-signal-in-java
- 服務(wù)如何優(yōu)雅關(guān)閉:https://juejin.cn/post/6844903814181421064