Netty版本升級及線程模型詳解

做者 李林鋒 發佈於 2015年2月7日 | 注意:GTLC全球技術領導力峯會,500+CTO技聚從新定義技術領導力!18 討論後端

1. 背景

1.1. Netty 3.X系列版本現狀

根據對Netty社區部分用戶的調查,結合Netty在其它開源項目中的使用狀況,咱們能夠看出目前Netty商用的主流版本集中在3.X和4.X上,其中以Netty 3.X系列版本使用最爲普遍。promise

Netty社區很是活躍,3.X系列版本從2011年2月7日發佈的netty-3.2.4 Final版本到2014年12月17日發佈的netty-3.10.0 Final版本,版本跨度達3年多,期間共推出了61個Final版本。安全

1.2. 升級仍是堅守老版本

相比於其它開源項目,Netty用戶的版本升級之路更加艱辛,最根本的緣由就是Netty 4對Netty 3沒有作到很好的前向兼容。服務器

相關廠商內容微信

經過探針技術,實現Java應用程序自我防禦

新Java,新將來

你離成爲一位合格的技術領導者還有多遠?

你瞭解技術領導與技術管理的差異嗎?

相關贊助商網絡

QCon全球軟件開發大會上海站,2016年10月20日-22日,上海寶華萬豪酒店,精彩內容搶先看多線程

因爲版本不兼容,大多數老版本使用者的想法就是既然升級這麼麻煩,我暫時又不須要使用到Netty 4的新特性,當前版本還挺穩定,就暫時先不升級,之後看看再說。架構

堅守老版本還有不少其它的理由,例如考慮到線上系統的穩定性、對新版本的熟悉程度等。不管如何升級Netty都是一件大事,特別是對Netty有直接強依賴的產品。併發

從上面的分析能夠看出,堅守老版本彷佛是個不錯的選擇;可是,「理想是美好的,現實倒是殘酷的」,堅守老版本並不是老是那麼容易,下面咱們就看下被迫升級的案例。

1.3. 「被迫」升級到Netty 4.X

除了爲了使用新特性而主動進行的版本升級,大多數升級都是「被迫的」。下面咱們對這些升級緣由進行分析。

  1. 公司的開源軟件管理策略:對於那些大廠,不一樣部門和產品線依賴的開源軟件版本常常不一樣,爲了對開源依賴進行統一管理,下降安全、維護和管理成本,每每會指定優選的軟件版本。因爲Netty 4.X 系列版本已經很是成熟,由於,不少公司都優選Netty 4.X版本。
  2. 維護成本:不管是依賴Netty 3.X,仍是Netty4.X,每每須要在原框架之上作定製。例如,客戶端的短連重連、心跳檢測、流控等。分別對Netty 4.X和3.X版本實現兩套定製框架,開發和維護成本都很是高。根據開源軟件的使用策略,當存在版本衝突的時候,每每會選擇升級到更高的版本。對於Netty,依然遵循這個規則。
  3. 新特性:Netty 4.X相比於Netty 3.X,提供了不少新的特性,例如優化的內存管理池、對MQTT協議的支持等。若是用戶須要使用這些新特性,最簡便的作法就是升級Netty到4.X系列版本。
  4. 更優異的性能:Netty 4.X版本相比於3.X老版本,優化了內存池,減小了GC的頻率、下降了內存消耗;經過優化Rector線程池模型,用戶的開發更加簡單,線程調度也更加高效。

1.4. 升級不當付出的代價

表面上看,類庫包路徑的修改、API的重構等彷佛是升級的重頭戲,你們每每把注意力放到這些「明槍」上,但真正隱藏和致命的倒是「暗箭」。若是對Netty底層的事件調度機制和線程模型不熟悉,每每就會「中槍」。

本文以幾個比較典型的真實案例爲例,經過問題描述、問題定位和問題總結,讓這些隱藏的「暗箭」再也不傷人。

