1. 程式人生 > >netty可靠性

netty可靠性

鏈路 非正常關閉 阻塞i/o 處理過程 心跳機制 source 統一 問題 限制

Netty的可靠性

首先,我們要從Netty的主要用途來分析它的可靠性,Netty目前的主流用法有三種:

1) 構建RPC調用的基礎通信組件,提供跨節點的遠程服務調用能力;

2) NIO通信框架,用於跨節點的數據交換;

3) 其它應用協議棧的基礎通信組件,例如HTTP協議以及其它基於Netty開發的應用層協議棧。

以阿裏的分布式服務框架Dubbo為例,Netty是Dubbo RPC框架的核心。它的服務調用示例圖如下:

技術分享

圖1-1 Dubbo的節點角色說明圖

其中,服務提供者和服務調用者之間可以通過Dubbo協議進行RPC調用,消息的收發默認通過Netty完成。

通過對Netty主流應用場景的分析,我們發現Netty面臨的可靠性問題大致分為三類:

1) 傳統的網絡I/O故障,例如網絡閃斷、防火墻Hang住連接、網絡超時等;

2) NIO特有的故障,例如NIO類庫特有的BUG、讀寫半包處理異常、Reactor線程跑飛等等;

3) 編解碼相關的異常。

在大多數的業務應用場景中,一旦因為某些故障導致Netty不能正常工作,業務往往會陷入癱瘓。所以,從業務訴求來看,對Netty框架的可靠性要求是非常的高。作為當前業界最流行的一款NIO框架,Netty在不同行業和領域都得到了廣泛的應用,它的高可靠性已經得到了成百上千的生產系統檢驗。

Netty是如何支持系統高可靠性的?下面,我們就從幾個不同維度出發一探究竟。

2. Netty高可靠性之道

2.1. 網絡通信類故障

2.1.1. 客戶端連接超時

在傳統的同步阻塞編程模式下,客戶端Socket發起網絡連接,往往需要指定連接超時時間,這樣做的目的主要有兩個:

1) 在同步阻塞I/O模型中,連接操作是同步阻塞的,如果不設置超時時間,客戶端I/O線程可能會被長時間阻塞,這會導致系統可用I/O線程數的減少;

2) 業務層需要:大多數系統都會對業務流程執行時間有限制,例如WEB交互類的響應時間要小於3S。客戶端設置連接超時時間是為了實現業務層的超時。

JDK原生的Socket連接接口定義如下:

技術分享

圖2-1 JDK Socket連接超時接口

對於NIO的SocketChannel,在非阻塞模式下,它會直接返回連接結果,如果沒有連接成功,也沒有發生IO異常,則需要將SocketChannel註冊到Selector上監聽連接結果。所以,異步連接的超時無法在API層面直接設置,而是需要通過定時器來主動監測。

下面我們首先看下JDK NIO類庫的SocketChannel連接接口定義:

技術分享

圖2-2 JDK NIO 類庫SocketChannel連接接口

從上面的接口定義可以看出,NIO類庫並沒有現成的連接超時接口供用戶直接使用,如果要在NIO編程中支持連接超時,往往需要NIO框架或者用戶自己封裝實現。

下面我們看下Netty是如何支持連接超時的,首先,在創建NIO客戶端的時候,可以配置連接超時參數:

技術分享

圖2-3 Netty客戶端創建支持設置連接超時參數

設置完連接超時之後,Netty在發起連接的時候,會根據超時時間創建ScheduledFuture掛載在Reactor線程上,用於定時監測是否發生連接超時,相關代碼如下:

技術分享

圖2-4 根據連接超時創建超時監測定時任務

創建連接超時定時任務之後,會由NioEventLoop負責執行。如果已經連接超時,但是服務端仍然沒有返回TCP握手應答,則關閉連接,代碼如上圖所示。

如果在超時期限內處理完成連接操作,則取消連接超時定時任務,相關代碼如下:

技術分享

圖2-5 取消連接超時定時任務

Netty的客戶端連接超時參數與其它常用的TCP參數一起配置,使用起來非常方便,上層用戶不用關心底層的超時實現機制。這既滿足了用戶的個性化需求,又實現了故障的分層隔離。

2.1.2. 通信對端強制關閉連接

在客戶端和服務端正常通信過程中,如果發生網絡閃斷、對方進程突然宕機或者其它非正常關閉鏈路事件時,TCP鏈路就會發生異常。由於TCP是全雙工的,通信雙方都需要關閉和釋放Socket句柄才不會發生句柄的泄漏。

在實際的NIO編程過程中,我們經常會發現由於句柄沒有被及時關閉導致的功能和可靠性問題。究其原因總結如下:

1) IO的讀寫等操作並非僅僅集中在Reactor線程內部,用戶上層的一些定制行為可能會導致IO操作的外逸,例如業務自定義心跳機制。這些定制行為加大了統一異常處理的難度,IO操作越發散,故障發生的概率就越大;

2) 一些異常分支沒有考慮到,由於外部環境誘因導致程序進入這些分支,就會引起故障。

