1. 背景1.1. 宕機的代價1.1.1. 電信行業(yè)畢馬威國際(KPMG International)在對46個國家的74家運營商進行調(diào)查后發(fā)現(xiàn),全球通信行業(yè)每年的收益流失約為400億美元,占總收入的1%-3%。導(dǎo)致收益流失的因素有多種,主要原因就是計費BUG。 1.1.2. 互聯(lián)網(wǎng)行業(yè)美國太平洋時間8月16日下午3點50分到3點55分(北京時間8月17日6點50分到6點55分),谷歌遭遇了宕機。根據(jù)事后統(tǒng)計,短短的5分鐘,谷歌損失了54.5萬美元。也就是服務(wù)每中斷一分鐘,損失就達10.8萬美元。 2013年,從美國東部時間8月19日下午2點45分開始,有用戶率先發(fā)現(xiàn)了亞馬遜網(wǎng)站出現(xiàn)宕機,大約在20多分鐘后又恢復(fù)正常。此次宕機讓亞馬遜每分鐘損失近6.7萬美元,在宕機期間,消費者無法通過Amazon.com、亞馬遜移動端以及Amazon.ca等網(wǎng)站進行購物。 1.2. 軟件可靠性軟件可靠性是指在給定時間內(nèi),特定環(huán)境下軟件無錯運行的概率。軟件可靠性包含了以下三個要素: 1) 規(guī)定的時間:軟件可靠性只是體現(xiàn)在其運行階段,所以將運行時間作為規(guī)定的時間的度量。運行時間包括軟件系統(tǒng)運行后工作與掛起(開啟但空閑)的累計時間。由于軟件運行的環(huán)境與程序路徑選取的隨機性,軟件的失效為隨機事件,所以運行時間屬于隨機變量; 2) 規(guī)定的環(huán)境條件:環(huán)境條件指軟件的運行環(huán)境。它涉及軟件系統(tǒng)運行時所需的各種支持要素,如支持硬件、操作系統(tǒng)、其它支持軟件、輸入數(shù)據(jù)格式和范圍以及操作規(guī)程等。不同的環(huán)境條件下軟件的可靠性是不同的。具體地說,規(guī)定的環(huán)境條件主要是描述軟件系統(tǒng)運行時計算機的配置情況以及對輸入數(shù)據(jù)的要求,并假定其它一切因素都是理想的。有了明確規(guī)定的環(huán)境條件,還可以有效判斷軟件失效的責(zé)任在用戶方還是提供方; 3) 規(guī)定的功能:軟件可靠性還與規(guī)定的任務(wù)和功能有關(guān)。由于要完成的任務(wù)不同,軟件的運行剖面會有所區(qū)別,則調(diào)用的子模塊就不同(即程序路徑選擇不同),其可靠性也就可能不同。所以要準(zhǔn)確度量軟件系統(tǒng)的可靠性必須首先明確它的任務(wù)和功能。 1.3. Netty的可靠性首先,我們要從Netty的主要用途來分析它的可靠性,Netty目前的主流用法有三種: 1) 構(gòu)建RPC調(diào)用的基礎(chǔ)通信組件,提供跨節(jié)點的遠程服務(wù)調(diào)用能力; 2) NIO通信框架,用于跨節(jié)點的數(shù)據(jù)交換; 3) 其它應(yīng)用協(xié)議棧的基礎(chǔ)通信組件,例如HTTP協(xié)議以及其它基于Netty開發(fā)的應(yīng)用層協(xié)議棧。 以阿里的分布式服務(wù)框架Dubbo為例,Netty是Dubbo RPC框架的核心。它的服務(wù)調(diào)用示例圖如下: 圖1-1 Dubbo的節(jié)點角色說明圖 其中,服務(wù)提供者和服務(wù)調(diào)用者之間可以通過Dubbo協(xié)議進行RPC調(diào)用,消息的收發(fā)默認(rèn)通過Netty完成。 通過對Netty主流應(yīng)用場景的分析,我們發(fā)現(xiàn)Netty面臨的可靠性問題大致分為三類: 1) 傳統(tǒng)的網(wǎng)絡(luò)I/O故障,例如網(wǎng)絡(luò)閃斷、防火墻Hang住連接、網(wǎng)絡(luò)超時等; 2) NIO特有的故障,例如NIO類庫特有的BUG、讀寫半包處理異常、Reactor線程跑飛等等; 3) 編解碼相關(guān)的異常。 在大多數(shù)的業(yè)務(wù)應(yīng)用場景中,一旦因為某些故障導(dǎo)致Netty不能正常工作,業(yè)務(wù)往往會陷入癱瘓。所以,從業(yè)務(wù)訴求來看,對Netty框架的可靠性要求是非常的高。作為當(dāng)前業(yè)界最流行的一款NIO框架,Netty在不同行業(yè)和領(lǐng)域都得到了廣泛的應(yīng)用,它的高可靠性已經(jīng)得到了成百上千的生產(chǎn)系統(tǒng)檢驗。 Netty是如何支持系統(tǒng)高可靠性的?下面,我們就從幾個不同維度出發(fā)一探究竟。 2. Netty高可靠性之道2.1. 網(wǎng)絡(luò)通信類故障2.1.1. 客戶端連接超時在傳統(tǒng)的同步阻塞編程模式下,客戶端Socket發(fā)起網(wǎng)絡(luò)連接,往往需要指定連接超時時間,這樣做的目的主要有兩個: 1) 在同步阻塞I/O模型中,連接操作是同步阻塞的,如果不設(shè)置超時時間,客戶端I/O線程可能會被長時間阻塞,這會導(dǎo)致系統(tǒng)可用I/O線程數(shù)的減少; 2) 業(yè)務(wù)層需要:大多數(shù)系統(tǒng)都會對業(yè)務(wù)流程執(zhí)行時間有限制,例如WEB交互類的響應(yīng)時間要小于3S??蛻舳嗽O(shè)置連接超時時間是為了實現(xiàn)業(yè)務(wù)層的超時。 JDK原生的Socket連接接口定義如下: 圖2-1 JDK Socket連接超時接口 對于NIO的SocketChannel,在非阻塞模式下,它會直接返回連接結(jié)果,如果沒有連接成功,也沒有發(fā)生IO異常,則需要將SocketChannel注冊到Selector上監(jiān)聽連接結(jié)果。所以,異步連接的超時無法在API層面直接設(shè)置,而是需要通過定時器來主動監(jiān)測。 下面我們首先看下JDK NIO類庫的SocketChannel連接接口定義: 圖2-2 JDK NIO 類庫SocketChannel連接接口 從上面的接口定義可以看出,NIO類庫并沒有現(xiàn)成的連接超時接口供用戶直接使用,如果要在NIO編程中支持連接超時,往往需要NIO框架或者用戶自己封裝實現(xiàn)。 下面我們看下Netty是如何支持連接超時的,首先,在創(chuàng)建NIO客戶端的時候,可以配置連接超時參數(shù): 圖2-3 Netty客戶端創(chuàng)建支持設(shè)置連接超時參數(shù) 設(shè)置完連接超時之后,Netty在發(fā)起連接的時候,會根據(jù)超時時間創(chuàng)建ScheduledFuture掛載在Reactor線程上,用于定時監(jiān)測是否發(fā)生連接超時,相關(guān)代碼如下: 圖2-4 根據(jù)連接超時創(chuàng)建超時監(jiān)測定時任務(wù) 創(chuàng)建連接超時定時任務(wù)之后,會由NioEventLoop負(fù)責(zé)執(zhí)行。如果已經(jīng)連接超時,但是服務(wù)端仍然沒有返回TCP握手應(yīng)答,則關(guān)閉連接,代碼如上圖所示。 如果在超時期限內(nèi)處理完成連接操作,則取消連接超時定時任務(wù),相關(guān)代碼如下: 圖2-5 取消連接超時定時任務(wù) Netty的客戶端連接超時參數(shù)與其它常用的TCP參數(shù)一起配置,使用起來非常方便,上層用戶不用關(guān)心底層的超時實現(xiàn)機制。這既滿足了用戶的個性化需求,又實現(xiàn)了故障的分層隔離。 2.1.2. 通信對端強制關(guān)閉連接在客戶端和服務(wù)端正常通信過程中,如果發(fā)生網(wǎng)絡(luò)閃斷、對方進程突然宕機或者其它非正常關(guān)閉鏈路事件時,TCP鏈路就會發(fā)生異常。由于TCP是全雙工的,通信雙方都需要關(guān)閉和釋放Socket句柄才不會發(fā)生句柄的泄漏。 在實際的NIO編程過程中,我們經(jīng)常會發(fā)現(xiàn)由于句柄沒有被及時關(guān)閉導(dǎo)致的功能和可靠性問題。究其原因總結(jié)如下: 1) IO的讀寫等操作并非僅僅集中在Reactor線程內(nèi)部,用戶上層的一些定制行為可能會導(dǎo)致IO操作的外逸,例如業(yè)務(wù)自定義心跳機制。這些定制行為加大了統(tǒng)一異常處理的難度,IO操作越發(fā)散,故障發(fā)生的概率就越大; 2) 一些異常分支沒有考慮到,由于外部環(huán)境誘因?qū)е鲁绦蜻M入這些分支,就會引起故障。 下面我們通過故障模擬,看Netty是如何處理對端鏈路強制關(guān)閉異常的。首先啟動Netty服務(wù)端和客戶端,TCP鏈路建立成功之后,雙方維持該鏈路,查看鏈路狀態(tài),結(jié)果如下: 圖2-6 Netty服務(wù)端和客戶端TCP鏈路狀態(tài)正常 強制關(guān)閉客戶端,模擬客戶端宕機,服務(wù)端控制臺打印如下異常: 圖2-7 模擬TCP鏈路故障 從堆棧信息可以判斷,服務(wù)端已經(jīng)監(jiān)控到客戶端強制關(guān)閉了連接,下面我們看下服務(wù)端是否已經(jīng)釋放了連接句柄,再次執(zhí)行netstat命令,執(zhí)行結(jié)果如下: 圖2-8 查看故障鏈路狀態(tài) 從執(zhí)行結(jié)果可以看出,服務(wù)端已經(jīng)關(guān)閉了和客戶端的TCP連接,句柄資源正常釋放。由此可以得出結(jié)論,Netty底層已經(jīng)自動對該故障進行了處理。 下面我們一起看下Netty是如何感知到鏈路關(guān)閉異常并進行正確處理的,查看AbstractByteBuf的writeBytes方法,它負(fù)責(zé)將指定Channel的緩沖區(qū)數(shù)據(jù)寫入到ByteBuf中,詳細代碼如下: 圖2-9 AbstractByteBuf的writeBytes方法 在調(diào)用SocketChannel的read方法時發(fā)生了IOException,代碼如下: 圖2-10 讀取緩沖區(qū)數(shù)據(jù)發(fā)生IO異常 為了保證IO異常被統(tǒng)一處理,該異常向上拋,由AbstractNioByteChannel進行統(tǒng)一異常處理,代碼如下: 圖2-11 鏈路異常退出異常處理 為了能夠?qū)Ξ惓2呗赃M行統(tǒng)一,也為了方便維護,防止處理不當(dāng)導(dǎo)致的句柄泄漏等問題,句柄的關(guān)閉,統(tǒng)一調(diào)用AbstractChannel的close方法,代碼如下: 圖2-12 統(tǒng)一的Socket句柄關(guān)閉接口 2.1.3. 正常的連接關(guān)閉對于短連接協(xié)議,例如HTTP協(xié)議,通信雙方數(shù)據(jù)交互完成之后,通常按照雙方的約定由服務(wù)端關(guān)閉連接,客戶端獲得TCP連接關(guān)閉請求之后,關(guān)閉自身的Socket連接,雙方正式斷開連接。 在實際的NIO編程過程中,經(jīng)常存在一種誤區(qū):認(rèn)為只要是對方關(guān)閉連接,就會發(fā)生IO異常,捕獲IO異常之后再關(guān)閉連接即可。實際上,連接的合法關(guān)閉不會發(fā)生IO異常,它是一種正常場景,如果遺漏了該場景的判斷和處理就會導(dǎo)致連接句柄泄漏。 下面我們一起模擬故障,看Netty是如何處理的。測試場景設(shè)計如下:改造下Netty客戶端,雙發(fā)鏈路建立成功之后,等待120S,客戶端正常關(guān)閉鏈路??捶?wù)端是否能夠感知并釋放句柄資源。 首先啟動Netty客戶端和服務(wù)端,雙方TCP鏈路連接正常: 圖2-13 TCP連接狀態(tài)正常 120S之后,客戶端關(guān)閉連接,進程退出,為了能夠看到整個處理過程,我們在服務(wù)端的Reactor線程處設(shè)置斷點,先不做處理,此時鏈路狀態(tài)如下: 圖2-14 TCP連接句柄等待釋放 從上圖可以看出,此時服務(wù)端并沒有關(guān)閉Socket連接,鏈路處于CLOSE_WAIT狀態(tài),放開代碼讓服務(wù)端執(zhí)行完,結(jié)果如下: 圖2-15 TCP連接句柄正常釋放 下面我們一起看下服務(wù)端是如何判斷出客戶端關(guān)閉連接的,當(dāng)連接被對方合法關(guān)閉后,被關(guān)閉的SocketChannel會處于就緒狀態(tài),SocketChannel的read操作返回值為-1,說明連接已經(jīng)被關(guān)閉,代碼如下: 圖2-16 需要對讀取的字節(jié)數(shù)進行判斷 如果SocketChannel被設(shè)置為非阻塞,則它的read操作可能返回三個值: 1) 大于0,表示讀取到了字節(jié)數(shù); 2) 等于0,沒有讀取到消息,可能TCP處于Keep-Alive狀態(tài),接收到的是TCP握手消息; 3) -1,連接已經(jīng)被對方合法關(guān)閉。 通過調(diào)試,我們發(fā)現(xiàn),NIO類庫的返回值確實為-1: 圖2-17 鏈路正常關(guān)閉,返回值為-1 得知連接關(guān)閉之后,Netty將關(guān)閉操作位設(shè)置為true,關(guān)閉句柄,代碼如下: 圖2-18 連接正常關(guān)閉,釋放資源 2.1.4. 故障定制在大多數(shù)場景下,當(dāng)?shù)讓泳W(wǎng)絡(luò)發(fā)生故障的時候,應(yīng)該由底層的NIO框架負(fù)責(zé)釋放資源,處理異常等。上層的業(yè)務(wù)應(yīng)用不需要關(guān)心底層的處理細節(jié)。但是,在一些特殊的場景下,用戶可能需要感知這些異常,并針對這些異常進行定制處理,例如: 1) 客戶端的斷連重連機制; 2) 消息的緩存重發(fā); 3) 接口日志中詳細記錄故障細節(jié); 4) 運維相關(guān)功能,例如告警、觸發(fā)郵件/短信等 Netty的處理策略是發(fā)生IO異常,底層的資源由它負(fù)責(zé)釋放,同時將異常堆棧信息以事件的形式通知給上層用戶,由用戶對異常進行定制。這種處理機制既保證了異常處理的安全性,也向上層提供了靈活的定制能力。 具體接口定義以及默認(rèn)實現(xiàn)如下: 圖2-19 故障定制接口 用戶可以覆蓋該接口,進行個性化的異常定制。例如發(fā)起重連等。 2.2. 鏈路的有效性檢測當(dāng)網(wǎng)絡(luò)發(fā)生單通、連接被防火墻Hang住、長時間GC或者通信線程發(fā)生非預(yù)期異常時,會導(dǎo)致鏈路不可用且不易被及時發(fā)現(xiàn)。特別是異常發(fā)生在凌晨業(yè)務(wù)低谷期間,當(dāng)早晨業(yè)務(wù)高峰期到來時,由于鏈路不可用會導(dǎo)致瞬間的大批量業(yè)務(wù)失敗或者超時,這將對系統(tǒng)的可靠性產(chǎn)生重大的威脅。 從技術(shù)層面看,要解決鏈路的可靠性問題,必須周期性的對鏈路進行有效性檢測。目前最流行和通用的做法就是心跳檢測。 心跳檢測機制分為三個層面: 1) TCP層面的心跳檢測,即TCP的Keep-Alive機制,它的作用域是整個TCP協(xié)議棧; 2) 協(xié)議層的心跳檢測,主要存在于長連接協(xié)議中。例如SMPP協(xié)議; 3) 應(yīng)用層的心跳檢測,它主要由各業(yè)務(wù)產(chǎn)品通過約定方式定時給對方發(fā)送心跳消息實現(xiàn)。 心跳檢測的目的就是確認(rèn)當(dāng)前鏈路可用,對方活著并且能夠正常接收和發(fā)送消息。 做為高可靠的NIO框架,Netty也提供了心跳檢測機制,下面我們一起熟悉下心跳的檢測原理。 圖2-20 心跳檢測機制 不同的協(xié)議,心跳檢測機制也存在差異,歸納起來主要分為兩類: 1) Ping-Pong型心跳:由通信一方定時發(fā)送Ping消息,對方接收到Ping消息之后,立即返回Pong應(yīng)答消息給對方,屬于請求-響應(yīng)型心跳; 2) Ping-Ping型心跳:不區(qū)分心跳請求和應(yīng)答,由通信雙方按照約定定時向?qū)Ψ桨l(fā)送心跳Ping消息,它屬于雙向心跳。 心跳檢測策略如下: 1) 連續(xù)N次心跳檢測都沒有收到對方的Pong應(yīng)答消息或者Ping請求消息,則認(rèn)為鏈路已經(jīng)發(fā)生邏輯失效,這被稱作心跳超時; 2) 讀取和發(fā)送心跳消息的時候如何直接發(fā)生了IO異常,說明鏈路已經(jīng)失效,這被稱為心跳失敗。 無論發(fā)生心跳超時還是心跳失敗,都需要關(guān)閉鏈路,由客戶端發(fā)起重連操作,保證鏈路能夠恢復(fù)正常。 Netty的心跳檢測實際上是利用了鏈路空閑檢測機制實現(xiàn)的,相關(guān)代碼如下: 圖2-21 心跳檢測的代碼包路徑 Netty提供的空閑檢測機制分為三種: 1) 讀空閑,鏈路持續(xù)時間t沒有讀取到任何消息; 2) 寫空閑,鏈路持續(xù)時間t沒有發(fā)送任何消息; 3) 讀寫空閑,鏈路持續(xù)時間t沒有接收或者發(fā)送任何消息。 Netty的默認(rèn)讀寫空閑機制是發(fā)生超時異常,關(guān)閉連接,但是,我們可以定制它的超時實現(xiàn)機制,以便支持不同的用戶場景。 WriteTimeoutHandler的超時接口如下: 圖2-22 寫超時 ReadTimeoutHandler的超時接口如下: 圖2-23 讀超時 讀寫空閑的接口如下: 圖2-24 讀寫空閑 利用Netty提供的鏈路空閑檢測機制,可以非常靈活的實現(xiàn)協(xié)議層的心跳檢測。在《Netty權(quán)威指南》中的私有協(xié)議棧設(shè)計和開發(fā)章節(jié),我利用Netty提供的自定義Task接口實現(xiàn)了另一種心跳檢測機制,感興趣的朋友可以參閱該書。 2.3. Reactor線程的保護Reactor線程是IO操作的核心,NIO框架的發(fā)動機,一旦出現(xiàn)故障,將會導(dǎo)致掛載在其上面的多路用復(fù)用器和多個鏈路無法正常工作。因此它的可靠性要求非常高。 筆者就曾經(jīng)遇到過因為異常處理不當(dāng)導(dǎo)致Reactor線程跑飛,大量業(yè)務(wù)請求處理失敗的故障。下面我們一起看下Netty是如何有效提升Reactor線程的可靠性的。 2.3.1. 異常處理要當(dāng)心盡管Reactor線程主要處理IO操作,發(fā)生的異常通常是IO異常,但是,實際上在一些特殊場景下會發(fā)生非IO異常,如果僅僅捕獲IO異??赡芫蜁?dǎo)致Reactor線程跑飛。為了防止發(fā)生這種意外,在循環(huán)體內(nèi)一定要捕獲Throwable,而不是IO異?;蛘逧xception。 Netty的相關(guān)代碼如下: 圖2-25 Reactor線程異常保護 捕獲Throwable之后,即便發(fā)生了意外未知對異常,線程也不會跑飛,它休眠1S,防止死循環(huán)導(dǎo)致的異常繞接,然后繼續(xù)恢復(fù)執(zhí)行。這樣處理的核心理念就是: 1) 某個消息的異常不應(yīng)該導(dǎo)致整條鏈路不可用; 2) 某條鏈路不可用不應(yīng)該導(dǎo)致其它鏈路不可用; 3) 某個進程不可用不應(yīng)該導(dǎo)致其它集群節(jié)點不可用。 2.3.2. 死循環(huán)保護通常情況下,死循環(huán)是可檢測、可預(yù)防但是無法完全避免的。Reactor線程通常處理的都是IO相關(guān)的操作,因此我們重點關(guān)注IO層面的死循環(huán)。 JDK NIO類庫最著名的就是 epoll bug了,它會導(dǎo)致Selector空輪詢,IO線程CPU 100%,嚴(yán)重影響系統(tǒng)的安全性和可靠性。 SUN在JKD1.6 update18版本聲稱解決了該BUG,但是根據(jù)業(yè)界的測試和大家的反饋,直到JDK1.7的早期版本,該BUG依然存在,并沒有完全被修復(fù)。發(fā)生該BUG的主機資源占用圖如下: 圖2-26 epoll bug CPU空輪詢 SUN在解決該BUG的問題上不給力,只能從NIO框架層面進行問題規(guī)避,下面我們看下Netty是如何解決該問題的。 Netty的解決策略: 1) 根據(jù)該BUG的特征,首先偵測該BUG是否發(fā)生; 2) 將問題Selector上注冊的Channel轉(zhuǎn)移到新建的Selector上; 3) 老的問題Selector關(guān)閉,使用新建的Selector替換。 下面具體看下代碼,首先檢測是否發(fā)生了該BUG: 圖2-27 epoll bug 檢測 一旦檢測發(fā)生該BUG,則重建Selector,代碼如下: 圖2-28 重建Selector 重建完成之后,替換老的Selector,代碼如下: 圖2-29 替換Selector 大量生產(chǎn)系統(tǒng)的運行表明,Netty的規(guī)避策略可以解決epoll bug 導(dǎo)致的IO線程CPU死循環(huán)問題。 2.4. 優(yōu)雅退出Java的優(yōu)雅停機通常通過注冊JDK的ShutdownHook來實現(xiàn),當(dāng)系統(tǒng)接收到退出指令后,首先標(biāo)記系統(tǒng)處于退出狀態(tài),不再接收新的消息,然后將積壓的消息處理完,最后調(diào)用資源回收接口將資源銷毀,最后各線程退出執(zhí)行。 通常優(yōu)雅退出有個時間限制,例如30S,如果到達執(zhí)行時間仍然沒有完成退出前的操作,則由監(jiān)控腳本直接kill -9 pid,強制退出。 Netty的優(yōu)雅退出功能隨著版本的優(yōu)化和演進也在不斷的增強,下面我們一起看下Netty5的優(yōu)雅退出。 首先看下Reactor線程和線程組,它們提供了優(yōu)雅退出接口。EventExecutorGroup的接口定義如下: 圖2-30 EventExecutorGroup優(yōu)雅退出 NioEventLoop的資源釋放接口實現(xiàn): 圖2-31 NioEventLoop資源釋放 ChannelPipeline的關(guān)閉接口: 圖2-32 ChannelPipeline關(guān)閉接口 目前Netty向用戶提供的主要接口和類庫都提供了資源銷毀和優(yōu)雅退出的接口,用戶的自定義實現(xiàn)類可以繼承這些接口,完成用戶資源的釋放和優(yōu)雅退出。 2.5. 內(nèi)存保護2.5.1. 緩沖區(qū)的內(nèi)存泄漏保護為了提升內(nèi)存的利用率,Netty提供了內(nèi)存池和對象池。但是,基于緩存池實現(xiàn)以后需要對內(nèi)存的申請和釋放進行嚴(yán)格的管理,否則很容易導(dǎo)致內(nèi)存泄漏。 如果不采用內(nèi)存池技術(shù)實現(xiàn),每次對象都是以方法的局部變量形式被創(chuàng)建,使用完成之后,只要不再繼續(xù)引用它,JVM會自動釋放。但是,一旦引入內(nèi)存池機制,對象的生命周期將由內(nèi)存池負(fù)責(zé)管理,這通常是個全局引用,如果不顯式釋放JVM是不會回收這部分內(nèi)存的。 對于Netty的用戶而言,使用者的技術(shù)水平差異很大,一些對JVM內(nèi)存模型和內(nèi)存泄漏機制不了解的用戶,可能只記得申請內(nèi)存,忘記主動釋放內(nèi)存,特別是JAVA程序員。 為了防止因為用戶遺漏導(dǎo)致內(nèi)存泄漏,Netty在Pipe line的尾Handler中自動對內(nèi)存進行釋放,相關(guān)代碼如下: 圖2-33 TailHandler的內(nèi)存回收操作 對于內(nèi)存池,實際就是將緩沖區(qū)重新放到內(nèi)存池中循環(huán)使用,代碼如下: 圖2-34 PooledByteBuf的內(nèi)存回收操作 2.5.2. 緩沖區(qū)內(nèi)存溢出保護做過協(xié)議棧的讀者都知道,當(dāng)我們對消息進行解碼的時候,需要創(chuàng)建緩沖區(qū)。緩沖區(qū)的創(chuàng)建方式通常有兩種: 1) 容量預(yù)分配,在實際讀寫過程中如果不夠再擴展; 2) 根據(jù)協(xié)議消息長度創(chuàng)建緩沖區(qū)。 在實際的商用環(huán)境中,如果遇到畸形碼流攻擊、協(xié)議消息編碼異常、消息丟包等問題時,可能會解析到一個超長的長度字段。筆者曾經(jīng)遇到過類似問題,報文長度字段值竟然是2G多,由于代碼的一個分支沒有對長度上限做有效保護,結(jié)果導(dǎo)致內(nèi)存溢出。系統(tǒng)重啟后幾秒內(nèi)再次內(nèi)存溢出,幸好及時定位出問題根因,險些釀成嚴(yán)重的事故。 Netty提供了編解碼框架,因此對于解碼緩沖區(qū)的上限保護就顯得非常重要。下面,我們看下Netty是如何對緩沖區(qū)進行上限保護的: 首先,在內(nèi)存分配的時候指定緩沖區(qū)長度上限: 圖2-35 緩沖區(qū)分配器可以指定緩沖區(qū)最大長度 其次,在對緩沖區(qū)進行寫入操作的時候,如果緩沖區(qū)容量不足需要擴展,首先對最大容量進行判斷,如果擴展后的容量超過上限,則拒絕擴展: 圖2-35 緩沖區(qū)擴展上限保護 最后,在解碼的時候,對消息長度進行判斷,如果超過最大容量上限,則拋出解碼異常,拒絕分配內(nèi)存: 圖2-36 超出容量上限的半包解碼,失敗 圖2-37 拋出TooLongFrameException異常 2.6. 流量整形大多數(shù)的商用系統(tǒng)都有多個網(wǎng)元或者部件組成,例如參與短信互動,會涉及到手機、基站、短信中心、短信網(wǎng)關(guān)、SP/CP等網(wǎng)元。不同網(wǎng)元或者部件的處理性能不同。為了防止因為浪涌業(yè)務(wù)或者下游網(wǎng)元性能低導(dǎo)致下游網(wǎng)元被壓垮,有時候需要系統(tǒng)提供流量整形功能。 下面我們一起看下流量整形(traffic shaping)的定義:流量整形(Traffic Shaping)是一種主動調(diào)整流量輸出速率的措施。一個典型應(yīng)用是基于下游網(wǎng)絡(luò)結(jié)點的TP指標(biāo)來控制本地流量的輸出。流量整形與流量監(jiān)管的主要區(qū)別在于,流量整形對流量監(jiān)管中需要丟棄的報文進行緩存——通常是將它們放入緩沖區(qū)或隊列內(nèi),也稱流量整形(Traffic Shaping,簡稱TS)。當(dāng)令牌桶有足夠的令牌時,再均勻的向外發(fā)送這些被緩存的報文。流量整形與流量監(jiān)管的另一區(qū)別是,整形可能會增加延遲,而監(jiān)管幾乎不引入額外的延遲。 流量整形的原理示意圖如下: 圖2-38 流量整形原理圖 作為高性能的NIO框架,Netty的流量整形有兩個作用: 1) 防止由于上下游網(wǎng)元性能不均衡導(dǎo)致下游網(wǎng)元被壓垮,業(yè)務(wù)流程中斷; 2) 防止由于通信模塊接收消息過快,后端業(yè)務(wù)線程處理不及時導(dǎo)致的“撐死”問題。 下面我們就具體學(xué)習(xí)下Netty的流量整形功能。 2.6.1. 全局流量整形全局流量整形的作用范圍是進程級的,無論你創(chuàng)建了多少個Channel,它的作用域針對所有的Channel。 用戶可以通過參數(shù)設(shè)置:報文的接收速率、報文的發(fā)送速率、整形周期。相關(guān)的接口如下所示: 圖2-39 全局流量整形參數(shù)設(shè)置 Netty流量整形的原理是:對每次讀取到的ByteBuf可寫字節(jié)數(shù)進行計算,獲取當(dāng)前的報文流量,然后與流量整形閾值對比。如果已經(jīng)達到或者超過了閾值。則計算等待時間delay,將當(dāng)前的ByteBuf放到定時任務(wù)Task中緩存,由定時任務(wù)線程池在延遲delay之后繼續(xù)處理該ByteBuf。相關(guān)代碼如下: 圖2-40 動態(tài)計算當(dāng)前流量 如果達到整形閾值,則對新接收的ByteBuf進行緩存,放入線程池的消息隊列中,稍后處理,代碼如下: 圖2-41 緩存當(dāng)前的ByteBuf 定時任務(wù)的延時時間根據(jù)檢測周期T和流量整形閾值計算得來,代碼如下: 圖2-42 計算緩存等待周期 需要指出的是,流量整形的閾值limit越大,流量整形的精度越高,流量整形功能是可靠性的一種保障,它無法做到100%的精確。這個跟后端的編解碼以及緩沖區(qū)的處理策略相關(guān),此處不再贅述。感興趣的朋友可以思考下,Netty為什么不做到 100%的精確。 流量整形與流控的最大區(qū)別在于流控會拒絕消息,流量整形不拒絕和丟棄消息,無論接收量多大,它總能以近似恒定的速度下發(fā)消息,跟變壓器的原理和功能類似。 2.6.2. 單條鏈路流量整形除了全局流量整形,Netty也支持但鏈路的流量整形,相關(guān)的接口定義如下: 圖2-43 單鏈路流量整形 單鏈路流量整形與全局流量整形的最大區(qū)別就是它以單個鏈路為作用域,可以對不同的鏈路設(shè)置不同的整形策略。 它的實現(xiàn)原理與全局流量整形類似,我們不再贅述。值得說明的是,Netty支持用戶自定義流量整形策略,通過繼承AbstractTrafficShapingHandler的doAccounting方法可以定制整形策略。相關(guān)接口定義如下: 圖2-44 定制流量整形策略 3. 總結(jié)盡管Netty在架構(gòu)可靠性上面已經(jīng)做了很多精細化的設(shè)計,以及基于防御式編程對系統(tǒng)進行了大量可靠性保護。但是,系統(tǒng)的可靠性是個持續(xù)投入和改進的過程,不可能在一個版本中一蹴而就,可靠性工作任重而道遠。 從業(yè)務(wù)的角度看,不同的行業(yè)、應(yīng)用場景對可靠性的要求也是不同的,例如電信行業(yè)的可靠性要求是5個9,對于鐵路等特殊行業(yè),可靠性要求更高,達到6個9。對于企業(yè)的一些邊緣IT系統(tǒng),可靠性要求會低些。 可靠性是一種投資,對于企業(yè)而言,追求極端可靠性對研發(fā)成本是個沉重的包袱,但是相反,如果不重視系統(tǒng)的可靠性,一旦不幸遭遇網(wǎng)上事故,損失往往也是驚人的。 對于架構(gòu)師和設(shè)計師,如何權(quán)衡架構(gòu)的可靠性和其它特性的關(guān)系,是一個很大的挑戰(zhàn)。通過研究和學(xué)習(xí)Netty的可靠性設(shè)計,也許能夠給大家?guī)硪恍﹩⑹尽?/p> 4. Netty學(xué)習(xí)推薦書籍目前市面上介紹netty的文章很多,如果讀者希望系統(tǒng)性的學(xué)習(xí)Netty,推薦兩本書: 1) 《Netty in Action》 2) 《Netty權(quán)威指南》 5.作者簡介李林鋒,2007年畢業(yè)于東北大學(xué),2008年進入華為公司從事高性能通信軟件的設(shè)計和開發(fā)工作,有6年NIO設(shè)計和開發(fā)經(jīng)驗,精通Netty、Mina等NIO框架。Netty中國社區(qū)創(chuàng)始人,《Netty權(quán)威指南》作者。 |
|