因爲Netty 4線程模型改變致使的升級事故還有不少,限於篇幅,本文不一一枚舉,這些問題萬變不離其宗,只要抓住線程模型這個關鍵點,所謂的疑難雜症都將迎刃而解。

2. Netty升級以後遭遇內存泄露

2.1. 問題描述

隨着JVM虛擬機和JIT即時編譯技術的發展,對象的分配和回收是個很是輕量級的工做。可是對於緩衝區Buffer,狀況卻稍有不一樣,特別是對於堆外直接內存的分配和回收,是一件耗時的操做。爲了儘可能重用緩衝區,Netty4.X提供了基於內存池的緩衝區重用機制。性能測試代表,採用內存池的ByteBuf相比於朝生夕滅的ByteBuf,性能高23倍左右(性能數據與使用場景強相關)。

業務應用的特色是高併發、短流程,大多數對象都是朝生夕滅的短生命週期對象。爲了減小內存的拷貝,用戶指望在序列化的時候直接將對象編碼到PooledByteBuf裏,這樣就不須要爲每一個業務消息都從新申請和釋放內存。

業務的相關代碼示例以下:

//在業務線程中初始化內存池分配器,分配非堆內存
 ByteBufAllocator allocator = new PooledByteBufAllocator(true);
 ByteBuf buffer = allocator.ioBuffer(1024);
//構造訂購請求消息並賦值,業務邏輯省略
SubInfoReq infoReq = new SubInfoReq ();
infoReq.setXXX(......);
//將對象編碼到ByteBuf中
codec.encode(buffer, info);
//調用ChannelHandlerContext進行消息發送
ctx.writeAndFlush(buffer);

業務代碼升級Netty版本並重構以後,運行一段時間,Java進程就會宕機,查看系統運行日誌發現系統發生了內存泄露(示例堆棧):

圖2-1 OOM內存溢出堆棧

對內存進行監控(切換使用堆內存池,方便對內存進行監控),發現堆內存一直飆升,以下所示(示例堆內存監控):

圖2-2 堆內存監控

2.2. 問題定位

使用jmap -dump:format=b,file=netty.bin PID 將堆內存dump出來,經過IBM的HeapAnalyzer工具進行分析,發現ByteBuf發生了泄露。

由於使用了內存池,因此首先懷疑是否是申請的ByteBuf沒有被釋放致使?查看代碼,發現消息發送完成以後,Netty底層已經調用ReferenceCountUtil.release(message)對內存進行了釋放。這是怎麼回事呢?難道Netty 4.X的內存池有Bug,調用release操做釋放內存失敗?

考慮到Netty 內存池自身Bug的可能性不大,首先從業務的使用方式入手分析:

  1. 內存的分配是在業務代碼中進行,因爲使用到了業務線程池作I/O操做和業務操做的隔離,實際上內存是在業務線程中分配的;
  2. 內存的釋放操做是在outbound中進行,按照Netty 3的線程模型,downstream(對應Netty 4的outbound,Netty 4取消了upstream和downstream)的handler也是由業務調用者線程執行的,也就是說釋放跟分配在同一個業務線程中進行。

初次排查並無發現致使內存泄露的根因,束手無策之際開始查看Netty的內存池分配器PooledByteBufAllocator的Doc和源碼實現,發現內存池實際是基於線程上下文實現的,相關代碼以下:

