<rt id="bn8ez"></rt>
<label id="bn8ez"></label>

  • <span id="bn8ez"></span>

    <label id="bn8ez"><meter id="bn8ez"></meter></label>

    瘋狂

    STANDING ON THE SHOULDERS OF GIANTS
    posts - 481, comments - 486, trackbacks - 0, articles - 1
      BlogJava :: 首頁 :: 新隨筆 :: 聯系 :: 聚合  :: 管理

    Netty優雅退出機制和原理(轉)

    Posted on 2016-07-13 09:39 瘋狂 閱讀(1317) 評論(0)  編輯  收藏 所屬分類: netty
    轉載自:http://www.infoq.com/cn/articles/netty-elegant-exit-mechanism-and-principles

    1.進程的優雅退出

    1.1.Kill -9 PID帶來的問題

    在Linux上通常會通過kill -9 pid的方式強制將某個進程殺掉,這種方式簡單高效,因此很多程序的停止腳本經常會選擇使用kill -9 pid的方式。

    無論是Linux的Kill -9 pid還是windows的taskkill /f /pid強制進程退出,都會帶來一些副作用:對應用軟件而言其效果等同于突然掉電,可能會導致如下一些問題:

    1. 緩存中的數據尚未持久化到磁盤中,導致數據丟失;
    2. 正在進行文件的write操作,沒有更新完成,突然退出,導致文件損壞;
    3. 線程的消息隊列中尚有接收到的請求消息還沒來得及處理,導致請求消息丟失;
    4. 數據庫操作已經完成,例如賬戶余額更新,準備返回應答消息給客戶端時,消息尚在通信線程的發送隊列中排隊等待發送,進程強制退出導致應答消息沒有返回給客戶端,客戶端發起超時重試,會帶來重復更新問題;
    5. 其它問題等...

    1.2.JAVA優雅退出

    Java的優雅停機通常通過注冊JDK的ShutdownHook來實現,當系統接收到退出指令后,首先標記系統處于退出狀態,不再接收新的消息,然后將積壓的消息處理完,最后調用資源回收接口將資源銷毀,最后各線程退出執行。

    通常優雅退出需要有超時控制機制,例如30S,如果到達超時時間仍然沒有完成退出前的資源回收等操作,則由停機腳本直接調用kill -9 pid,強制退出。

    2. 如何實現Netty的優雅退出

    要實現Netty的優雅退出,首先需要了解通用Java進程的優雅退出如何實現。下面我們先講解下優雅退出的實現原理,并結合實際代碼進行講解。最后看下如何實現Netty的優雅退出。

    2.0.1. 信號簡介

    信號是在軟件層次上對中斷機制的一種模擬,在原理上,一個進程收到一個信號與處理器收到一個中斷請求可以說是一樣的,它是進程間一種異步通信的機制。以Linux的kill命令為例,kill -s SIGKILL pid (即kill -9 pid) 立即殺死指定pid的進程,SIGKILL就是發送給pid進程的信號。

    信號具有平臺相關性,Linux平臺支持的一些終止進程信號如下所示:

     

    信號名稱

    用途

    SIGKILL

    終止進程,強制殺死進程

    SIGTERM

    終止進程,軟件終止信號

    SIGTSTP

    停止進程,終端來的停止信號

    SIGPROF

    終止進程,統計分布圖用計時器到時

    SIGUSR1

    終止進程,用戶定義信號1

    SIGUSR2

    終止進程,用戶定義信號2

    SIGINT

    終止進程,中斷進程

    SIGQUIT

    建立CORE文件終止進程,并且生成core文件

    Windows平臺存在一些差異,它的一些信號舉例如下:SIGINT(Ctrl+C中斷)、SIGILL、SIGTERM (kill發出的軟件終止)、SIGBREAK (Ctrl+Break中斷)。

    信號選擇:為了不干擾正常信號的運作,又能模擬Java異步通知,在Linux上我們需要先選定一種特殊的信號。通過查看信號列表上的描述,發現 SIGUSR1 和 SIGUSR2 是允許用戶自定義的信號,我們可以選擇SIGUSR2,為了測試方便,在Windows上我們可以選擇SIGINT。

    2.0.2. Java程序的優雅退出

    首先看下通用的Java進程優雅退出的流程圖:

    第一步,應用進程啟動的時候,初始化Signal實例,它的代碼示例如下:

    Signal sig = new Signal(getOSSignalType());

    其中Signal構造函數的參數為String字符串,也就是2.1.1小節中介紹的信號量名稱。

    第二步,根據操作系統的名稱來獲取對應的信號名稱,代碼如下:

    private String getOSSignalType()    {        return System.getProperties().getProperty("os.name"). 		 toLowerCase().startsWith("win") ? "INT" : "USR2";     }

    判斷是否是windows操作系統,如果是則選擇SIGINT,接收Ctrl+C中斷的指令;否則選擇USR2信號,接收SIGUSR2(等價于kill -12 pid)指令。

    第三步,將實例化之后的SignalHandler注冊到JDK的Signal,一旦Java進程接收到kill -12 或者 Ctrl+C則回調handle接口,代碼示例如下:

    Signal.handle(sig, shutdownHandler);

    其中shutdownHandler實現了SignalHandler接口的handle(Signal sgin)方法,代碼示例如下:

    第四步,在接收到信號回調的handle接口中,初始化JDK的ShutdownHook線程,并將其注冊到Runtime中,示例代碼如下:

    private void invokeShutdownHook()  { 	Thread t = new Thread(new ShutdownHook(), "ShutdownHook-Thread"); 	Runtime.getRuntime().addShutdownHook(t);  }

    第五步,接收到進程退出信號后,在回調的handle接口中執行虛擬機的退出操作,示例代碼如下:

    Runtime.getRuntime().exit(0);

    虛擬機退出時,底層會自動檢測用戶是否注冊了ShutdownHook任務,如果有,則會自動將ShutdownHook線程拉起,執行它的Run方法,用戶只需要在ShutdownHook中執行資源釋放操作即可,示例代碼如下:

    class ShutdownHook implements Runnable { 	@Override 	public void run() { 		System.out.println("ShutdownHook execute start..."); 		System.out.print("Netty NioEventLoopGroup shutdownGracefully..."); 		try { 			TimeUnit.SECONDS.sleep(10);//模擬應用進程退出前的處理操作 		} catch (InterruptedException e) { 				e.printStackTrace(); 		} 		System.out.println("ShutdownHook execute end..."); 	System.out.println("Sytem shutdown over, the cost time is 10000MS"); 		} }

    下面我們在Windows環境中對通用的Java優雅退出程序進行測試,打開CMD控制臺,拉起待測試程序,如下所示:

    啟動進程:

    查看線程信息,發現注冊的ShutdownHook線程沒有啟動,符合預期:

    在控制臺執行Ctrl+C,使進程退出,示例如下:

    如上圖所示,我們定義的ShutdownHook線程在JVM退出時被執行,作為測試程序,它休眠10S之后退出,控制臺打印的相關信息如下:

    下面我們總結下通用的Java程序優雅退出的技術要點:

    2.0.3. Netty的優雅退出

    在實際項目中,Netty作為高性能的異步NIO通信框架,往往用作基礎通信框架負責各種協議的接入、解析和調度等,例如在RPC和分布式服務框架中,往往會使用Netty作為內部私有協議的基礎通信框架。

    當應用進程優雅退出時,作為通信框架的Netty也需要優雅退出,主要原因如下:

    1. 盡快的釋放NIO線程、句柄等資源;
    2. 如果使用flush做批量消息發送,需要將積攢在發送隊列中的待發送消息發送完成;
    3. 正在write或者read的消息,需要繼續處理;
    4. 設置在NioEventLoop線程調度器中的定時任務,需要執行或者清理。

    下面我們看下Netty優雅退出涉及的主要操作和資源對象:

    Netty的優雅退出總結起來有三大步操作:

    1. 把NIO線程的狀態位設置成ST_SHUTTING_DOWN狀態,不再處理新的消息(不允許再對外發送消息);
    2. 退出前的預處理操作:把發送隊列中尚未發送或者正在發送的消息發送完、把已經到期或者在退出超時之前到期的定時任務執行完成、把用戶注冊到NIO線程的退出Hook任務執行完成;
    3. 資源的釋放操作:所有Channel的釋放、多路復用器的去注冊和關閉、所有隊列和定時任務的清空取消,最后是NIO線程的退出。

    下面我們具體看下如何實現Netty的優雅退出:

    Netty優雅退出的接口和總入口在EventLoopGroup,調用它的shutdownGracefully方法即可,相關代碼如下:

    bossGroup.shutdownGracefully();  workerGroup.shutdownGracefully();

    除了無參的shutdownGracefully方法,還可以指定退出的超時時間和周期,相關接口定義如下:

    EventLoopGroup的shutdownGracefully工作原理下個章節做詳細講解,結合Java通用的優雅退出機制,即可實現Netty的優雅退出,相關偽代碼如下:

    //統一定義JVM退出事件,并將JVM退出事件作為主題對進程內部發布 //所有需要優雅退出的消費者訂閱JVM退出事件主題 //監聽JVM退出的ShutdownHook被啟動之后,發布JVM退出事件 //消費者監聽到JVM退出事件,開始執行自身的優雅退出 //如果所有的非守護線程都成功完成優雅退出,進程主動退出 //如果到了退出的超時時間仍然沒正常退出,則由停機腳本通過kill -9 pid強殺進程,強制退出

    總結一下:JVM的ShutdownHook被觸發之后,調用所有EventLoopGroup實例的shutdownGracefully方法進行優雅退出。由于Netty自身對優雅退出有較完善的支持,所以實現起來相對比較簡單。

    2.0.4. 一些誤區

    在實際工作中,由于對優雅退出和資源釋放的原理不太清楚,或者對Netty的接口不太了解,很容易把優雅退出和資源釋放混淆,導致出現各種問題。

    如下案例:本意是想把某個Channel關閉,但是卻調用了Channel關聯的EventLoop的shutdownGracefully,導致把EventLoop線程和注冊在該線程持有的多路復用器上所有的Channel都關閉了,錯誤代碼如下所示:

    ctx.channel().eventLoop().shutdownGracefully();

    正確的做法如下所示:調用channel的close方法,關閉鏈路,釋放與該Channel相關的資源:

    ctx.channel().close();

    除非是整個進程優雅退出,一般情況下不會調用EventLoopGroup和EventLoop的shutdownGracefully方法,更多的是鏈路channel的關閉和資源釋放。

    3. Netty優雅退出原理分析

    Netty優雅退出涉及到線程組、線程、鏈路、定時任務等,底層實現細節非常復雜,下面我們就層層分解,通過源碼來剖析它的實現原理。

    3.1. NioEventLoopGroup

    NioEventLoopGroup實際是NioEventLoop的線程組,它的優雅退出比較簡單,直接遍歷EventLoop數組,循環調用它們的shutdownGracefully方法,源碼如下:

    3.2. NioEventLoop

    調用NioEventLoop的shutdownGracefully方法,首先就是要修改線程狀態為正在關閉狀態,它的實現在父類SingleThreadEventExecutor中,它們的繼承關系如下:

    SingleThreadEventExecutor的shutdownGracefully代碼比較簡單,就是修改線程的狀態位,需要注意的是修改時需要對并發調用做判斷,如果是由NioEventLoop自身調用,則不需要加鎖,否則需要加鎖,代碼如下:

    解釋下為什么要加鎖,因為shutdownGracefully是public的方法,任何能夠獲取到NioEventLoop的代碼都可以調用它,在Netty中,業務代碼通常不需要直接獲取NioEventLoop并操作它,但是Netty對NioEventLoop做了比較厚的封裝,它不僅僅只能讀寫消息,還能夠執行定時任務,并作為線程池執行用戶自定義Task。因此在Channel中將獲取NioEventLoop的方法開放了出來,這就意味著用戶只要能夠獲取到Channel,理論上就會存在并發執行shutdownGracefully的可能,因此在優雅退出的時候做了并發保護。

    完成狀態修改之后,剩下的操作主要在NioEventLoop中進行,代碼如下:

    我們繼續看下closeAll的實現,它的原理是把注冊在selector上的所有Channel都關閉,但是有些Channel正在發送消息,暫時還不能關,需要稍后再執行,核心代碼如下:

    循環調用Channel Unsafe的close方法,下面我們跳轉到Unsafe中,對close方法進行分析。

    3.3. AbstractUnsafe

    AbstractUnsafe的close方法主要做了如下幾件事:

    1.判斷當前該鏈路是否有消息正在發送,如果有則將關閉操作封裝成Task放到eventLoop中稍后再執行:

    2.將發送隊列清空,不再允許發送新的消息:

    3.調用SocketChannel的close方法,關閉鏈路:

    4.調用pipeline的fireChannelInactive,觸發鏈路關閉通知事件:

    5.最后是調用deregister,從多路復用器上取消SelectionKey:

    至此,優雅退出流程已經完成,這是否意味著NioEventLoop線程可以退出了,其實并非如此。

    在此處,只是做了Channel的關閉和從Selector上的去注冊,總結如下:

    1. 通過inFlush0來判斷當前是否正在發送消息,如果是,則不執行Channel關閉動作,放入NIO線程的任務隊列中稍后再執行close()操作;
    2. 因為已經不允許新的發送消息加入,一旦發送操作完成,就執行鏈路關閉、觸發鏈路關閉事件和從Selector上取消注冊操作。

    之前已經說了,NioEventLoop除了I/O讀寫之外,還兼具定時任務執行、關閉ShutdownHook的執行等,如果此時有到期的定時任務,即使Chanel已經關閉,但是仍然需要繼續執行,線程不能退出。下面我們具體分析下TaskQueue的處理流程。

    3.4. TaskQueue

    NioEventLoop執行完closeAll()操作之后,需要調用confirmShutdown看是否真的能夠退出,它的處理邏輯如下:

    1.執行TaskQueue中排隊的Task,代碼如下:

    2.執行注冊到NioEventLoop中的ShutdownHook,代碼如下:

    3.判斷是否到達優雅退出的指定超時時間,如果達到或者過了超時時間,則立即退出,代碼如下:

    4.如果沒到達指定的超時時間,暫時不退出,每隔100MS檢測下是否有新的任務加入,有則繼續執行:

    在confirmShutdown方法中,夾雜了一些對已經廢棄的shutdown()方法的處理,例如:

    調用新的shutdownGracefully系列方法,該判斷條件是永遠都不會成立的,因此對于已經廢棄的shutdown相關的處理邏輯,不再詳細分析。

    到此為止,confirmShutdown方法講解完畢,confirmShutdown返回true,則NioEventLoop線程正式退出,Netty的優雅退出完成,代碼如下:

    3.5. 疑問解答

    3.5.1. runAllTasks重復執行問題

    在NioEventLoop的run方法中,已經調用了runAllTasks方法,為何緊隨其后,在confirmShutdown中有繼續調用runAllTasks方法呢,疑問代碼如下:

    原因主要有兩個:

    1.為了防止定時任務Task或者用戶自定義的線程Task的執行過多占用NioEventLoop線程的調度資源,Netty對NioEventLoop線程I/O操作和非I/O操作時間做了比例限制,即限制非I/O操作的執行時間,如上圖紅框中代碼所示。有了執行時間限制,因此可能會導致已經到期的定時任務、普通任務沒有執行完,需要等待下次Selector輪詢繼續執行。在線程退出之前,需要對本該執行但是沒有執行完成的Task進行掃尾處理,所以在confirmShutdown中再次調用了runAllTasks方法;

    2.在調用runAllTasks方法之后,執行confirmShutdown之前,用戶向NioEventLoop中添加了新的普通任務或者定時任務,因此需要在退出之前再次遍歷并處理一遍Task Queue。

    3.5.2. 優雅退出是否能夠保證所有在通信線程排隊的消息全部發送出去

    實際是無法保證的,它只能保證如果現在正在發送消息過程中,調用了優雅退出方法,此時不會關閉鏈路,繼續發送,如果發送操作完成,無論是否還有消息尚未發送出去,在下一輪Selector的輪詢中,鏈路將會關閉,沒有發送完成的消息將會被丟棄,甚至是半包消息。它的處理原理圖如下:

    它的原理比較復雜,現對主要邏輯處理進行解讀:

    1. 調用優雅退出之后,是否關閉鏈路,判斷標準是inFlush0是否為true,如果為False,則會執行鏈路關閉操作;
    2. 如果用戶是類似批量發送,例如每達到N條或者定時觸發flush操作,則在此期間調用優雅退出方法,inFlush0為False,鏈路關閉,積壓的待發送消息會被丟棄掉;
    3. 如果優雅退出時鏈路正好在發送消息過程中,則它不會立即退出,等待發送完成之后,下次Selector輪詢的時候才退出。在這種場景下,又有兩種可能的場景:

    場景A:如果一次把積壓的消息全部發送完,沒有發生寫半包,則不會發生消息丟失;

    場景B:如果一次沒有把消息發送完成,此時Netty會監聽寫事件,觸發Selector的下一次輪詢并發送消息,代碼如下:

    Selector輪詢時,首先處理讀寫事件,然后再處理定時任務和普通任務,因此在鏈路關閉之前,還有最后一次繼續發送的機會,代碼如下:

    如果非常不幸,再次發送仍然沒有把積壓的消息全部發送完畢,再次發生了寫半包,那無論是否有積壓消息,執行AbstractUnsafe.close的Task還是會把鏈路給關閉掉,原因是只要完成一次消息發送操作,Netty就會把inFlush0置為false,代碼如下:

    鏈路關閉之后,所有尚未發送的消息都將被丟棄。

    可能有些讀者會有疑問,如果在第二次發送之后,執行AbstractUnsafe.close之前,業務正好又調用了flush操作,inFlush0是否會被修改成True呢?這個是不可能的,因為從Netty 4.X之后線程模型發生了變更,flush操作不是由用戶線程執行,而是由Channel對應的NioEventLoop線程執行,所以在兩者之間不會發生inFlush0被修改的情況。

    Netty 4.X之后的線程模型如下所示:

    另外,由于優雅退出有超時時間,如果在超時時間內沒有完成積壓消息的發送,也會發生消息丟棄的情況。

    對于上述場景,需要應用層來保證相關的可靠性,或者對Netty的優雅退出機制進行優化。

    4. 作者簡介

    李林鋒,2007年畢業于東北大學,2008年進入華為公司從事電信軟件的設計和開發工作,有多年Java NIO、平臺中間件設計和開發經驗,精通Netty、Mina、分布式服務框架等,《Netty權威指南》、《分布式服務框架原理與實踐》作者。目前從事云平臺相關的架構和設計工作。


    只有注冊用戶登錄后才能發表評論。


    網站導航:
     
    主站蜘蛛池模板: 一区二区三区在线免费观看视频| 8x网站免费入口在线观看| 国产成人综合亚洲AV第一页 | 黄色网址免费在线观看| 亚洲AV日韩精品久久久久| 69天堂人成无码麻豆免费视频| 亚洲成a∨人片在无码2023| 亚洲中文字幕无码爆乳AV| 亚洲免费福利视频| 国产精品成人亚洲| 亚洲精品福利视频| 国产又黄又爽又刺激的免费网址| 9i9精品国产免费久久| 亚洲偷自精品三十六区| 色噜噜亚洲精品中文字幕| 真人做人试看60分钟免费视频 | a级午夜毛片免费一区二区| 亚洲a视频在线观看| 亚洲色成人网站WWW永久| 免费一本色道久久一区| 在线观看免费黄网站| 亚洲日韩精品无码AV海量| 国产成A人亚洲精V品无码性色| 成人免费视频小说| 久久永久免费人妻精品| 国产精品亚洲一区二区在线观看| 亚洲人成在线播放网站岛国| www.亚洲色图| 成人免费视频网址| 真实国产乱子伦精品免费| 亚欧乱色国产精品免费视频| 一本色道久久88—综合亚洲精品| 久久精品国产亚洲av麻| 亚洲麻豆精品国偷自产在线91| 色婷婷7777免费视频在线观看| 免费看无码特级毛片| 欧洲精品码一区二区三区免费看| 亚洲AV无码专区在线亚| 麻豆亚洲AV永久无码精品久久| 亚洲综合另类小说色区| 免费jlzzjlzz在线播放视频|