下面我們通過故障模擬,看Netty是如何處理對端鏈路強制關閉異常的。首先啟動Netty服務端和客戶端,TCP鏈路建立成功之後,雙方維持該鏈路,查看鏈路狀態,結果如下:

技術分享

圖2-6 Netty服務端和客戶端TCP鏈路狀態正常

強制關閉客戶端,模擬客戶端宕機,服務端控制臺打印如下異常:

技術分享

圖2-7 模擬TCP鏈路故障

從堆棧信息可以判斷,服務端已經監控到客戶端強制關閉了連接,下面我們看下服務端是否已經釋放了連接句柄,再次執行netstat命令,執行結果如下:

技術分享

圖2-8 查看故障鏈路狀態

從執行結果可以看出,服務端已經關閉了和客戶端的TCP連接,句柄資源正常釋放。由此可以得出結論,Netty底層已經自動對該故障進行了處理。

下面我們一起看下Netty是如何感知到鏈路關閉異常並進行正確處理的,查看AbstractByteBuf的writeBytes方法,它負責將指定Channel的緩沖區數據寫入到ByteBuf中,詳細代碼如下:

技術分享

圖2-9 AbstractByteBuf的writeBytes方法

在調用SocketChannel的read方法時發生了IOException,代碼如下:

技術分享

圖2-10 讀取緩沖區數據發生IO異常

為了保證IO異常被統一處理,該異常向上拋,由AbstractNioByteChannel進行統一異常處理,代碼如下:

技術分享

圖2-11 鏈路異常退出異常處理

為了能夠對異常策略進行統一,也為了方便維護,防止處理不當導致的句柄泄漏等問題,句柄的關閉,統一調用AbstractChannel的close方法,代碼如下:

技術分享

圖2-12 統一的Socket句柄關閉接口

2.1.3. 正常的連接關閉

對於短連接協議,例如HTTP協議,通信雙方數據交互完成之後,通常按照雙方的約定由服務端關閉連接,客戶端獲得TCP連接關閉請求之後,關閉自身的Socket連接,雙方正式斷開連接。

在實際的NIO編程過程中,經常存在一種誤區:認為只要是對方關閉連接,就會發生IO異常,捕獲IO異常之後再關閉連接即可。實際上,連接的合法關閉不會發生IO異常,它是一種正常場景,如果遺漏了該場景的判斷和處理就會導致連接句柄泄漏。

下面我們一起模擬故障,看Netty是如何處理的。測試場景設計如下:改造下Netty客戶端,雙發鏈路建立成功之後,等待120S,客戶端正常關閉鏈路。看服務端是否能夠感知並釋放句柄資源。

首先啟動Netty客戶端和服務端,雙方TCP鏈路連接正常:

技術分享

圖2-13 TCP連接狀態正常

120S之後,客戶端關閉連接,進程退出,為了能夠看到整個處理過程,我們在服務端的Reactor線程處設置斷點,先不做處理,此時鏈路狀態如下:

技術分享

圖2-14 TCP連接句柄等待釋放

從上圖可以看出,此時服務端並沒有關閉Socket連接,鏈路處於CLOSE_WAIT狀態,放開代碼讓服務端執行完,結果如下:

技術分享

圖2-15 TCP連接句柄正常釋放

下面我們一起看下服務端是如何判斷出客戶端關閉連接的,當連接被對方合法關閉後,被關閉的SocketChannel會處於就緒狀態,SocketChannel的read操作返回值為-1,說明連接已經被關閉,代碼如下:

技術分享

圖2-16 需要對讀取的字節數進行判斷

如果SocketChannel被設置為非阻塞,則它的read操作可能返回三個值:

1) 大於0,表示讀取到了字節數;

2) 等於0,沒有讀取到消息,可能TCP處於Keep-Alive狀態,接收到的是TCP握手消息;

3) -1,連接已經被對方合法關閉。

通過調試,我們發現,NIO類庫的返回值確實為-1:

技術分享

圖2-17 鏈路正常關閉,返回值為-1

得知連接關閉之後,Netty將關閉操作位設置為true,關閉句柄,代碼如下:

技術分享

圖2-18 連接正常關閉,釋放資源

2.1.4. 故障定制

在大多數場景下,當底層網絡發生故障的時候,應該由底層的NIO框架負責釋放資源,處理異常等。上層的業務應用不需要關心底層的處理細節。但是,在一些特殊的場景下,用戶可能需要感知這些異常,並針對這些異常進行定制處理,例如:

1) 客戶端的斷連重連機制;

2) 消息的緩存重發;

3) 接口日誌中詳細記錄故障細節;

4) 運維相關功能,例如告警、觸發郵件/短信等

Netty的處理策略是發生IO異常,底層的資源由它負責釋放,同時將異常堆棧信息以事件的形式通知給上層用戶,由用戶對異常進行定制。這種處理機制既保證了異常處理的安全性,也向上層提供了靈活的定制能力。

具體接口定義以及默認實現如下:

技術分享

圖2-19 故障定制接口

用戶可以覆蓋該接口,進行個性化的異常定制。例如發起重連等。

netty可靠性