Java 如何實現優雅停服?刨根問底

一猿小講發表於2020-05-20

在 Java 的世界裡遨遊,如果能擁有一雙善於發現的眼睛,有很多東西留心去看,外加耐心助力,仔細去品,往往會品出不一樣的味道。

通過本次分享,能讓你輕鬆 get 如下幾點,絕對收穫滿滿。

a)如何讓 Java 程式實現優雅停服?有思想才是硬道理!

b)addShutdownHook 的使用場景?會用才是王道!

c)addShutdownHook 鉤子函式到底是個啥?刨根問底!

1. 如何讓 Java 程式實現優雅停服?

無論是自研基礎服務框架,還是分析開源專案原始碼,細心的 Java 開發同學,都會發現 Runtime.getRuntime().addShutdownHook 這麼一句程式碼的身影,這句到底是幹什麼用的?

接下來就一起細品,看看它香不香?

阿里開源的資料同步神器 Canal 啟動時的部分原始碼:

Apache 麾下的用於海量日誌收集的 Flume 啟動時的部分原始碼:

 

仰望了一下開源的專案,不妨從中提煉一下共性(同樣的程式碼遇到多次,勢必會品出味道),寫段程式碼跑跑看(站在 flume 原始碼的肩膀上,起飛)。

 1 import java.util.concurrent.ScheduledThreadPoolExecutor;
 2 import java.util.concurrent.TimeUnit;
 3 
 4 /**
 5  * 體驗 Java 優雅停服
 6  *
 7  * @author 一猿小講
 8  */
 9 public class Application {
10 
11     /**
12      * 監控服務
13      */
14     private ScheduledThreadPoolExecutor monitorService;
15 
16     public Application() {
17         monitorService = new ScheduledThreadPoolExecutor(1);
18     }
19 
20     /**
21      * 啟動監控服務,監控一下記憶體資訊
22      */
23     public void start() {
24         System.out.println(String.format("啟動監控服務 %s", Thread.currentThread().getId()));
25         monitorService.scheduleWithFixedDelay(new Runnable() {
26             @Override
27             public void run() {
28                 System.out.println(String.format("最大記憶體: %dm  已分配記憶體: %dm  已分配記憶體中的剩餘空間: %dm  最大可用記憶體: %dm",
29                         Runtime.getRuntime().maxMemory() / 1024 / 1024,
30                         Runtime.getRuntime().totalMemory() / 1024 / 1024,
31                         Runtime.getRuntime().freeMemory() / 1024 / 1024,
32                         (Runtime.getRuntime().maxMemory() - Runtime.getRuntime().totalMemory() +
33                                 Runtime.getRuntime().freeMemory()) / 1024 / 1024));
34             }
35         }, 2, 2, TimeUnit.SECONDS);
36     }
37 
38     /**
39      * 釋放資源(程式碼來源於 flume 原始碼)
40      * 主要用於關閉執行緒池(看不懂的同學莫糾結,當做黑盒去對待)
41      */
42     public void stop() {
43         System.out.println(String.format("開始關閉執行緒池 %s", Thread.currentThread().getId()));
44         if (monitorService != null) {
45             monitorService.shutdown();
46             try {
47                 monitorService.awaitTermination(10, TimeUnit.SECONDS);
48             } catch (InterruptedException e) {
49                 System.err.println("Interrupted while waiting for monitor service to stop");
50             }
51             if (!monitorService.isTerminated()) {
52                 monitorService.shutdownNow();
53                 try {
54                     while (!monitorService.isTerminated()) {
55                         monitorService.awaitTermination(10, TimeUnit.SECONDS);
56                     }
57                 } catch (InterruptedException e) {
58                     System.err.println("Interrupted while waiting for monitor service to stop");
59                 }
60             }
61         }
62         System.out.println(String.format("執行緒池關閉完成 %s", Thread.currentThread().getId()));
63     }
64 
65     /**
66      * 應用入口
67      */
68     public static void main(String[] args) {
69         Application application = new Application();
70         // 啟動服務(每隔一段時間監控輸出一下記憶體資訊)
71         application.start();
72 
73         // 新增鉤子,實現優雅停服(主要驗證鉤子的作用)
74         final Application appReference = application;
75         Runtime.getRuntime().addShutdownHook(new Thread("shutdown-hook") {
76             @Override
77             public void run() {
78                 System.out.println("接收到退出的訊號,開始打掃戰場,釋放資源,完成優雅停服");
79                 appReference.stop();
80             }
81         });
82         System.out.println("服務啟動完成");
83     }
84 }

