乡下人产国偷v产偷v自拍,国产午夜片在线观看,婷婷成人亚洲综合国产麻豆,久久综合给合久久狠狠狠9

  • <output id="e9wm2"></output>
    <s id="e9wm2"><nobr id="e9wm2"><ins id="e9wm2"></ins></nobr></s>

    • 分享

      Netty系列之Netty可靠性分析

       集微筆記 2014-06-24

      1. 背景

      1.1. 宕機(jī)的代價(jià)

      1.1.1. 電信行業(yè)

      畢馬威國(guó)際(KPMG International)在對(duì)46個(gè)國(guó)家的74家運(yùn)營(yíng)商進(jìn)行調(diào)查后發(fā)現(xiàn),全球通信行業(yè)每年的收益流失約為400億美元,占總收入的1%-3%。導(dǎo)致收益流失的因素有多種,主要原因就是計(jì)費(fèi)BUG。

      1.1.2. 互聯(lián)網(wǎng)行業(yè)

      美國(guó)太平洋時(shí)間8月16日下午3點(diǎn)50分到3點(diǎn)55分(北京時(shí)間8月17日6點(diǎn)50分到6點(diǎn)55分),谷歌遭遇了宕機(jī)。根據(jù)事后統(tǒng)計(jì),短短的5分鐘,谷歌損失了54.5萬美元。也就是服務(wù)每中斷一分鐘,損失就達(dá)10.8萬美元。

      2013年,從美國(guó)東部時(shí)間8月19日下午2點(diǎn)45分開始,有用戶率先發(fā)現(xiàn)了亞馬遜網(wǎng)站出現(xiàn)宕機(jī),大約在20多分鐘后又恢復(fù)正常。此次宕機(jī)讓亞馬遜每分鐘損失近6.7萬美元,在宕機(jī)期間,消費(fèi)者無法通過Amazon.com、亞馬遜移動(dòng)端以及Amazon.ca等網(wǎng)站進(jìn)行購(gòu)物。

      1.2. 軟件可靠性

      軟件可靠性是指在給定時(shí)間內(nèi),特定環(huán)境下軟件無錯(cuò)運(yùn)行的概率。軟件可靠性包含了以下三個(gè)要素:

      1) 規(guī)定的時(shí)間:軟件可靠性只是體現(xiàn)在其運(yùn)行階段,所以將運(yùn)行時(shí)間作為規(guī)定的時(shí)間的度量。運(yùn)行時(shí)間包括軟件系統(tǒng)運(yùn)行后工作與掛起(開啟但空閑)的累計(jì)時(shí)間。由于軟件運(yùn)行的環(huán)境與程序路徑選取的隨機(jī)性,軟件的失效為隨機(jī)事件,所以運(yùn)行時(shí)間屬于隨機(jī)變量;

      2) 規(guī)定的環(huán)境條件:環(huán)境條件指軟件的運(yùn)行環(huán)境。它涉及軟件系統(tǒng)運(yùn)行時(shí)所需的各種支持要素,如支持硬件、操作系統(tǒng)、其它支持軟件、輸入數(shù)據(jù)格式和范圍以及操作規(guī)程等。不同的環(huán)境條件下軟件的可靠性是不同的。具體地說,規(guī)定的環(huán)境條件主要是描述軟件系統(tǒng)運(yùn)行時(shí)計(jì)算機(jī)的配置情況以及對(duì)輸入數(shù)據(jù)的要求,并假定其它一切因素都是理想的。有了明確規(guī)定的環(huán)境條件,還可以有效判斷軟件失效的責(zé)任在用戶方還是提供方;

      3) 規(guī)定的功能:軟件可靠性還與規(guī)定的任務(wù)和功能有關(guān)。由于要完成的任務(wù)不同,軟件的運(yùn)行剖面會(huì)有所區(qū)別,則調(diào)用的子模塊就不同(即程序路徑選擇不同),其可靠性也就可能不同。所以要準(zhǔn)確度量軟件系統(tǒng)的可靠性必須首先明確它的任務(wù)和功能。

      1.3. Netty的可靠性

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

      1) 構(gòu)建RPC調(diào)用的基礎(chǔ)通信組件,提供跨節(jié)點(diǎn)的遠(yuǎn)程服務(wù)調(diào)用能力;

      2) NIO通信框架,用于跨節(jié)點(diǎn)的數(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é)點(diǎn)角色說明圖

      其中,服務(wù)提供者和服務(wù)調(diào)用者之間可以通過Dubbo協(xié)議進(jìn)行RPC調(diào)用,消息的收發(fā)默認(rèn)通過Netty完成。

      通過對(duì)Netty主流應(yīng)用場(chǎng)景的分析,我們發(fā)現(xiàn)Netty面臨的可靠性問題大致分為三類:

      1) 傳統(tǒng)的網(wǎng)絡(luò)I/O故障,例如網(wǎng)絡(luò)閃斷、防火墻Hang住連接、網(wǎng)絡(luò)超時(shí)等;

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

      3) 編解碼相關(guān)的異常。

      在大多數(shù)的業(yè)務(wù)應(yīng)用場(chǎng)景中,一旦因?yàn)槟承┕收蠈?dǎo)致Netty不能正常工作,業(yè)務(wù)往往會(huì)陷入癱瘓。所以,從業(yè)務(wù)訴求來看,對(duì)Netty框架的可靠性要求是非常的高。作為當(dāng)前業(yè)界最流行的一款NIO框架,Netty在不同行業(yè)和領(lǐng)域都得到了廣泛的應(yīng)用,它的高可靠性已經(jīng)得到了成百上千的生產(chǎn)系統(tǒng)檢驗(yàn)。

      Netty是如何支持系統(tǒng)高可靠性的?下面,我們就從幾個(gè)不同維度出發(fā)一探究竟。

      2. Netty高可靠性之道

      2.1. 網(wǎng)絡(luò)通信類故障

      2.1.1. 客戶端連接超時(shí)

      在傳統(tǒng)的同步阻塞編程模式下,客戶端Socket發(fā)起網(wǎng)絡(luò)連接,往往需要指定連接超時(shí)時(shí)間,這樣做的目的主要有兩個(gè):

      1) 在同步阻塞I/O模型中,連接操作是同步阻塞的,如果不設(shè)置超時(shí)時(shí)間,客戶端I/O線程可能會(huì)被長(zhǎng)時(shí)間阻塞,這會(huì)導(dǎo)致系統(tǒng)可用I/O線程數(shù)的減少;

      2) 業(yè)務(wù)層需要:大多數(shù)系統(tǒng)都會(huì)對(duì)業(yè)務(wù)流程執(zhí)行時(shí)間有限制,例如WEB交互類的響應(yīng)時(shí)間要小于3S。客戶端設(shè)置連接超時(shí)時(shí)間是為了實(shí)現(xiàn)業(yè)務(wù)層的超時(shí)。

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

      圖2-1 JDK Socket連接超時(shí)接口

      對(duì)于NIO的SocketChannel,在非阻塞模式下,它會(huì)直接返回連接結(jié)果,如果沒有連接成功,也沒有發(fā)生IO異常,則需要將SocketChannel注冊(cè)到Selector上監(jiān)聽連接結(jié)果。所以,異步連接的超時(shí)無法在API層面直接設(shè)置,而是需要通過定時(shí)器來主動(dòng)監(jiān)測(cè)。

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

      圖2-2 JDK NIO 類庫(kù)SocketChannel連接接口

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

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

      圖2-3 Netty客戶端創(chuàng)建支持設(shè)置連接超時(shí)參數(shù)

      設(shè)置完連接超時(shí)之后,Netty在發(fā)起連接的時(shí)候,會(huì)根據(jù)超時(shí)時(shí)間創(chuàng)建ScheduledFuture掛載在Reactor線程上,用于定時(shí)監(jiān)測(cè)是否發(fā)生連接超時(shí),相關(guān)代碼如下:

      圖2-4 根據(jù)連接超時(shí)創(chuàng)建超時(shí)監(jiān)測(cè)定時(shí)任務(wù)

      創(chuàng)建連接超時(shí)定時(shí)任務(wù)之后,會(huì)由NioEventLoop負(fù)責(zé)執(zhí)行。如果已經(jīng)連接超時(shí),但是服務(wù)端仍然沒有返回TCP握手應(yīng)答,則關(guān)閉連接,代碼如上圖所示。

      如果在超時(shí)期限內(nèi)處理完成連接操作,則取消連接超時(shí)定時(shí)任務(wù),相關(guān)代碼如下:

      圖2-5 取消連接超時(shí)定時(shí)任務(wù)

      Netty的客戶端連接超時(shí)參數(shù)與其它常用的TCP參數(shù)一起配置,使用起來非常方便,上層用戶不用關(guān)心底層的超時(shí)實(shí)現(xiàn)機(jī)制。這既滿足了用戶的個(gè)性化需求,又實(shí)現(xiàn)了故障的分層隔離。

      2.1.2. 通信對(duì)端強(qiáng)制關(guān)閉連接

      在客戶端和服務(wù)端正常通信過程中,如果發(fā)生網(wǎng)絡(luò)閃斷、對(duì)方進(jìn)程突然宕機(jī)或者其它非正常關(guān)閉鏈路事件時(shí),TCP鏈路就會(huì)發(fā)生異常。由于TCP是全雙工的,通信雙方都需要關(guān)閉和釋放Socket句柄才不會(huì)發(fā)生句柄的泄漏。

      在實(shí)際的NIO編程過程中,我們經(jīng)常會(huì)發(fā)現(xiàn)由于句柄沒有被及時(shí)關(guān)閉導(dǎo)致的功能和可靠性問題。究其原因總結(jié)如下:

      1) IO的讀寫等操作并非僅僅集中在Reactor線程內(nèi)部,用戶上層的一些定制行為可能會(huì)導(dǎo)致IO操作的外逸,例如業(yè)務(wù)自定義心跳機(jī)制。這些定制行為加大了統(tǒng)一異常處理的難度,IO操作越發(fā)散,故障發(fā)生的概率就越大;

      2) 一些異常分支沒有考慮到,由于外部環(huán)境誘因?qū)е鲁绦蜻M(jìn)入這些分支,就會(huì)引起故障。

      下面我們通過故障模擬,看Netty是如何處理對(duì)端鏈路強(qiáng)制關(guān)閉異常的。首先啟動(dòng)Netty服務(wù)端和客戶端,TCP鏈路建立成功之后,雙方維持該鏈路,查看鏈路狀態(tài),結(jié)果如下:

      圖2-6 Netty服務(wù)端和客戶端TCP鏈路狀態(tài)正常

      強(qiáng)制關(guān)閉客戶端,模擬客戶端宕機(jī),服務(wù)端控制臺(tái)打印如下異常:

      圖2-7 模擬TCP鏈路故障

      從堆棧信息可以判斷,服務(wù)端已經(jīng)監(jiān)控到客戶端強(qiáng)制關(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)自動(dòng)對(duì)該故障進(jìn)行了處理。

      下面我們一起看下Netty是如何感知到鏈路關(guān)閉異常并進(jìn)行正確處理的,查看AbstractByteBuf的writeBytes方法,它負(fù)責(zé)將指定Channel的緩沖區(qū)數(shù)據(jù)寫入到ByteBuf中,詳細(xì)代碼如下:

      圖2-9 AbstractByteBuf的writeBytes方法

      在調(diào)用SocketChannel的read方法時(shí)發(fā)生了IOException,代碼如下:

      圖2-10 讀取緩沖區(qū)數(shù)據(jù)發(fā)生IO異常

      為了保證IO異常被統(tǒng)一處理,該異常向上拋,由AbstractNioByteChannel進(jìn)行統(tǒng)一異常處理,代碼如下:

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

      為了能夠?qū)Ξ惓2呗赃M(jìn)行統(tǒng)一,也為了方便維護(hù),防止處理不當(dāng)導(dǎo)致的句柄泄漏等問題,句柄的關(guān)閉,統(tǒng)一調(diào)用AbstractChannel的close方法,代碼如下:

      圖2-12 統(tǒng)一的Socket句柄關(guān)閉接口

      2.1.3. 正常的連接關(guān)閉

      對(duì)于短連接協(xié)議,例如HTTP協(xié)議,通信雙方數(shù)據(jù)交互完成之后,通常按照雙方的約定由服務(wù)端關(guān)閉連接,客戶端獲得TCP連接關(guān)閉請(qǐng)求之后,關(guān)閉自身的Socket連接,雙方正式斷開連接。

      在實(shí)際的NIO編程過程中,經(jīng)常存在一種誤區(qū):認(rèn)為只要是對(duì)方關(guān)閉連接,就會(huì)發(fā)生IO異常,捕獲IO異常之后再關(guān)閉連接即可。實(shí)際上,連接的合法關(guān)閉不會(huì)發(fā)生IO異常,它是一種正常場(chǎng)景,如果遺漏了該場(chǎng)景的判斷和處理就會(huì)導(dǎo)致連接句柄泄漏。

      下面我們一起模擬故障,看Netty是如何處理的。測(cè)試場(chǎng)景設(shè)計(jì)如下:改造下Netty客戶端,雙發(fā)鏈路建立成功之后,等待120S,客戶端正常關(guān)閉鏈路??捶?wù)端是否能夠感知并釋放句柄資源。

      首先啟動(dòng)Netty客戶端和服務(wù)端,雙方TCP鏈路連接正常:

      圖2-13 TCP連接狀態(tài)正常

      120S之后,客戶端關(guān)閉連接,進(jìn)程退出,為了能夠看到整個(gè)處理過程,我們?cè)诜?wù)端的Reactor線程處設(shè)置斷點(diǎn),先不做處理,此時(shí)鏈路狀態(tài)如下:

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

      從上圖可以看出,此時(shí)服務(wù)端并沒有關(guān)閉Socket連接,鏈路處于CLOSE_WAIT狀態(tài),放開代碼讓服務(wù)端執(zhí)行完,結(jié)果如下:

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

      下面我們一起看下服務(wù)端是如何判斷出客戶端關(guān)閉連接的,當(dāng)連接被對(duì)方合法關(guān)閉后,被關(guān)閉的SocketChannel會(huì)處于就緒狀態(tài),SocketChannel的read操作返回值為-1,說明連接已經(jīng)被關(guān)閉,代碼如下:

      圖2-16 需要對(duì)讀取的字節(jié)數(shù)進(jìn)行判斷

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

      1) 大于0,表示讀取到了字節(jié)數(shù);

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

      3) -1,連接已經(jīng)被對(duì)方合法關(guān)閉。

      通過調(diào)試,我們發(fā)現(xiàn),NIO類庫(kù)的返回值確實(shí)為-1:

      圖2-17 鏈路正常關(guān)閉,返回值為-1

      得知連接關(guān)閉之后,Netty將關(guān)閉操作位設(shè)置為true,關(guān)閉句柄,代碼如下:

      圖2-18 連接正常關(guān)閉,釋放資源

      2.1.4. 故障定制

      在大多數(shù)場(chǎng)景下,當(dāng)?shù)讓泳W(wǎng)絡(luò)發(fā)生故障的時(shí)候,應(yīng)該由底層的NIO框架負(fù)責(zé)釋放資源,處理異常等。上層的業(yè)務(wù)應(yīng)用不需要關(guān)心底層的處理細(xì)節(jié)。但是,在一些特殊的場(chǎng)景下,用戶可能需要感知這些異常,并針對(duì)這些異常進(jìn)行定制處理,例如:

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

      2) 消息的緩存重發(fā);

      3) 接口日志中詳細(xì)記錄故障細(xì)節(jié);

      4) 運(yùn)維相關(guān)功能,例如告警、觸發(fā)郵件/短信等

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

      具體接口定義以及默認(rèn)實(shí)現(xiàn)如下:

      圖2-19 故障定制接口

      用戶可以覆蓋該接口,進(jìn)行個(gè)性化的異常定制。例如發(fā)起重連等。

      2.2. 鏈路的有效性檢測(cè)

      當(dāng)網(wǎng)絡(luò)發(fā)生單通、連接被防火墻Hang住、長(zhǎng)時(shí)間GC或者通信線程發(fā)生非預(yù)期異常時(shí),會(huì)導(dǎo)致鏈路不可用且不易被及時(shí)發(fā)現(xiàn)。特別是異常發(fā)生在凌晨業(yè)務(wù)低谷期間,當(dāng)早晨業(yè)務(wù)高峰期到來時(shí),由于鏈路不可用會(huì)導(dǎo)致瞬間的大批量業(yè)務(wù)失敗或者超時(shí),這將對(duì)系統(tǒng)的可靠性產(chǎn)生重大的威脅。

      從技術(shù)層面看,要解決鏈路的可靠性問題,必須周期性的對(duì)鏈路進(jìn)行有效性檢測(cè)。目前最流行和通用的做法就是心跳檢測(cè)。

      心跳檢測(cè)機(jī)制分為三個(gè)層面:

      1) TCP層面的心跳檢測(cè),即TCP的Keep-Alive機(jī)制,它的作用域是整個(gè)TCP協(xié)議棧;

      2) 協(xié)議層的心跳檢測(cè),主要存在于長(zhǎng)連接協(xié)議中。例如SMPP協(xié)議;

      3) 應(yīng)用層的心跳檢測(cè),它主要由各業(yè)務(wù)產(chǎn)品通過約定方式定時(shí)給對(duì)方發(fā)送心跳消息實(shí)現(xiàn)。

      心跳檢測(cè)的目的就是確認(rèn)當(dāng)前鏈路可用,對(duì)方活著并且能夠正常接收和發(fā)送消息。

      做為高可靠的NIO框架,Netty也提供了心跳檢測(cè)機(jī)制,下面我們一起熟悉下心跳的檢測(cè)原理。

      圖2-20 心跳檢測(cè)機(jī)制

      不同的協(xié)議,心跳檢測(cè)機(jī)制也存在差異,歸納起來主要分為兩類:

      1) Ping-Pong型心跳:由通信一方定時(shí)發(fā)送Ping消息,對(duì)方接收到Ping消息之后,立即返回Pong應(yīng)答消息給對(duì)方,屬于請(qǐng)求-響應(yīng)型心跳;

      2) Ping-Ping型心跳:不區(qū)分心跳請(qǐng)求和應(yīng)答,由通信雙方按照約定定時(shí)向?qū)Ψ桨l(fā)送心跳Ping消息,它屬于雙向心跳。

      心跳檢測(cè)策略如下:

      1) 連續(xù)N次心跳檢測(cè)都沒有收到對(duì)方的Pong應(yīng)答消息或者Ping請(qǐng)求消息,則認(rèn)為鏈路已經(jīng)發(fā)生邏輯失效,這被稱作心跳超時(shí);

      2) 讀取和發(fā)送心跳消息的時(shí)候如何直接發(fā)生了IO異常,說明鏈路已經(jīng)失效,這被稱為心跳失敗。

      無論發(fā)生心跳超時(shí)還是心跳失敗,都需要關(guān)閉鏈路,由客戶端發(fā)起重連操作,保證鏈路能夠恢復(fù)正常。

      Netty的心跳檢測(cè)實(shí)際上是利用了鏈路空閑檢測(cè)機(jī)制實(shí)現(xiàn)的,相關(guān)代碼如下:

      圖2-21 心跳檢測(cè)的代碼包路徑

      Netty提供的空閑檢測(cè)機(jī)制分為三種:

      1) 讀空閑,鏈路持續(xù)時(shí)間t沒有讀取到任何消息;

      2) 寫空閑,鏈路持續(xù)時(shí)間t沒有發(fā)送任何消息;

      3) 讀寫空閑,鏈路持續(xù)時(shí)間t沒有接收或者發(fā)送任何消息。

      Netty的默認(rèn)讀寫空閑機(jī)制是發(fā)生超時(shí)異常,關(guān)閉連接,但是,我們可以定制它的超時(shí)實(shí)現(xiàn)機(jī)制,以便支持不同的用戶場(chǎng)景。

      WriteTimeoutHandler的超時(shí)接口如下:

      圖2-22 寫超時(shí)

      ReadTimeoutHandler的超時(shí)接口如下:

      圖2-23 讀超時(shí)

      讀寫空閑的接口如下:

      圖2-24 讀寫空閑

      利用Netty提供的鏈路空閑檢測(cè)機(jī)制,可以非常靈活的實(shí)現(xiàn)協(xié)議層的心跳檢測(cè)。在《Netty權(quán)威指南》中的私有協(xié)議棧設(shè)計(jì)和開發(fā)章節(jié),我利用Netty提供的自定義Task接口實(shí)現(xiàn)了另一種心跳檢測(cè)機(jī)制,感興趣的朋友可以參閱該書。

      2.3. Reactor線程的保護(hù)

      Reactor線程是IO操作的核心,NIO框架的發(fā)動(dòng)機(jī),一旦出現(xiàn)故障,將會(huì)導(dǎo)致掛載在其上面的多路用復(fù)用器和多個(gè)鏈路無法正常工作。因此它的可靠性要求非常高。

      筆者就曾經(jīng)遇到過因?yàn)楫惓L幚聿划?dāng)導(dǎo)致Reactor線程跑飛,大量業(yè)務(wù)請(qǐng)求處理失敗的故障。下面我們一起看下Netty是如何有效提升Reactor線程的可靠性的。

      2.3.1. 異常處理要當(dāng)心

      盡管Reactor線程主要處理IO操作,發(fā)生的異常通常是IO異常,但是,實(shí)際上在一些特殊場(chǎng)景下會(huì)發(fā)生非IO異常,如果僅僅捕獲IO異??赡芫蜁?huì)導(dǎo)致Reactor線程跑飛。為了防止發(fā)生這種意外,在循環(huán)體內(nèi)一定要捕獲Throwable,而不是IO異常或者Exception。

      Netty的相關(guān)代碼如下:

      圖2-25 Reactor線程異常保護(hù)

      捕獲Throwable之后,即便發(fā)生了意外未知對(duì)異常,線程也不會(huì)跑飛,它休眠1S,防止死循環(huán)導(dǎo)致的異常繞接,然后繼續(xù)恢復(fù)執(zhí)行。這樣處理的核心理念就是:

      1) 某個(gè)消息的異常不應(yīng)該導(dǎo)致整條鏈路不可用;

      2) 某條鏈路不可用不應(yīng)該導(dǎo)致其它鏈路不可用;

      3) 某個(gè)進(jìn)程不可用不應(yīng)該導(dǎo)致其它集群節(jié)點(diǎn)不可用。

      2.3.2. 死循環(huán)保護(hù)

      通常情況下,死循環(huán)是可檢測(cè)、可預(yù)防但是無法完全避免的。Reactor線程通常處理的都是IO相關(guān)的操作,因此我們重點(diǎn)關(guān)注IO層面的死循環(huán)。

      JDK NIO類庫(kù)最著名的就是 epoll bug了,它會(huì)導(dǎo)致Selector空輪詢,IO線程CPU 100%,嚴(yán)重影響系統(tǒng)的安全性和可靠性。

      SUN在JKD1.6 update18版本聲稱解決了該BUG,但是根據(jù)業(yè)界的測(cè)試和大家的反饋,直到JDK1.7的早期版本,該BUG依然存在,并沒有完全被修復(fù)。發(fā)生該BUG的主機(jī)資源占用圖如下:

      圖2-26 epoll bug CPU空輪詢

      SUN在解決該BUG的問題上不給力,只能從NIO框架層面進(jìn)行問題規(guī)避,下面我們看下Netty是如何解決該問題的。

      Netty的解決策略:

      1) 根據(jù)該BUG的特征,首先偵測(cè)該BUG是否發(fā)生;

      2) 將問題Selector上注冊(cè)的Channel轉(zhuǎn)移到新建的Selector上;

      3) 老的問題Selector關(guān)閉,使用新建的Selector替換。

      下面具體看下代碼,首先檢測(cè)是否發(fā)生了該BUG:

      圖2-27 epoll bug 檢測(cè)

      一旦檢測(cè)發(fā)生該BUG,則重建Selector,代碼如下:

      圖2-28 重建Selector

      重建完成之后,替換老的Selector,代碼如下:

      圖2-29 替換Selector

      大量生產(chǎn)系統(tǒng)的運(yùn)行表明,Netty的規(guī)避策略可以解決epoll bug 導(dǎo)致的IO線程CPU死循環(huán)問題。

      2.4. 優(yōu)雅退出

      Java的優(yōu)雅停機(jī)通常通過注冊(cè)JDK的ShutdownHook來實(shí)現(xiàn),當(dāng)系統(tǒng)接收到退出指令后,首先標(biāo)記系統(tǒng)處于退出狀態(tài),不再接收新的消息,然后將積壓的消息處理完,最后調(diào)用資源回收接口將資源銷毀,最后各線程退出執(zhí)行。

      通常優(yōu)雅退出有個(gè)時(shí)間限制,例如30S,如果到達(dá)執(zhí)行時(shí)間仍然沒有完成退出前的操作,則由監(jiān)控腳本直接kill -9 pid,強(qiáng)制退出。

      Netty的優(yōu)雅退出功能隨著版本的優(yōu)化和演進(jìn)也在不斷的增強(qiáng),下面我們一起看下Netty5的優(yōu)雅退出。

      首先看下Reactor線程和線程組,它們提供了優(yōu)雅退出接口。EventExecutorGroup的接口定義如下:

      圖2-30 EventExecutorGroup優(yōu)雅退出

      NioEventLoop的資源釋放接口實(shí)現(xiàn):

      圖2-31 NioEventLoop資源釋放

      ChannelPipeline的關(guān)閉接口:

      圖2-32 ChannelPipeline關(guān)閉接口

      目前Netty向用戶提供的主要接口和類庫(kù)都提供了資源銷毀和優(yōu)雅退出的接口,用戶的自定義實(shí)現(xiàn)類可以繼承這些接口,完成用戶資源的釋放和優(yōu)雅退出。

      2.5. 內(nèi)存保護(hù)

      2.5.1. 緩沖區(qū)的內(nèi)存泄漏保護(hù)

      為了提升內(nèi)存的利用率,Netty提供了內(nèi)存池和對(duì)象池。但是,基于緩存池實(shí)現(xiàn)以后需要對(duì)內(nèi)存的申請(qǐng)和釋放進(jìn)行嚴(yán)格的管理,否則很容易導(dǎo)致內(nèi)存泄漏。

      如果不采用內(nèi)存池技術(shù)實(shí)現(xiàn),每次對(duì)象都是以方法的局部變量形式被創(chuàng)建,使用完成之后,只要不再繼續(xù)引用它,JVM會(huì)自動(dòng)釋放。但是,一旦引入內(nèi)存池機(jī)制,對(duì)象的生命周期將由內(nèi)存池負(fù)責(zé)管理,這通常是個(gè)全局引用,如果不顯式釋放JVM是不會(huì)回收這部分內(nèi)存的。

      對(duì)于Netty的用戶而言,使用者的技術(shù)水平差異很大,一些對(duì)JVM內(nèi)存模型和內(nèi)存泄漏機(jī)制不了解的用戶,可能只記得申請(qǐng)內(nèi)存,忘記主動(dòng)釋放內(nèi)存,特別是JAVA程序員。

      為了防止因?yàn)橛脩暨z漏導(dǎo)致內(nèi)存泄漏,Netty在Pipe line的尾Handler中自動(dòng)對(duì)內(nèi)存進(jìn)行釋放,相關(guān)代碼如下:

      圖2-33 TailHandler的內(nèi)存回收操作

      對(duì)于內(nèi)存池,實(shí)際就是將緩沖區(qū)重新放到內(nèi)存池中循環(huán)使用,代碼如下:

      圖2-34 PooledByteBuf的內(nèi)存回收操作

      2.5.2. 緩沖區(qū)內(nèi)存溢出保護(hù)

      做過協(xié)議棧的讀者都知道,當(dāng)我們對(duì)消息進(jìn)行解碼的時(shí)候,需要?jiǎng)?chuàng)建緩沖區(qū)。緩沖區(qū)的創(chuàng)建方式通常有兩種:

      1) 容量預(yù)分配,在實(shí)際讀寫過程中如果不夠再擴(kuò)展;

      2) 根據(jù)協(xié)議消息長(zhǎng)度創(chuàng)建緩沖區(qū)。

      在實(shí)際的商用環(huán)境中,如果遇到畸形碼流攻擊、協(xié)議消息編碼異常、消息丟包等問題時(shí),可能會(huì)解析到一個(gè)超長(zhǎng)的長(zhǎng)度字段。筆者曾經(jīng)遇到過類似問題,報(bào)文長(zhǎng)度字段值竟然是2G多,由于代碼的一個(gè)分支沒有對(duì)長(zhǎng)度上限做有效保護(hù),結(jié)果導(dǎo)致內(nèi)存溢出。系統(tǒng)重啟后幾秒內(nèi)再次內(nèi)存溢出,幸好及時(shí)定位出問題根因,險(xiǎn)些釀成嚴(yán)重的事故。

      Netty提供了編解碼框架,因此對(duì)于解碼緩沖區(qū)的上限保護(hù)就顯得非常重要。下面,我們看下Netty是如何對(duì)緩沖區(qū)進(jìn)行上限保護(hù)的:

      首先,在內(nèi)存分配的時(shí)候指定緩沖區(qū)長(zhǎng)度上限:

      圖2-35 緩沖區(qū)分配器可以指定緩沖區(qū)最大長(zhǎng)度

      其次,在對(duì)緩沖區(qū)進(jìn)行寫入操作的時(shí)候,如果緩沖區(qū)容量不足需要擴(kuò)展,首先對(duì)最大容量進(jìn)行判斷,如果擴(kuò)展后的容量超過上限,則拒絕擴(kuò)展:

      圖2-35 緩沖區(qū)擴(kuò)展上限保護(hù)

      最后,在解碼的時(shí)候,對(duì)消息長(zhǎng)度進(jìn)行判斷,如果超過最大容量上限,則拋出解碼異常,拒絕分配內(nèi)存:

      圖2-36 超出容量上限的半包解碼,失敗

      圖2-37 拋出TooLongFrameException異常

      2.6. 流量整形

      大多數(shù)的商用系統(tǒng)都有多個(gè)網(wǎng)元或者部件組成,例如參與短信互動(dòng),會(huì)涉及到手機(jī)、基站、短信中心、短信網(wǎng)關(guān)、SP/CP等網(wǎng)元。不同網(wǎng)元或者部件的處理性能不同。為了防止因?yàn)槔擞繕I(yè)務(wù)或者下游網(wǎng)元性能低導(dǎo)致下游網(wǎng)元被壓垮,有時(shí)候需要系統(tǒng)提供流量整形功能。

      下面我們一起看下流量整形(traffic shaping)的定義:流量整形(Traffic Shaping)是一種主動(dòng)調(diào)整流量輸出速率的措施。一個(gè)典型應(yīng)用是基于下游網(wǎng)絡(luò)結(jié)點(diǎn)的TP指標(biāo)來控制本地流量的輸出。流量整形與流量監(jiān)管的主要區(qū)別在于,流量整形對(duì)流量監(jiān)管中需要丟棄的報(bào)文進(jìn)行緩存——通常是將它們放入緩沖區(qū)或隊(duì)列內(nèi),也稱流量整形(Traffic Shaping,簡(jiǎn)稱TS)。當(dāng)令牌桶有足夠的令牌時(shí),再均勻的向外發(fā)送這些被緩存的報(bào)文。流量整形與流量監(jiān)管的另一區(qū)別是,整形可能會(huì)增加延遲,而監(jiān)管幾乎不引入額外的延遲。

      流量整形的原理示意圖如下:

      圖2-38 流量整形原理圖

      作為高性能的NIO框架,Netty的流量整形有兩個(gè)作用:

      1) 防止由于上下游網(wǎng)元性能不均衡導(dǎo)致下游網(wǎng)元被壓垮,業(yè)務(wù)流程中斷;

      2) 防止由于通信模塊接收消息過快,后端業(yè)務(wù)線程處理不及時(shí)導(dǎo)致的“撐死”問題。

      下面我們就具體學(xué)習(xí)下Netty的流量整形功能。

      2.6.1. 全局流量整形

      全局流量整形的作用范圍是進(jìn)程級(jí)的,無論你創(chuàng)建了多少個(gè)Channel,它的作用域針對(duì)所有的Channel。

      用戶可以通過參數(shù)設(shè)置:報(bào)文的接收速率、報(bào)文的發(fā)送速率、整形周期。相關(guān)的接口如下所示:

      圖2-39 全局流量整形參數(shù)設(shè)置

      Netty流量整形的原理是:對(duì)每次讀取到的ByteBuf可寫字節(jié)數(shù)進(jìn)行計(jì)算,獲取當(dāng)前的報(bào)文流量,然后與流量整形閾值對(duì)比。如果已經(jīng)達(dá)到或者超過了閾值。則計(jì)算等待時(shí)間delay,將當(dāng)前的ByteBuf放到定時(shí)任務(wù)Task中緩存,由定時(shí)任務(wù)線程池在延遲delay之后繼續(xù)處理該ByteBuf。相關(guān)代碼如下:

      圖2-40 動(dòng)態(tài)計(jì)算當(dāng)前流量

      如果達(dá)到整形閾值,則對(duì)新接收的ByteBuf進(jìn)行緩存,放入線程池的消息隊(duì)列中,稍后處理,代碼如下:

      圖2-41 緩存當(dāng)前的ByteBuf

      定時(shí)任務(wù)的延時(shí)時(shí)間根據(jù)檢測(cè)周期T和流量整形閾值計(jì)算得來,代碼如下:

      圖2-42 計(jì)算緩存等待周期

      需要指出的是,流量整形的閾值limit越大,流量整形的精度越高,流量整形功能是可靠性的一種保障,它無法做到100%的精確。這個(gè)跟后端的編解碼以及緩沖區(qū)的處理策略相關(guān),此處不再贅述。感興趣的朋友可以思考下,Netty為什么不做到 100%的精確。

      流量整形與流控的最大區(qū)別在于流控會(huì)拒絕消息,流量整形不拒絕和丟棄消息,無論接收量多大,它總能以近似恒定的速度下發(fā)消息,跟變壓器的原理和功能類似。

      2.6.2. 單條鏈路流量整形

      除了全局流量整形,Netty也支持但鏈路的流量整形,相關(guān)的接口定義如下:

      圖2-43 單鏈路流量整形

      單鏈路流量整形與全局流量整形的最大區(qū)別就是它以單個(gè)鏈路為作用域,可以對(duì)不同的鏈路設(shè)置不同的整形策略。

      它的實(shí)現(xiàn)原理與全局流量整形類似,我們不再贅述。值得說明的是,Netty支持用戶自定義流量整形策略,通過繼承AbstractTrafficShapingHandler的doAccounting方法可以定制整形策略。相關(guān)接口定義如下:

      圖2-44 定制流量整形策略

      3. 總結(jié)

      盡管Netty在架構(gòu)可靠性上面已經(jīng)做了很多精細(xì)化的設(shè)計(jì),以及基于防御式編程對(duì)系統(tǒng)進(jìn)行了大量可靠性保護(hù)。但是,系統(tǒng)的可靠性是個(gè)持續(xù)投入和改進(jìn)的過程,不可能在一個(gè)版本中一蹴而就,可靠性工作任重而道遠(yuǎn)。

      從業(yè)務(wù)的角度看,不同的行業(yè)、應(yīng)用場(chǎng)景對(duì)可靠性的要求也是不同的,例如電信行業(yè)的可靠性要求是5個(gè)9,對(duì)于鐵路等特殊行業(yè),可靠性要求更高,達(dá)到6個(gè)9。對(duì)于企業(yè)的一些邊緣IT系統(tǒng),可靠性要求會(huì)低些。

      可靠性是一種投資,對(duì)于企業(yè)而言,追求極端可靠性對(duì)研發(fā)成本是個(gè)沉重的包袱,但是相反,如果不重視系統(tǒng)的可靠性,一旦不幸遭遇網(wǎng)上事故,損失往往也是驚人的。

      對(duì)于架構(gòu)師和設(shè)計(jì)師,如何權(quán)衡架構(gòu)的可靠性和其它特性的關(guān)系,是一個(gè)很大的挑戰(zhàn)。通過研究和學(xué)習(xí)Netty的可靠性設(shè)計(jì),也許能夠給大家?guī)硪恍﹩⑹尽?/p>

      4. Netty學(xué)習(xí)推薦書籍

      目前市面上介紹netty的文章很多,如果讀者希望系統(tǒng)性的學(xué)習(xí)Netty,推薦兩本書:

      1) 《Netty in Action》

      2) 《Netty權(quán)威指南》

      5.作者簡(jiǎn)介

      李林鋒,2007年畢業(yè)于東北大學(xué),2008年進(jìn)入華為公司從事高性能通信軟件的設(shè)計(jì)和開發(fā)工作,有6年NIO設(shè)計(jì)和開發(fā)經(jīng)驗(yàn),精通Netty、Mina等NIO框架。Netty中國(guó)社區(qū)創(chuàng)始人,《Netty權(quán)威指南》作者。


        本站是提供個(gè)人知識(shí)管理的網(wǎng)絡(luò)存儲(chǔ)空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請(qǐng)注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購(gòu)買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊一鍵舉報(bào)。
        轉(zhuǎn)藏 分享 獻(xiàn)花(0

        0條評(píng)論

        發(fā)表

        請(qǐng)遵守用戶 評(píng)論公約

        類似文章 更多