您好,登錄后才能下訂單哦!
這期內容當中小編將會給大家帶來有關怎么進行Netty高可靠性原理分析,文章內容豐富且以專業的角度為大家分析和敘述,閱讀完這篇文章希望大家可以有所收獲。
1. 背景
畢馬威國際(KPMG International)在對46個國家的74家運營商進行調查后發現,全球通信行業每年的收益流失約為400億美元,占總收入的1%-3%。導致收益流失的因素有多種,主要原因就是計費BUG。
美國太平洋時間8月16日下午3點50分到3點55分(北京時間8月17日6點50分到6點55分),谷歌遭遇了宕機。根據事后統計,短短的5分鐘,谷歌損失了54.5萬美元。也就是服務每中斷一分鐘,損失就達10.8萬美元。
2013年,從美國東部時間8月19日下午2點45分開始,有用戶率先發現了亞馬遜網站出現宕機,大約在20多分鐘后又恢復正常。此次宕機讓亞馬遜每分鐘損失近6.7萬美元,在宕機期間,消費者無法通過Amazon.com、亞馬遜移動端以及Amazon.ca等網站進行購物。
軟件可靠性是指在給定時間內,特定環境下軟件無錯運行的概率。軟件可靠性包含了以下三個要素:
1) 規定的時間:軟件可靠性只是體現在其運行階段,所以將運行時間作為規定的時間的度量。運行時間包括軟件系統運行后工作與掛起(開啟但空閑)的累計時間。由于軟件運行的環境與程序路徑選取的隨機性,軟件的失效為隨機事件,所以運行時間屬于隨機變量;
2) 規定的環境條件:環境條件指軟件的運行環境。它涉及軟件系統運行時所需的各種支持要素,如支持硬件、操作系統、其它支持軟件、輸入數據格式和范圍以及操作規程等。不同的環境條件下軟件的可靠性是不同的。具體地說,規定的環境條件主要是描述軟件系統運行時計算機的配置情況以及對輸入數據的要求,并假定其它一切因素都是理想的。有了明確規定的環境條件,還可以有效判斷軟件失效的責任在用戶方還是提供方;
3) 規定的功能:軟件可靠性還與規定的任務和功能有關。由于要完成的任務不同,軟件的運行剖面會有所區別,則調用的子模塊就不同(即程序路徑選擇不同),其可靠性也就可能不同。所以要準確度量軟件系統的可靠性必須首先明確它的任務和功能。
首先,我們要從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是如何支持系統高可靠性的?下面,我們就從幾個不同維度出發一探究竟。
在傳統的同步阻塞編程模式下,客戶端Socket發起網絡連接,往往需要指定連接超時時間,這樣做的目的主要有兩個:
1) 在同步阻塞I/O模型中,連接操作是同步阻塞的,如果不設置超時時間,客戶端I/O線程可能會被長時間阻塞,這會導致系統可用I/O線程數的減少;
2) 業務層需要:大多數系統都會對業務流程執行時間有限制,例如WEB交互類的響應時間要小于3S。客戶端設置連接超時時間是為了實現業務層的超時。
JDK原生的Socket連接接口定義如下:
圖2-2 JDK NIO 類庫SocketChannel連接接口
從上面的接口定義可以看出,NIO類庫并沒有現成的連接超時接口供用戶直接使用,如果要在NIO編程中支持連接超時,往往需要NIO框架或者用戶自己封裝實現。
下面我們看下Netty是如何支持連接超時的,首先,在創建NIO客戶端的時候,可以配置連接超時參數:
圖2-3 Netty客戶端創建支持設置連接超時參數
設置完連接超時之后,Netty在發起連接的時候,會根據超時時間創建ScheduledFuture掛載在Reactor線程上,用于定時監測是否發生連接超時,相關代碼如下:
圖2-4 根據連接超時創建超時監測定時任務
創建連接超時定時任務之后,會由NioEventLoop負責執行。如果已經連接超時,但是服務端仍然沒有返回TCP握手應答,則關閉連接,代碼如上圖所示。
如果在超時期限內處理完成連接操作,則取消連接超時定時任務,相關代碼如下:
圖2-5 取消連接超時定時任務
Netty的客戶端連接超時參數與其它常用的TCP參數一起配置,使用起來非常方便,上層用戶不用關心底層的超時實現機制。這既滿足了用戶的個性化需求,又實現了故障的分層隔離。
在客戶端和服務端正常通信過程中,如果發生網絡閃斷、對方進程突然宕機或者其它非正常關閉鏈路事件時,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句柄關閉接口
對于短連接協議,例如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 連接正常關閉,釋放資源
在大多數場景下,當底層網絡發生故障的時候,應該由底層的NIO框架負責釋放資源,處理異常等。上層的業務應用不需要關心底層的處理細節。但是,在一些特殊的場景下,用戶可能需要感知這些異常,并針對這些異常進行定制處理,例如:
1) 客戶端的斷連重連機制;
2) 消息的緩存重發;
3) 接口日志中詳細記錄故障細節;
4) 運維相關功能,例如告警、觸發郵件/短信等
Netty的處理策略是發生IO異常,底層的資源由它負責釋放,同時將異常堆棧信息以事件的形式通知給上層用戶,由用戶對異常進行定制。這種處理機制既保證了異常處理的安全性,也向上層提供了靈活的定制能力。
具體接口定義以及默認實現如下:
圖2-19 故障定制接口
用戶可以覆蓋該接口,進行個性化的異常定制。例如發起重連等。
當網絡發生單通、連接被防火墻Hang住、長時間GC或者通信線程發生非預期異常時,會導致鏈路不可用且不易被及時發現。特別是異常發生在凌晨業務低谷期間,當早晨業務高峰期到來時,由于鏈路不可用會導致瞬間的大批量業務失敗或者超時,這將對系統的可靠性產生重大的威脅。
從技術層面看,要解決鏈路的可靠性問題,必須周期性的對鏈路進行有效性檢測。目前最流行和通用的做法就是心跳檢測。
心跳檢測機制分為三個層面:
1) TCP層面的心跳檢測,即TCP的Keep-Alive機制,它的作用域是整個TCP協議棧;
2) 協議層的心跳檢測,主要存在于長連接協議中。例如SMPP協議;
3) 應用層的心跳檢測,它主要由各業務產品通過約定方式定時給對方發送心跳消息實現。
心跳檢測的目的就是確認當前鏈路可用,對方活著并且能夠正常接收和發送消息。
做為高可靠的NIO框架,Netty也提供了心跳檢測機制,下面我們一起熟悉下心跳的檢測原理。
圖2-20 心跳檢測機制
不同的協議,心跳檢測機制也存在差異,歸納起來主要分為兩類:
1) Ping-Pong型心跳:由通信一方定時發送Ping消息,對方接收到Ping消息之后,立即返回Pong應答消息給對方,屬于請求-響應型心跳;
2) Ping-Ping型心跳:不區分心跳請求和應答,由通信雙方按照約定定時向對方發送心跳Ping消息,它屬于雙向心跳。
心跳檢測策略如下:
1) 連續N次心跳檢測都沒有收到對方的Pong應答消息或者Ping請求消息,則認為鏈路已經發生邏輯失效,這被稱作心跳超時;
2) 讀取和發送心跳消息的時候如何直接發生了IO異常,說明鏈路已經失效,這被稱為心跳失敗。
無論發生心跳超時還是心跳失敗,都需要關閉鏈路,由客戶端發起重連操作,保證鏈路能夠恢復正常。
Netty的心跳檢測實際上是利用了鏈路空閑檢測機制實現的,相關代碼如下:
圖2-21 心跳檢測的代碼包路徑
Netty提供的空閑檢測機制分為三種:
1) 讀空閑,鏈路持續時間t沒有讀取到任何消息;
2) 寫空閑,鏈路持續時間t沒有發送任何消息;
3) 讀寫空閑,鏈路持續時間t沒有接收或者發送任何消息。
Netty的默認讀寫空閑機制是發生超時異常,關閉連接,但是,我們可以定制它的超時實現機制,以便支持不同的用戶場景。
WriteTimeoutHandler的超時接口如下:
圖2-22 寫超時
ReadTimeoutHandler的超時接口如下:
圖2-23 讀超時
讀寫空閑的接口如下:
圖2-24 讀寫空閑
利用Netty提供的鏈路空閑檢測機制,可以非常靈活的實現協議層的心跳檢測。在《Netty權威指南》中的私有協議棧設計和開發章節,我利用Netty提供的自定義Task接口實現了另一種心跳檢測機制,感興趣的朋友可以參閱該書。
Reactor線程是IO操作的核心,NIO框架的發動機,一旦出現故障,將會導致掛載在其上面的多路用復用器和多個鏈路無法正常工作。因此它的可靠性要求非常高。
筆者就曾經遇到過因為異常處理不當導致Reactor線程跑飛,大量業務請求處理失敗的故障。下面我們一起看下Netty是如何有效提升Reactor線程的可靠性的。
盡管Reactor線程主要處理IO操作,發生的異常通常是IO異常,但是,實際上在一些特殊場景下會發生非IO異常,如果僅僅捕獲IO異常可能就會導致Reactor線程跑飛。為了防止發生這種意外,在循環體內一定要捕獲Throwable,而不是IO異常或者Exception。
Netty的相關代碼如下:
圖2-25 Reactor線程異常保護
捕獲Throwable之后,即便發生了意外未知對異常,線程也不會跑飛,它休眠1S,防止死循環導致的異常繞接,然后繼續恢復執行。這樣處理的核心理念就是:
1) 某個消息的異常不應該導致整條鏈路不可用;
2) 某條鏈路不可用不應該導致其它鏈路不可用;
3) 某個進程不可用不應該導致其它集群節點不可用。
通常情況下,死循環是可檢測、可預防但是無法完全避免的。Reactor線程通常處理的都是IO相關的操作,因此我們重點關注IO層面的死循環。
JDK NIO類庫最著名的就是 epoll bug了,它會導致Selector空輪詢,IO線程CPU 100%,嚴重影響系統的安全性和可靠性。
SUN在JKD1.6 update18版本聲稱解決了該BUG,但是根據業界的測試和大家的反饋,直到JDK1.7的早期版本,該BUG依然存在,并沒有完全被修復。發生該BUG的主機資源占用圖如下:
圖2-26 epoll bug CPU空輪詢
SUN在解決該BUG的問題上不給力,只能從NIO框架層面進行問題規避,下面我們看下Netty是如何解決該問題的。
Netty的解決策略:
1) 根據該BUG的特征,首先偵測該BUG是否發生;
2) 將問題Selector上注冊的Channel轉移到新建的Selector上;
3) 老的問題Selector關閉,使用新建的Selector替換。
下面具體看下代碼,首先檢測是否發生了該BUG:
圖2-27 epoll bug 檢測
一旦檢測發生該BUG,則重建Selector,代碼如下:
圖2-28 重建Selector
重建完成之后,替換老的Selector,代碼如下:
圖2-29 替換Selector
大量生產系統的運行表明,Netty的規避策略可以解決epoll bug 導致的IO線程CPU死循環問題。
Java的優雅停機通常通過注冊JDK的ShutdownHook來實現,當系統接收到退出指令后,首先標記系統處于退出狀態,不再接收新的消息,然后將積壓的消息處理完,最后調用資源回收接口將資源銷毀,最后各線程退出執行。
通常優雅退出有個時間限制,例如30S,如果到達執行時間仍然沒有完成退出前的操作,則由監控腳本直接kill -9 pid,強制退出。
Netty的優雅退出功能隨著版本的優化和演進也在不斷的增強,下面我們一起看下Netty5的優雅退出。
首先看下Reactor線程和線程組,它們提供了優雅退出接口。EventExecutorGroup的接口定義如下:
圖2-30 EventExecutorGroup優雅退出
NioEventLoop的資源釋放接口實現:
圖2-31 NioEventLoop資源釋放
ChannelPipeline的關閉接口:
圖2-32 ChannelPipeline關閉接口
目前Netty向用戶提供的主要接口和類庫都提供了資源銷毀和優雅退出的接口,用戶的自定義實現類可以繼承這些接口,完成用戶資源的釋放和優雅退出。
為了提升內存的利用率,Netty提供了內存池和對象池。但是,基于緩存池實現以后需要對內存的申請和釋放進行嚴格的管理,否則很容易導致內存泄漏。
如果不采用內存池技術實現,每次對象都是以方法的局部變量形式被創建,使用完成之后,只要不再繼續引用它,JVM會自動釋放。但是,一旦引入內存池機制,對象的生命周期將由內存池負責管理,這通常是個全局引用,如果不顯式釋放JVM是不會回收這部分內存的。
對于Netty的用戶而言,使用者的技術水平差異很大,一些對JVM內存模型和內存泄漏機制不了解的用戶,可能只記得申請內存,忘記主動釋放內存,特別是JAVA程序員。
為了防止因為用戶遺漏導致內存泄漏,Netty在Pipe line的尾Handler中自動對內存進行釋放,相關代碼如下:
圖2-33 TailHandler的內存回收操作
對于內存池,實際就是將緩沖區重新放到內存池中循環使用,代碼如下:
圖2-34 PooledByteBuf的內存回收操作
做過協議棧的讀者都知道,當我們對消息進行解碼的時候,需要創建緩沖區。緩沖區的創建方式通常有兩種:
1) 容量預分配,在實際讀寫過程中如果不夠再擴展;
2) 根據協議消息長度創建緩沖區。
在實際的商用環境中,如果遇到畸形碼流攻擊、協議消息編碼異常、消息丟包等問題時,可能會解析到一個超長的長度字段。筆者曾經遇到過類似問題,報文長度字段值竟然是2G多,由于代碼的一個分支沒有對長度上限做有效保護,結果導致內存溢出。系統重啟后幾秒內再次內存溢出,幸好及時定位出問題根因,險些釀成嚴重的事故。
Netty提供了編解碼框架,因此對于解碼緩沖區的上限保護就顯得非常重要。下面,我們看下Netty是如何對緩沖區進行上限保護的:
首先,在內存分配的時候指定緩沖區長度上限:
圖2-35 緩沖區分配器可以指定緩沖區最大長度
其次,在對緩沖區進行寫入操作的時候,如果緩沖區容量不足需要擴展,首先對最大容量進行判斷,如果擴展后的容量超過上限,則拒絕擴展:
圖2-35 緩沖區擴展上限保護
最后,在解碼的時候,對消息長度進行判斷,如果超過最大容量上限,則拋出解碼異常,拒絕分配內存:
圖2-36 超出容量上限的半包解碼,失敗
圖2-37 拋出TooLongFrameException異常
大多數的商用系統都有多個網元或者部件組成,例如參與短信互動,會涉及到手機、基站、短信中心、短信網關、SP/CP等網元。不同網元或者部件的處理性能不同。為了防止因為浪涌業務或者下游網元性能低導致下游網元被壓垮,有時候需要系統提供流量整形功能。
下面我們一起看下流量整形(traffic shaping)的定義:流量整形(Traffic Shaping)是一種主動調整流量輸出速率的措施。一個典型應用是基于下游網絡結點的TP指標來控制本地流量的輸出。流量整形與流量監管的主要區別在于,流量整形對流量監管中需要丟棄的報文進行緩存——通常是將它們放入緩沖區或隊列內,也稱流量整形(Traffic Shaping,簡稱TS)。當令牌桶有足夠的令牌時,再均勻的向外發送這些被緩存的報文。流量整形與流量監管的另一區別是,整形可能會增加延遲,而監管幾乎不引入額外的延遲。
流量整形的原理示意圖如下:
圖2-38 流量整形原理圖
作為高性能的NIO框架,Netty的流量整形有兩個作用:
1) 防止由于上下游網元性能不均衡導致下游網元被壓垮,業務流程中斷;
2) 防止由于通信模塊接收消息過快,后端業務線程處理不及時導致的“撐死”問題。
下面我們就具體學習下Netty的流量整形功能。
全局流量整形的作用范圍是進程級的,無論你創建了多少個Channel,它的作用域針對所有的Channel。
用戶可以通過參數設置:報文的接收速率、報文的發送速率、整形周期。相關的接口如下所示:
圖2-39 全局流量整形參數設置
Netty流量整形的原理是:對每次讀取到的ByteBuf可寫字節數進行計算,獲取當前的報文流量,然后與流量整形閾值對比。如果已經達到或者超過了閾值。則計算等待時間delay,將當前的ByteBuf放到定時任務Task中緩存,由定時任務線程池在延遲delay之后繼續處理該ByteBuf。相關代碼如下:
圖2-40 動態計算當前流量
如果達到整形閾值,則對新接收的ByteBuf進行緩存,放入線程池的消息隊列中,稍后處理,代碼如下:
圖2-41 緩存當前的ByteBuf
定時任務的延時時間根據檢測周期T和流量整形閾值計算得來,代碼如下:
圖2-42 計算緩存等待周期
需要指出的是,流量整形的閾值limit越大,流量整形的精度越高,流量整形功能是可靠性的一種保障,它無法做到100%的精確。這個跟后端的編解碼以及緩沖區的處理策略相關,此處不再贅述。感興趣的朋友可以思考下,Netty為什么不做到 100%的精確。
流量整形與流控的最大區別在于流控會拒絕消息,流量整形不拒絕和丟棄消息,無論接收量多大,它總能以近似恒定的速度下發消息,跟變壓器的原理和功能類似。
除了全局流量整形,Netty也支持但鏈路的流量整形,相關的接口定義如下:
圖2-43 單鏈路流量整形
單鏈路流量整形與全局流量整形的最大區別就是它以單個鏈路為作用域,可以對不同的鏈路設置不同的整形策略。
它的實現原理與全局流量整形類似,我們不再贅述。值得說明的是,Netty支持用戶自定義流量整形策略,通過繼承AbstractTrafficShapingHandler的doAccounting方法可以定制整形策略。相關接口定義如下:
圖2-44 定制流量整形策略
盡管Netty在架構可靠性上面已經做了很多精細化的設計,以及基于防御式編程對系統進行了大量可靠性保護。但是,系統的可靠性是個持續投入和改進的過程,不可能在一個版本中一蹴而就,可靠性工作任重而道遠。
從業務的角度看,不同的行業、應用場景對可靠性的要求也是不同的,例如電信行業的可靠性要求是5個9,對于鐵路等特殊行業,可靠性要求更高,達到6個9。對于企業的一些邊緣IT系統,可靠性要求會低些。
可靠性是一種投資,對于企業而言,追求極端可靠性對研發成本是個沉重的包袱,但是相反,如果不重視系統的可靠性,一旦不幸遭遇網上事故,損失往往也是驚人的。
上述就是小編為大家分享的怎么進行Netty高可靠性原理分析了,如果剛好有類似的疑惑,不妨參照上述分析進行理解。如果想知道更多相關知識,歡迎關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。