△Hollis, 一個對Coding有著獨特追求的人△ 這是Hollis的第 350 篇原創(chuàng)分享 來源 l Hollis(ID:hollischuang) 最近無論是校招還是社招,都進(jìn)行的如火如荼,我也承擔(dān)了很多的面試工作,在一次面試過程中,和候選人聊了一些關(guān)于Dubbo的知識。 Dubbo是一個比較著名的RPC框架,很多人對于他的一些網(wǎng)絡(luò)通信、通信協(xié)議、動態(tài)代理等等都有一定的了解,這位候選人也一樣。 但是,我接下來問了他一個問題:你們在使用Dubbo的時候,應(yīng)用如果重啟,怎么保證一個請求不會被中斷處理的呢? 他沒怎么說的上來,我以為他不理解我的問題,我接著問他:我就是想問下Dubbo是如何做優(yōu)雅上下線的你知道嗎? 關(guān)于"優(yōu)雅上下線"這個詞,我沒找到官方的解釋,我嘗試解釋一下這是什么。 首先,上線、下線大家一定都很清楚,比如我們一次應(yīng)用發(fā)布過程中,就需要先將應(yīng)用服務(wù)停掉,然后再把服務(wù)啟動起來。這個過程就包含了一次下線和一次上線。 1、服務(wù)停止時,沒有關(guān)閉對應(yīng)的監(jiān)控,導(dǎo)致應(yīng)用停止后發(fā)生大量報警。 2、應(yīng)用停止時,沒有通知外部調(diào)用方,很多請求還會過來,導(dǎo)致很多調(diào)用失敗。 3、應(yīng)用停止時,有線程正在執(zhí)行中,執(zhí)行了一半,JVM進(jìn)程就被干掉了。 4、應(yīng)用啟動時,服務(wù)還沒準(zhǔn)備好,就開始對外提供服務(wù),導(dǎo)致很多失敗調(diào)用。 5、應(yīng)用啟動時,沒有檢查應(yīng)用的健康狀態(tài),就開始對外提供服務(wù),導(dǎo)致很多失敗調(diào)用。 以上,都是我們認(rèn)為的不優(yōu)雅的情況,那么,反過來,優(yōu)雅上下線就是一種避免上述情況發(fā)生的手段。 一個應(yīng)用的優(yōu)雅上下線涉及到的內(nèi)容其實有很多,從底層的操作系統(tǒng)、容器層面,到編程語言、框架層面,再到應(yīng)用架構(gòu)層面,涉及到的知識很廣泛。 其實,優(yōu)雅上下線中,最重要的還是優(yōu)雅下線。因為如果下線過程不優(yōu)雅的話,就會發(fā)生很多調(diào)用失敗了、服務(wù)找不到等問題。所以很多時候,大家也會提優(yōu)雅停機(jī)這樣的概念。 本文后面介紹的優(yōu)雅上下線也重點關(guān)注優(yōu)雅停機(jī)的過程。 我們知道, kill -9 之所以不建議使用,是因為 kill -9 特別強(qiáng)硬,系統(tǒng)會發(fā)出SIGKILL信號,他要求接收到該信號的程序應(yīng)該立即結(jié)束運行,不能被阻塞或者忽略。 這個過程顯然是不優(yōu)雅的,因為應(yīng)用立刻停止的話,就沒辦法做收尾動作。而更優(yōu)雅的方式是 kill -15 。 當(dāng)使用 kill -15 時,系統(tǒng)會發(fā)送一個SIGTERM的信號給對應(yīng)的程序。當(dāng)程序接收到該信號后,具體要如何處理是自己可以決定的。 kill -15 會通知到應(yīng)用程序,這就是操作系統(tǒng)對于優(yōu)雅上下線的最基本的支持。 以前,在操作系統(tǒng)之上就是應(yīng)用程序了,但是,自從容器化技術(shù)推出之后,在操作系統(tǒng)和應(yīng)用程序之間,多了一個容器層,而Docker、k8s等容器其實也是支持優(yōu)雅上下線的。 如Docker中同樣提供了兩個命令, docker stop 和 docker kill docker stop 就像 kill -15 一樣 ,他會向容器內(nèi)的進(jìn)程發(fā)送SIGTERM信號,在10S之后(可通過參數(shù)指定)再發(fā)送SIGKILL信號。 而 docker kill 就像 kill -9 ,直接發(fā)送SIGKILL信號。 在操作系統(tǒng)、容器等對優(yōu)雅上下線有了基本的支持之后,在接收到 docker stop 、 kill -15 等命令后,會通知應(yīng)用進(jìn)程進(jìn)行進(jìn)程關(guān)閉。 而Java應(yīng)用在運行時就是一個獨立運行的進(jìn)程,這個進(jìn)程是如何關(guān)閉的呢? Java程序的終止運行是基于JVM的關(guān)閉實現(xiàn)的,JVM關(guān)閉方式分為正常關(guān)閉、強(qiáng)制關(guān)閉和異常關(guān)閉3種。 這其中,正常關(guān)閉就是支持優(yōu)雅上下線的 。正常關(guān)閉過程中,JVM可以做一些清理動作,比如刪除臨時文件。 當(dāng)然,開發(fā)者也是可以自定義做一些額外的事情的,比如通知應(yīng)用框架優(yōu)雅上下線操作。 而這種機(jī)制是通過JDK中提供的shutdown hook實現(xiàn)的。JDK提供了Java.Runtime.addShutdownHook(Thread hook)方法,可以注冊一個JVM關(guān)閉的鉤子。 package com.hollis; public class ShutdownHookTest { public static void main (String[] args) { boolean flag = true ; Runtime.getRuntime().addShutdownHook(new Thread(() -> { System.out.println("hook execute..." ); })); while (flag) { // app is runing } System.out.println("main thread execute end..." ); } }
執(zhí)行命令:
jps 6520 ShutdownHookTest 6521 Jps kill 6520
控制臺輸出內(nèi)容:
hook execute... Process finished with exit code 143 (interrupted by signal 15 : SIGTERM)
可以看到,當(dāng)我們使用kill(默認(rèn)kill -15)關(guān)閉進(jìn)程的時候,程序會先執(zhí)行我注冊的shutdownHook,然后再退出 ,并且會給出一個提示: interrupted by signal 15: SIGTERM 有了JVM提供的shutdown hook之后,很多框架都可以通過這個機(jī)制來做優(yōu)雅下線的支持。 比如Spring,他就會向JVM注冊一個shutdown hook,在接收到關(guān)閉通知的時候,進(jìn)行bean的銷毀,容器的銷毀處理等操作。 同時,作為一個成熟的框架,Spring也提供了事件機(jī)制,可以借助這個機(jī)制實現(xiàn)更多的優(yōu)雅上下線功能。 ApplicationListener是Spring事件機(jī)制的一部分,與抽象類ApplicationEvent類配合來完成ApplicationContext的事件機(jī)制。 開發(fā)者可以實現(xiàn)ApplicationListener接口,監(jiān)聽到 Spring 容器的關(guān)閉事件(ContextClosedEvent),來做一些特殊的處理: @Component public class MyListener implements ApplicationListener <ContextClosedEvent > { @Override public void onApplicationEvent (ContextClosedEvent event) { // 做容器關(guān)閉之前的清理工作 } }
因為Spring中提供了ApplicationListener接口,幫助我們來監(jiān)聽容器關(guān)閉事件,那么,很多web容器、框架等就可以借助這個機(jī)制來做自己的優(yōu)雅上下線操作。 這里簡答說一下Dubbo的,在Dubbo的官網(wǎng)中,有關(guān)于優(yōu)雅停機(jī)的介紹: 應(yīng)用在停機(jī)時,接收到關(guān)閉通知時,會先把自己標(biāo)記為不接受(發(fā)起)新請求,然后再等待10s(默認(rèn)是10秒)的時候,等執(zhí)行中的線程執(zhí)行完。 那么,之所以他能做這些事,是因為從操作系統(tǒng)、到JVM、到Spring等都對優(yōu)雅停機(jī)做了很好的支持。 關(guān)于Dubbo各個版本中具體是如何借助JVM的shutdown hook機(jī)制、或者說Spring的事件機(jī)制的優(yōu)雅停機(jī),我的一位同事的一篇文章介紹的很清晰,大家可以看下: https://www.cnkirito.moe/dubbo-gracefully-shutdown/ 在從Dubbo 2.5 到 Dubbo 2.7介紹了歷史版本中,Dubbo為了解決優(yōu)雅上下線問題所遇到的問題和方案。 目前,Dubbo中實現(xiàn)方式如下,同樣是用到了Spring的事件機(jī)制: public class SpringExtensionFactory implements ExtensionFactory { public static void addApplicationContext (ApplicationContext context) { CONTEXTS.add(context); if (context instanceof ConfigurableApplicationContext) { ((ConfigurableApplicationContext) context).registerShutdownHook(); DubboShutdownHook.getDubboShutdownHook().unregister(); } BeanFactoryUtils.addApplicationListener(context, SHUTDOWN_HOOK_LISTENER); } }
本文從操作系統(tǒng)開始,分別介紹了Linux、Docker、JVM、Spring、Dubbo等對優(yōu)雅停機(jī)的支持。 可以看到,一個簡單的優(yōu)雅停機(jī)功能,上下游需要這么多底層基礎(chǔ)設(shè)施和上層應(yīng)用的支持。 相信通過學(xué)習(xí)本文,你一定對優(yōu)雅上下線有了更多的了解。 除此之外,我還希望你,通過本文以后,遇到一些實際問題的時候,可以想到文中提到的shutdown hook機(jī)制、Spring的event機(jī)制。很多時候,這些機(jī)制都能幫助我們解決很多問題。 我在工作中,就有很多次使用過這樣的機(jī)制的實例,后面有機(jī)會給大家介紹幾個實例。 好了,本文的全部內(nèi)容就是這么多啦,如果對你有幫助,記得一鍵三連哦~ 參考 :
https://zhuanlan.zhihu.com/p/29093407
https://www.cnkirito.moe/dubbo-gracefully-shutdown/
推薦閱讀:
歡 迎 關(guān) 注 微 信 公 眾 號 : 互聯(lián)網(wǎng)全棧架構(gòu) , 收 取 更 多 有 價 值 的 信 息 。