經常讀文的我很清楚,耐心讀文章中原始碼的同學應該很少,所以我還是用圖給你簡單捋一捋。

標註1:start 方法利用執行緒池啟動一個執行緒去定時監控記憶體資訊;

標註2:stop 方法用於在退出程式之前,進行關閉執行緒池進而釋放資源。

程式跑起來,效果如下。

 

當進行 kill 操作時,程式確實進行了資源釋放,效果確實很優雅。

 

一切看似那麼自然,一切又是那麼完美,這是真的嗎?殺程式時候如果用 kill -9,這種情況下會發生什麼現象呢?

 

嗚呼!結果不會騙人的,當用 kill -9 的時候,就顯得很粗暴了,壓根不管什麼資源釋放,不管三七二十一,就是終止程式。

估計很多同學,都擅長用 kill -9 進行殺程式,為了線上的應用安全,還是用 kill -15 命令殺程式吧,這樣會給應用留點時間去打掃一下戰場,釋放一下資源。

好了,通過仔細品味,藉助 JDK 自帶的 addShutdownHook 來助力應用,確實能讓線上服務跑起來很優雅。

有思想才是硬道理!

2. addShutdownHook 的使用場景?

通過程式碼試驗,能夠感知 addShutdownHook(new Thread(){}) 是 JVM 銷燬前要執行的一個執行緒,那麼只要是涉及到資源回收的場景,應該都可以滿足,下面簡單列舉幾個。

a)資料同步神器 Canal 藉助它,來進行關閉 socket 連結、釋放 canal 的工作節點、清理快取資訊等;

b)海量日誌收集 Flume 藉助它,來實現執行緒池資源關閉、工作執行緒停止等;

c)在應用正常退出時,執行特定的業務邏輯、關閉資源等操作。

d)在 OOM 當機、 CTRL+C、或執行 kill pid,導致 JVM 非正常退出時,加入必要的挽救措施成為可能。

其實,在 Java 的世界裡遨遊,只有想不到的,沒有做不到的!

3. addShutdownHook 鉤子函式是個啥?

刨根還要問到底!

 

Hook 翻譯過來是「鉤子」的意思,那顧名思義就是用來掛東西的。

 

如圖所示,在現實生活中,要製作臘肉,首先用鉤子把肉勾住,然後掛在竹竿上,這應該是鉤子的作用。

生活如此,一切設計理念都源於生活,在 Java 的世界裡,亦是如此。

 

如上圖 Runtime 的原始碼所示,遵循 Java 的核心思想「一切皆是物件」,那麼可以把 addShutdownHook 方法可以視作掛鉤子,其實稱之為鉤子函式會好一些,而現實生活中的肉就可以抽象為釋放資源的執行緒。

只要有這個鉤子函式,對外就提供了擴充套件能力,研發人員就可以往鉤子上掛各種自定義的場景實現,這種設計你細品那絕對是香!這也就是 Canal、Flume、Tomcat 等不同應用,在優雅停服時有著不同的實現的原因吧。

大白話,鉤子函式有了,想掛什麼東西,根據心情自己定就好了。

再深入去刨會發現,由於底層資料結構採用 Map 來進行儲存,那麼就支援研發人員掛多個 shutdownHook 的實現,又帶來了無限的可能性(又帶來了無限的「刺激」,自己好好去體會)。

 

好了,避免頭大,就刨到這兒吧,感興趣的可自行順著思路繼續刨下去。

4. 寄語,寫在最後

作為研發人員:要擁有一雙善於發現的眼睛,要善於發現程式碼之美。

作為研發人員:要時常思考面對當前的專案,是否能夠簡單重構讓程式跑的更順溜。

作為研發人員:要多看、多悟、多提煉、多實踐。

作為研發人員:請不要放棄程式碼,因為程式終會鑄就人生。

本次分享就到這裡,希望對你有所幫助吧。一起聊技術、談業務、噴架構,少走彎路,不踩大坑。

會持續輸出原創精彩分享,敬請期待!關注同名公眾號:一猿小講,回覆「1024」可以獲取精心為您準備的職場打怪進階資料。

相關文章