final ThreadLocal<PoolThreadCache> threadCache = new ThreadLocal<PoolThreadCache>() {
        private final AtomicInteger index = new AtomicInteger();
        @Override
        protected PoolThreadCache initialValue() {
            final int idx = index.getAndIncrement();
            final PoolArena<byte[]> heapArena;
            final PoolArena<ByteBuffer> directArena;
            if (heapArenas != null) {
                heapArena = heapArenas[Math.abs(idx % heapArenas.length)];
            } else {
                heapArena = null;
            }
            if (directArenas != null) {
                directArena = directArenas[Math.abs(idx % directArenas.length)];
            } else {
                directArena = null;
            }
            return new PoolThreadCache(heapArena, directArena);
        }

也就是說內存的申請和釋放必須在同一線程上下文中,不能跨線程。跨線程以後實際操做的就不是同一塊內存區域,這會致使不少嚴重的問題,內存泄露即是其中之一。內存在A線程申請,切換到B線程釋放,實際是沒法正確回收的。

經過對Netty內存池的源碼分析,問題基本鎖定。保險起見進行簡單驗證,經過對單條業務消息進行Debug,發現執行釋放的果真不是業務線程,而是Netty的NioEventLoop線程:當某個消息被徹底發送成功以後,會經過ReferenceCountUtil.release(message)方法釋放已經發送成功的ByteBuf。

問題定位出來以後,繼續溯源,發現Netty 4修改了Netty 3的線程模型:在Netty 3的時候,upstream是在I/O線程裏執行的,而downstream是在業務線程裏執行。當Netty從網絡讀取一個數據報投遞給業務handler的時候,handler是在I/O線程裏執行;而當咱們在業務線程中調用write和writeAndFlush向網絡發送消息的時候,handler是在業務線程裏執行,直到最後一個Header handler將消息寫入到發送隊列中,業務線程才返回。

Netty4修改了這一模型,在Netty 4裏inbound(對應Netty 3的upstream)和outbound(對應Netty 3的downstream)都是在NioEventLoop(I/O線程)中執行。當咱們在業務線程裏經過ChannelHandlerContext.write發送消息的時候,Netty 4在將消息發送事件調度到ChannelPipeline的時候,首先將待發送的消息封裝成一個Task,而後放到NioEventLoop的任務隊列中,由NioEventLoop線程異步執行。後續全部handler的調度和執行,包括消息的發送、I/O事件的通知,都由NioEventLoop線程負責處理。

下面咱們分別經過對比Netty 3和Netty 4的消息接收和發送流程,來理解兩個版本線程模型的差別:

Netty 3的I/O事件處理流程:

圖2-3 Netty 3 I/O事件處理線程模型

Netty 4的I/O消息處理流程:

圖2-4 Netty 4 I/O事件處理線程模型

2.3. 問題總結

Netty 4.X版本新增的內存池確實很是高效,可是若是使用不當則會致使各類嚴重的問題。諸如內存泄露這類問題,功能測試並無異常,若是相關接口沒有進行壓測或者穩定性測試而直接上線,則會致使嚴重的線上問題。

內存池PooledByteBuf的使用建議:

  1. 申請以後必定要記得釋放,Netty自身Socket讀取和發送的ByteBuf系統會自動釋放,用戶不須要作二次釋放;若是用戶使用Netty的內存池在應用中作ByteBuf的對象池使用,則須要本身主動釋放;
  2. 避免錯誤的釋放:跨線程釋放、重複釋放等都是非法操做,要避免。特別是跨線程申請和釋放,每每具備隱蔽性,問題定位難度較大;
  3. 防止隱式的申請和分配:以前曾經發生過一個案例,爲了解決內存池跨線程申請和釋放問題,有用戶對內存池作了二次包裝,以實現多線程操做時,內存始終由包裝的管理線程申請和釋放,這樣能夠屏蔽用戶業務線程模型和訪問方式的差別。誰知運行一段時間以後再次發生了內存泄露,最後發現原來調用ByteBuf的write操做時,若是內存容量不足,會自動進行容量擴展。擴展操做由業務線程執行,這就繞過了內存池管理線程,發生了「引用逃逸」。該Bug只有在ByteBuf容量動態擴展的時候才發生,所以,上線很長一段時間沒有發生,直到某一天......所以,你們在使用Netty 4.X的內存池時要格外小心,特別是作二次封裝時,必定要對內存池的實現細節有深入的理解。

3. Netty升級以後遭遇數據被篡改

3.1. 問題描述

某業務產品,Netty3.X升級到4.X以後,系統運行過程當中,偶現服務端發送給客戶端的應答數據被莫名「篡改」。

業務服務端的處理流程以下:

  1. 將解碼後的業務消息封裝成Task,投遞到後端的業務線程池中執行;
  2. 業務線程處理業務邏輯,完成以後構造應答消息發送給客戶端;
  3. 業務應答消息的編碼經過繼承Netty的CodeC框架實現,即Encoder ChannelHandler;
  4. 調用Netty的消息發送接口以後,流程繼續,根據業務場景,可能會繼續操做原發送的業務對象。

業務相關代碼示例以下:

//構造訂購應答消息
SubInfoResp infoResp = new SubInfoResp();
//根據業務邏輯,對應答消息賦值
infoResp.setResultCode(0);
infoResp.setXXX();
後續賦值操做省略......
//調用ChannelHandlerContext進行消息發送
ctx.writeAndFlush(infoResp);
//消息發送完成以後,後續根據業務流程進行分支處理,修改infoResp對象
infoResp.setXXX();
後續代碼省略......

3.2. 問題定位

首先對應答消息被非法「篡改」的緣由進行分析,通過定位發現當發生問題時,被「篡改」的內容是調用writeAndFlush接口以後,由後續業務分支代碼修改應答消息致使的。因爲修改操做發生在writeAndFlush操做以後,按照Netty 3.X的線程模型不該該出現該問題。

在Netty3中,downstream是在業務線程裏執行的,也就是說對SubInfoResp的編碼操做是在業務線程中執行的,當編碼後的ByteBuf對象被投遞到消息發送隊列以後,業務線程纔會返回並繼續執行後續的業務邏輯,此時修改應答消息是不會改變已完成編碼的ByteBuf對象的,因此確定不會出現應答消息被篡改的問題。

初步分析應該是因爲線程模型發生變動致使的問題,隨後查驗了Netty 4的線程模型,果真發生了變化:當調用outbound向外發送消息的時候,Netty會將發送事件封裝成Task,投遞到NioEventLoop的任務隊列中異步執行,相關代碼以下:

@Override
 public void invokeWrite(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) {
        if (msg == null) {
            throw new NullPointerException("msg");
        }
        validatePromise(ctx, promise, true);
        if (executor.inEventLoop()) {
            invokeWriteNow(ctx, msg, promise);
        } else {
            AbstractChannel channel = (AbstractChannel) ctx.channel();
            int size = channel.estimatorHandle().size(msg);
            if (size > 0) {
                ChannelOutboundBuffer buffer = channel.unsafe().outboundBuffer();
                // Check for null as it may be set to null if the channel is closed already
                if (buffer != null) {
                    buffer.incrementPendingOutboundBytes(size);
                }
            }
            safeExecuteOutbound(WriteTask.newInstance(ctx, msg, size, promise), promise, msg);
        }
    }

經過上述代碼能夠看出,Netty首先對當前的操做的線程進行判斷,若是操做自己就是由NioEventLoop線程執行,則調用寫操做;不然,執行線程安全的寫操做,即將寫事件封裝成Task,放入到任務隊列中由Netty的I/O線程執行,業務調用返回,流程繼續執行。

經過源碼分析,問題根源已經很清楚:系統升級到Netty 4以後,線程模型發生變化,響應消息的編碼由NioEventLoop線程異步執行,業務線程返回。這時存在兩種可能:

  1. 若是編碼操做先於修改應答消息的業務邏輯執行,則運行結果正確;
  2. 若是編碼操做在修改應答消息的業務邏輯以後執行,則運行結果錯誤。

因爲線程的執行前後順序沒法預測,所以該問題隱藏的至關深。若是對Netty 4和Netty3的線程模型不瞭解,就會掉入陷阱。

Netty 3版本業務邏輯沒有問題,流程以下:

圖3-1 升級以前的業務流程線程模型

升級到Netty 4版本以後,業務流程因爲Netty線程模型的變動而發生改變,致使業務邏輯發生問題:

圖3-2 升級以後的業務處理流程發生改變

3.3. 問題總結

不少讀者在進行Netty 版本升級的時候,只關注到了包路徑、類和API的變動,並無注意到隱藏在背後的「暗箭」- 線程模型變動。

升級到Netty 4的用戶須要根據新的線程模型對已有的系統進行評估,重點須要關注outbound的ChannelHandler,若是它的正確性依賴於Netty 3的線程模型,則極可能在新的線程模型中出問題,多是功能問題或者其它問題。

4. Netty升級以後性能嚴重降低

4.1. 問題描述

相信不少Netty用戶都看過以下相關報告:

在Twitter,Netty 4 GC開銷降爲五分之一:Netty 3使用Java對象表示I/O事件,這樣簡單,但會產生大量的垃圾,尤爲是在咱們這樣的規模下。Netty 4在新版本中對此作出了更改,取代生存週期短的事件對象,而以定義在生存週期長的通道對象上的方法處理I/O事件。它還有一個使用池的專用緩衝區分配器。

每當收到新信息或者用戶發送信息到遠程端,Netty 3均會建立一個新的堆緩衝區。這意味着,對應每個新的緩衝區,都會有一個‘new byte[capacity]’。這些緩衝區會致使GC壓力,並消耗內存帶寬:爲了安全起見,新的字節數組分配時會用零填充,這會消耗內存帶寬。然而,用零填充的數組極可能會再次用實際的數據填充,這又會消耗一樣的內存帶寬。若是Java虛擬機(JVM)提供了建立新字節數組而又無需用零填充的方式,那麼咱們原本就能夠將內存帶寬消耗減小50%,可是目前沒有那樣一種方式。

在Netty 4中,代碼定義了粒度更細的API,用來處理不一樣的事件類型,而不是建立事件對象。它還實現了一個新緩衝池,那是一個純Java版本的 jemalloc (Facebook也在用)。如今,Netty不會再由於用零填充緩衝區而浪費內存帶寬了。

咱們比較了兩個分別創建在Netty 3和4基礎上echo協議服務器。(Echo很是簡單,這樣,任何垃圾的產生都是Netty的緣由,而不是協議的緣由)。我使它們服務於相同的分佈式echo協議客戶端,來自這些客戶端的16384個併發鏈接重複發送256字節的隨機負載,幾乎使千兆以太網飽和。

根據測試結果,Netty 4:

  • GC中斷頻率是原來的1/5: 45.5 vs. 9.2次/分鐘
  • 垃圾生成速度是原來的1/5: 207.11 vs 41.81 MiB/秒

正是看到了相關的Netty 4性能提高報告,不少用戶選擇了升級。過後一些用戶反饋Netty 4並無跟產品帶來預期的性能提高,有些甚至還發生了很是嚴重的性能降低,下面咱們就以某業務產品的失敗升級經歷爲案例,詳細分析下致使性能降低的緣由。

4.2. 問題定位

首先經過JMC等性能分析工具對性能熱點進行分析,示例以下(信息安全等緣由,只給出分析過程示例截圖):

圖4-1 JMC性能監控分析

經過對熱點方法的分析,發如今消息發送過程當中,有兩處熱點:

  1. 消息發送性能統計相關Handler;
  2. 編碼Handler。

對使用Netty 3版本的業務產品進行性能對比測試,發現上述兩個Handler也是熱點方法。既然都是熱點,爲啥切換到Netty4以後性能降低這麼厲害呢?

經過方法的調用樹分析發現了兩個版本的差別:在Netty 3中,上述兩個熱點方法都是由業務線程負責執行;而在Netty 4中,則是由NioEventLoop(I/O)線程執行。對於某個鏈路,業務是擁有多個線程的線程池,而NioEventLoop只有一個,因此執行效率更低,返回給客戶端的應答時延就大。時延增大以後,天然致使系統併發量下降,性能降低。

找出問題根因以後,針對Netty 4的線程模型對業務進行專項優化,性能達到預期,遠超過了Netty 3老版本的性能。

Netty 3的業務線程調度模型圖以下所示:充分利用了業務多線程並行編碼和Handler處理的優點,週期T內能夠處理N條業務消息。

圖4-2 Netty 3業務調度性能模型

切換到Netty 4以後,業務耗時Handler被I/O線程串行執行,所以性能發生比較大的降低:

圖4-3 Netty 4業務調度性能模型

4.3. 問題總結

該問題的根因仍是因爲Netty 4的線程模型變動引發,線程模型變動以後,不只影響業務的功能,甚至對性能也會形成很大的影響。

對Netty的升級須要從功能、兼容性和性能等多個角度進行綜合考慮,切不可只盯着API變動這個芝麻,而丟掉了性能這個西瓜。API的變動會致使編譯錯誤,可是性能降低卻隱藏於無形之中,稍不留意就會中招。

對於講究快速交付、敏捷開發和灰度發佈的互聯網應用,升級的時候更應該要小心。

5. Netty升級以後上下文丟失

5.1. 問題描述

爲了提高業務的二次定製能力,下降對接口的侵入性,業務使用線程變量進行消息上下文的傳遞。例如消息發送源地址信息、消息Id、會話Id等。

業務同時使用到了一些第三方開源容器,也提供了線程級變量上下文的能力。業務經過容器上下文獲取第三方容器的系統變量信息。

升級到Netty 4以後,業務繼承自Netty的ChannelHandler發生了空指針異常,不管是業務自定義的線程上下文、仍是第三方容器的線程上下文,都獲取不到傳遞的變量值。

5.2. 問題定位

首先檢查代碼,看業務是否傳遞了相關變量,確認業務傳遞以後懷疑跟Netty 版本升級相關,調試發現,業務ChannelHandler獲取的線程上下文對象和以前業務傳遞的上下文不是同一個。這就說明執行ChannelHandler的線程跟處理業務的線程不是同一個線程!

查看Netty 4線程模型的相關Doc發現,Netty修改了outbound的線程模型,正好影響了業務消息發送時的線程上下文傳遞,最終致使線程變量丟失。

5.3. 問題總結

一般業務的線程模型有以下幾種:

  1. 業務自定義線程池/線程組處理業務,例如使用JDK 1.5提供的ExecutorService;
  2. 使用J2EE Web容器自帶的線程模型,常見的如JBoss和Tomcat的HTTP接入線程等;
  3. 隱式的使用其它第三方框架的線程模型,例如使用NIO框架進行協議處理,業務代碼隱式使用的就是NIO框架的線程模型,除非業務明確的實現自定義線程模型。

在實踐中咱們發現不少業務使用了第三方框架,可是隻熟悉API和功能,對線程模型並不清楚。某個類庫由哪一個線程調用,糊里糊塗。爲了方便變量傳遞,又隨意的使用線程變量,實際對背後第三方類庫的線程模型產生了強依賴。當容器或者第三方類庫升級以後,若是線程模型發生了變動,則原有功能就會發生問題。

鑑於此,在實際工做中,儘可能不要強依賴第三方類庫的線程模型,若是確實沒法避免,則必須對它的線程模型有深刻和清晰的瞭解。當第三方類庫升級以後,須要檢查線程模型是否發生變動,若是發生變化,相關的代碼也須要考慮同步升級。

6. Netty3.X VS Netty4.X 之線程模型

經過對三個具備典型性的升級失敗案例進行分析和總結,咱們發現有個共性:都是線程模型改變惹的禍!

下面小節咱們就詳細得對Netty3和Netty4版本的I/O線程模型進行對比,以方便你們掌握二者的差別,在升級和使用中儘可能少踩雷。

6.1 Netty 3.X 版本線程模型

Netty 3.X的I/O操做線程模型比較複雜,它的處理模型包括兩部分:

  1. Inbound:主要包括鏈路創建事件、鏈路激活事件、讀事件、I/O異常事件、鏈路關閉事件等;
  2. Outbound:主要包括寫事件、鏈接事件、監聽綁定事件、刷新事件等。

咱們首先分析下Inbound操做的線程模型:

圖6-1 Netty 3 Inbound操做線程模型

從上圖能夠看出,Inbound操做的主要處理流程以下:

  1. I/O線程(Work線程)將消息從TCP緩衝區讀取到SocketChannel的接收緩衝區中;
  2. 由I/O線程負責生成相應的事件,觸發事件向上執行,調度到ChannelPipeline中;
  3. I/O線程調度執行ChannelPipeline中Handler鏈的對應方法,直到業務實現的Last Handler;
  4. Last Handler將消息封裝成Runnable,放入到業務線程池中執行,I/O線程返回,繼續讀/寫等I/O操做;
  5. 業務線程池從任務隊列中彈出消息,併發執行業務邏輯。

經過對Netty 3的Inbound操做進行分析咱們能夠看出,Inbound的Handler都是由Netty的I/O Work線程負責執行。

下面咱們繼續分析Outbound操做的線程模型:

圖6-2 Netty 3 Outbound操做線程模型

從上圖能夠看出,Outbound操做的主要處理流程以下:

業務線程發起Channel Write操做,發送消息;

  1. Netty將寫操做封裝成寫事件,觸發事件向下傳播;
  2. 寫事件被調度到ChannelPipeline中,由業務線程按照Handler Chain串行調用支持Downstream事件的Channel Handler;
  3. 執行到系統最後一個ChannelHandler,將編碼後的消息Push到發送隊列中,業務線程返回;
  4. Netty的I/O線程從發送消息隊列中取出消息,調用SocketChannel的write方法進行消息發送。

6.2 Netty 4.X 版本線程模型

相比於Netty 3.X系列版本,Netty 4.X的I/O操做線程模型比較簡答,它的原理圖以下所示:

圖6-3 Netty 4 Inbound和Outbound操做線程模型

從上圖能夠看出,Outbound操做的主要處理流程以下:

  1. I/O線程NioEventLoop從SocketChannel中讀取數據報,將ByteBuf投遞到ChannelPipeline,觸發ChannelRead事件;
  2. I/O線程NioEventLoop調用ChannelHandler鏈,直到將消息投遞到業務線程,而後I/O線程返回,繼續後續的讀寫操做;
  3. 業務線程調用ChannelHandlerContext.write(Object msg)方法進行消息發送;
  4. 若是是由業務線程發起的寫操做,ChannelHandlerInvoker將發送消息封裝成Task,放入到I/O線程NioEventLoop的任務隊列中,由NioEventLoop在循環中統一調度和執行。放入任務隊列以後,業務線程返回;
  5. I/O線程NioEventLoop調用ChannelHandler鏈,進行消息發送,處理Outbound事件,直到將消息放入發送隊列,而後喚醒Selector,進而執行寫操做。

經過流程分析,咱們發現Netty 4修改了線程模型,不管是Inbound仍是Outbound操做,統一由I/O線程NioEventLoop調度執行。

6.3. 線程模型對比

在進行新老版本線程模型PK以前,首先仍是要熟悉下串行化設計的理念:

咱們知道當系統在運行過程當中,若是頻繁的進行線程上下文切換,會帶來額外的性能損耗。多線程併發執行某個業務流程,業務開發者還須要時刻對線程安全保持警戒,哪些數據可能會被併發修改,如何保護?這不只下降了開發效率,也會帶來額外的性能損耗。

爲了解決上述問題,Netty 4採用了串行化設計理念,從消息的讀取、編碼以及後續Handler的執行,始終都由I/O線程NioEventLoop負責,這就意外着整個流程不會進行線程上下文的切換,數據也不會面臨被併發修改的風險,對於用戶而言,甚至不須要了解Netty的線程細節,這確實是個很是好的設計理念,它的工做原理圖以下:

圖6-4 Netty 4的串行化設計理念

一個NioEventLoop聚合了一個多路複用器Selector,所以能夠處理成百上千的客戶端鏈接,Netty的處理策略是每當有一個新的客戶端接入,則從NioEventLoop線程組中順序獲取一個可用的NioEventLoop,當到達數組上限以後,從新返回到0,經過這種方式,能夠基本保證各個NioEventLoop的負載均衡。一個客戶端鏈接只註冊到一個NioEventLoop上,這樣就避免了多個I/O線程去併發操做它。

Netty經過串行化設計理念下降了用戶的開發難度,提高了處理性能。利用線程組實現了多個串行化線程水平並行執行,線程之間並無交集,這樣既能夠充分利用多核提高並行處理能力,同時避免了線程上下文的切換和併發保護帶來的額外性能損耗。

瞭解完了Netty 4的串行化設計理念以後,咱們繼續看Netty 3線程模型存在的問題,總結起來,它的主要問題以下:

  1. Inbound和Outbound實質都是I/O相關的操做,它們的線程模型居然不統一,這給用戶帶來了更多的學習和使用成本;
  2. Outbound操做由業務線程執行,一般業務會使用線程池並行處理業務消息,這就意味着在某一個時刻會有多個業務線程同時操做ChannelHandler,咱們須要對ChannelHandler進行併發保護,一般須要加鎖。若是同步塊的範圍不當,可能會致使嚴重的性能瓶頸,這對開發者的技能要求很是高,下降了開發效率;
  3. Outbound操做過程當中,例如消息編碼異常,會產生Exception,它會被轉換成Inbound的Exception並通知到ChannelPipeline,這就意味着業務線程發起了Inbound操做!它打破了Inbound操做由I/O線程操做的模型,若是開發者按照Inbound操做只會由一個I/O線程執行的約束進行設計,則會發生線程併發訪問安全問題。因爲該場景只在特定異常時發生,所以錯誤很是隱蔽!一旦在生產環境中發生此類線程併發問題,定位難度和成本都很是大。

講了這麼多,彷佛Netty 4 完勝 Netty 3的線程模型,其實並不盡然。在特定的場景下,Netty 3的性能可能更高,就如本文第4章節所講,若是編碼和其它Outbound操做很是耗時,由多個業務線程併發執行,性能確定高於單個NioEventLoop線程。

可是,這種性能優點不是不可逆轉的,若是咱們修改業務代碼,將耗時的Handler操做前置,Outbound操做不作複雜業務邏輯處理,性能一樣不輸於Netty 3,可是考慮內存池優化、不會反覆建立Event、不須要對Handler加鎖等Netty 4的優化,總體性能Netty 4版本確定會更高。

總而言之,若是用戶真正熟悉並掌握了Netty 4的線程模型和功能類庫,相信不只僅開發會更加簡單,性能也會更優!

6.4. 思考

就Netty 而言,掌握線程模型的重要性不亞於熟悉它的API和功能。不少時候我遇到的功能、性能等問題,都是因爲缺少對它線程模型和原理的理解致使的,結果咱們就以訛傳訛,認爲Netty 4版本不如3好用等。

不能說全部開源軟件的版本升級必定都賽過老版本,就Netty而言,我認爲Netty 4版本相比於老的Netty 3,確實是歷史的一大進步。

7. 做者簡介

李林鋒,2007年畢業於東北大學,2008年進入華爲公司從事高性能通訊軟件的設計和開發工做,有7年NIO設計和開發經驗,精通Netty、Mina等NIO框架和平臺中間件,現任華爲軟件平臺架構部架構師,《Netty權威指南》做者

相關文章
相關標籤/搜索