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


在 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」可以獲取精心為您准備的職場打怪進階資料。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM