Netty注意事項

該文章為轉載,原文章請點擊

1. 背景

1.1. Netty 3.X系列版本現(xiàn)狀

根據(jù)對Netty社區(qū)部分用戶的調(diào)查絮识,結合Netty在其它開源項目中的使用情況次舌,我們可以看出目前Netty商用的主流版本集中在3.X和4.X上兽愤,其中以Netty 3.X系列版本使用最為廣泛彼念。

Netty社區(qū)非常活躍浅萧,3.X系列版本從2011年2月7日發(fā)布的netty-3.2.4 Final版本到2014年12月17日發(fā)布的netty-3.10.0 Final版本逐沙,版本跨度達3年多,期間共推出了61個Final版本洼畅。

1.2. 升級還是堅守老版本

相比于其它開源項目吩案,Netty用戶的版本升級之路更加艱辛,最根本的原因就是Netty 4對Netty 3沒有做到很好的前向兼容土思。

由于版本不兼容务热,大多數(shù)老版本使用者的想法就是既然升級這么麻煩,我暫時又不需要使用到Netty 4的新特性己儒,當前版本還挺穩(wěn)定崎岂,就暫時先不升級,以后看看再說闪湾。

堅守老版本還有很多其它的理由冲甘,例如考慮到線上系統(tǒng)的穩(wěn)定性、對新版本的熟悉程度等途样。無論如何升級Netty都是一件大事江醇,特別是對Netty有直接強依賴的產(chǎn)品。

從上面的分析可以看出何暇,堅守老版本似乎是個不錯的選擇陶夜;但是,“理想是美好的,現(xiàn)實卻是殘酷的”裆站,堅守老版本并非總是那么容易,下面我們就看下被迫升級的案例羽嫡。

1.3. “被迫”升級到Netty 4.X

除了為了使用新特性而主動進行的版本升級,大多數(shù)升級都是“被迫的”魂爪。下面我們對這些升級原因進行分析密浑。

  1. 公司的開源軟件管理策略:對于那些大廠尔破,不同部門和產(chǎn)品線依賴的開源軟件版本經(jīng)常不同,為了對開源依賴進行統(tǒng)一管理耘擂,降低安全秩霍、維護和管理成本铃绒,往往會指定優(yōu)選的軟件版本。由于Netty 4.X 系列版本已經(jīng)非常成熟赔癌,因為,很多公司都優(yōu)選Netty 4.X版本刊苍。
  2. 維護成本:無論是依賴Netty 3.X席噩,還是Netty4.X贤壁,往往需要在原框架之上做定制馒索。例如旨怠,客戶端的短連重連、心跳檢測、流控等爽哎。分別對Netty 4.X和3.X版本實現(xiàn)兩套定制框架课锌,開發(fā)和維護成本都非常高。根據(jù)開源軟件的使用策略志鞍,當存在版本沖突的時候,往往會選擇升級到更高的版本玻孟。對于Netty,依然遵循這個規(guī)則匣掸。
  3. 新特性:Netty 4.X相比于Netty 3.X,提供了很多新的特性,例如優(yōu)化的內(nèi)存管理池戴差、對MQTT協(xié)議的支持等袭厂。如果用戶需要使用這些新特性帖烘,最簡便的做法就是升級Netty到4.X系列版本。
  4. 更優(yōu)異的性能:Netty 4.X版本相比于3.X老版本历极,優(yōu)化了內(nèi)存池,減少了GC的頻率锄列、降低了內(nèi)存消耗;通過優(yōu)化Rector線程池模型筒严,用戶的開發(fā)更加簡單,線程調(diào)度也更加高效娶视。

1.4. 升級不當付出的代價

表面上看柒傻,類庫包路徑的修改青柄、API的重構等似乎是升級的重頭戲,大家往往把注意力放到這些“明槍”上喇喉,但真正隱藏和致命的卻是“暗箭”。如果對Netty底層的事件調(diào)度機制和線程模型不熟悉膏斤,往往就會“中槍”。

本文以幾個比較典型的真實案例為例沮榜,通過問題描述、問題定位和問題總結守呜,讓這些隱藏的“暗箭”不再傷人型酥。

由于Netty 4線程模型改變導致的升級事故還有很多,限于篇幅查乒,本文不一一枚舉弥喉,這些問題萬變不離其宗,只要抓住線程模型這個關鍵點侣颂,所謂的疑難雜癥都將迎刃而解档桃。

2. Netty升級之后遭遇內(nèi)存泄露

2.1. 問題描述

隨著JVM虛擬機和JIT即時編譯技術的發(fā)展嘹屯,對象的分配和回收是個非常輕量級的工作婆翔。但是對于緩沖區(qū)Buffer,情況卻稍有不同慷嗜,特別是對于堆外直接內(nèi)存的分配和回收盏袄,是一件耗時的操作滤钱。為了盡量重用緩沖區(qū)争剿,Netty4.X提供了基于內(nèi)存池的緩沖區(qū)重用機制幔嫂。性能測試表明,采用內(nèi)存池的ByteBuf相比于朝生夕滅的ByteBuf爷贫,性能高23倍左右(性能數(shù)據(jù)與使用場景強相關)未巫。

業(yè)務應用的特點是高并發(fā)赡模、短流程,大多數(shù)對象都是朝生夕滅的短生命周期對象盾碗。為了減少內(nèi)存的拷貝榜轿,用戶期望在序列化的時候直接將對象編碼到PooledByteBuf里诬烹,這樣就不需要為每個業(yè)務消息都重新申請和釋放內(nèi)存购岗。

業(yè)務的相關代碼示例如下:

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

業(yè)務代碼升級Netty版本并重構之后熬拒,運行一段時間,Java進程就會宕機垫竞,查看系統(tǒng)運行日志發(fā)現(xiàn)系統(tǒng)發(fā)生了內(nèi)存泄露(示例堆棧):

OOM內(nèi)存溢出堆棧

對內(nèi)存進行監(jiān)控(切換使用堆內(nèi)存池澎粟,方便對內(nèi)存進行監(jiān)控),發(fā)現(xiàn)堆內(nèi)存一直飆升欢瞪,如下所示(示例堆內(nèi)存監(jiān)控):

圖2-2 堆內(nèi)存監(jiān)控

2.2. 問題定位

使用jmap -dump:format=b,file=netty.bin PID 將堆內(nèi)存dump出來活烙,通過IBM的HeapAnalyzer工具進行分析,發(fā)現(xiàn)ByteBuf發(fā)生了泄露遣鼓。

因為使用了內(nèi)存池啸盏,所以首先懷疑是不是申請的ByteBuf沒有被釋放導致?查看代碼骑祟,發(fā)現(xiàn)消息發(fā)送完成之后回懦,Netty底層已經(jīng)調(diào)用ReferenceCountUtil.release(message)對內(nèi)存進行了釋放气笙。這是怎么回事呢?難道Netty 4.X的內(nèi)存池有Bug粉怕,調(diào)用release操作釋放內(nèi)存失斀∶瘛?

考慮到Netty 內(nèi)存池自身Bug的可能性不大贫贝,首先從業(yè)務的使用方式入手分析:

  1. 內(nèi)存的分配是在業(yè)務代碼中進行秉犹,由于使用到了業(yè)務線程池做I/O操作和業(yè)務操作的隔離,實際上內(nèi)存是在業(yè)務線程中分配的稚晚;
  2. 內(nèi)存的釋放操作是在outbound中進行崇堵,按照Netty 3的線程模型,downstream(對應Netty 4的outbound客燕,Netty 4取消了upstream和downstream)的handler也是由業(yè)務調(diào)用者線程執(zhí)行的鸳劳,也就是說釋放跟分配在同一個業(yè)務線程中進行。

初次排查并沒有發(fā)現(xiàn)導致內(nèi)存泄露的根因也搓,一籌莫展之際開始查看Netty的內(nèi)存池分配器PooledByteBufAllocator的Doc和源碼實現(xiàn)赏廓,發(fā)現(xiàn)內(nèi)存池實際是基于線程上下文實現(xiàn)的,相關代碼如下:

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);
        }

也就是說內(nèi)存的申請和釋放必須在同一線程上下文中傍妒,不能跨線程幔摸。跨線程之后實際操作的就不是同一塊內(nèi)存區(qū)域颤练,這會導致很多嚴重的問題既忆,內(nèi)存泄露便是其中之一。內(nèi)存在A線程申請嗦玖,切換到B線程釋放患雇,實際是無法正確回收的。

通過對Netty內(nèi)存池的源碼分析宇挫,問題基本鎖定苛吱。保險起見進行簡單驗證,通過對單條業(yè)務消息進行Debug器瘪,發(fā)現(xiàn)執(zhí)行釋放的果然不是業(yè)務線程又谋,而是Netty的NioEventLoop線程:當某個消息被完全發(fā)送成功之后,會通過ReferenceCountUtil.release(message)方法釋放已經(jīng)發(fā)送成功的ByteBuf娱局。

問題定位出來之后彰亥,繼續(xù)溯源,發(fā)現(xiàn)Netty 4修改了Netty 3的線程模型:在Netty 3的時候衰齐,upstream是在I/O線程里執(zhí)行的任斋,而downstream是在業(yè)務線程里執(zhí)行。當Netty從網(wǎng)絡讀取一個數(shù)據(jù)報投遞給業(yè)務handler的時候,handler是在I/O線程里執(zhí)行废酷;而當我們在業(yè)務線程中調(diào)用write和writeAndFlush向網(wǎng)絡發(fā)送消息的時候,handler是在業(yè)務線程里執(zhí)行瘟檩,直到最后一個Header handler將消息寫入到發(fā)送隊列中,業(yè)務線程才返回澈蟆。

Netty4修改了這一模型墨辛,在Netty 4里inbound(對應Netty 3的upstream)和outbound(對應Netty 3的downstream)都是在NioEventLoop(I/O線程)中執(zhí)行。當我們在業(yè)務線程里通過ChannelHandlerContext.write發(fā)送消息的時候趴俘,Netty 4在將消息發(fā)送事件調(diào)度到ChannelPipeline的時候睹簇,首先將待發(fā)送的消息封裝成一個Task,然后放到NioEventLoop的任務隊列中寥闪,由NioEventLoop線程異步執(zhí)行太惠。后續(xù)所有handler的調(diào)度和執(zhí)行,包括消息的發(fā)送疲憋、I/O事件的通知凿渊,都由NioEventLoop線程負責處理。

下面我們分別通過對比Netty 3和Netty 4的消息接收和發(fā)送流程缚柳,來理解兩個版本線程模型的差異:

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版本新增的內(nèi)存池確實非常高效埃脏,但是如果使用不當則會導致各種嚴重的問題。諸如內(nèi)存泄露這類問題秋忙,功能測試并沒有異常彩掐,如果相關接口沒有進行壓測或者穩(wěn)定性測試而直接上線,則會導致嚴重的線上問題翰绊。

內(nèi)存池PooledByteBuf的使用建議:

  1. 申請之后一定要記得釋放佩谷,Netty自身Socket讀取和發(fā)送的ByteBuf系統(tǒng)會自動釋放旁壮,用戶不需要做二次釋放监嗜;如果用戶使用Netty的內(nèi)存池在應用中做ByteBuf的對象池使用,則需要自己主動釋放抡谐;
  2. 避免錯誤的釋放:跨線程釋放裁奇、重復釋放等都是非法操作,要避免麦撵。特別是跨線程申請和釋放刽肠,往往具有隱蔽性,問題定位難度較大免胃;
  3. 防止隱式的申請和分配:之前曾經(jīng)發(fā)生過一個案例音五,為了解決內(nèi)存池跨線程申請和釋放問題,有用戶對內(nèi)存池做了二次包裝羔沙,以實現(xiàn)多線程操作時躺涝,內(nèi)存始終由包裝的管理線程申請和釋放,這樣可以屏蔽用戶業(yè)務線程模型和訪問方式的差異扼雏。誰知運行一段時間之后再次發(fā)生了內(nèi)存泄露坚嗜,最后發(fā)現(xiàn)原來調(diào)用ByteBuf的write操作時夯膀,如果內(nèi)存容量不足,會自動進行容量擴展苍蔬。擴展操作由業(yè)務線程執(zhí)行诱建,這就繞過了內(nèi)存池管理線程,發(fā)生了“引用逃逸”碟绑。該Bug只有在ByteBuf容量動態(tài)擴展的時候才發(fā)生俺猿,因此,上線很長一段時間沒有發(fā)生蜈敢,直到某一天......因此辜荠,大家在使用Netty 4.X的內(nèi)存池時要格外當心,特別是做二次封裝時抓狭,一定要對內(nèi)存池的實現(xiàn)細節(jié)有深刻的理解伯病。

3. Netty升級之后遭遇數(shù)據(jù)被篡改

3.1. 問題描述

某業(yè)務產(chǎn)品,Netty3.X升級到4.X之后否过,系統(tǒng)運行過程中午笛,偶現(xiàn)服務端發(fā)送給客戶端的應答數(shù)據(jù)被莫名“篡改”。

業(yè)務服務端的處理流程如下:

  1. 將解碼后的業(yè)務消息封裝成Task苗桂,投遞到后端的業(yè)務線程池中執(zhí)行药磺;
  2. 業(yè)務線程處理業(yè)務邏輯,完成之后構造應答消息發(fā)送給客戶端煤伟;
  3. 業(yè)務應答消息的編碼通過繼承Netty的CodeC框架實現(xiàn)癌佩,即Encoder ChannelHandler;
  4. 調(diào)用Netty的消息發(fā)送接口之后,流程繼續(xù)便锨,根據(jù)業(yè)務場景围辙,可能會繼續(xù)操作原發(fā)送的業(yè)務對象。

業(yè)務相關代碼示例如下:

//構造訂購應答消息
SubInfoResp infoResp = new SubInfoResp();
//根據(jù)業(yè)務邏輯放案,對應答消息賦值
infoResp.setResultCode(0);
infoResp.setXXX()姚建;
后續(xù)賦值操作省略......
//調(diào)用ChannelHandlerContext進行消息發(fā)送
ctx.writeAndFlush(infoResp);
//消息發(fā)送完成之后,后續(xù)根據(jù)業(yè)務流程進行分支處理吱殉,修改infoResp對象
infoResp.setXXX();
后續(xù)代碼省略......

3.2. 問題定位

首先對應答消息被非法“篡改”的原因進行分析掸冤,經(jīng)過定位發(fā)現(xiàn)當發(fā)生問題時,被“篡改”的內(nèi)容是調(diào)用writeAndFlush接口之后友雳,由后續(xù)業(yè)務分支代碼修改應答消息導致的稿湿。由于修改操作發(fā)生在writeAndFlush操作之后,按照Netty 3.X的線程模型不應該出現(xiàn)該問題押赊。

在Netty3中饺藤,downstream是在業(yè)務線程里執(zhí)行的,也就是說對SubInfoResp的編碼操作是在業(yè)務線程中執(zhí)行的,當編碼后的ByteBuf對象被投遞到消息發(fā)送隊列之后策精,業(yè)務線程才會返回并繼續(xù)執(zhí)行后續(xù)的業(yè)務邏輯舰始,此時修改應答消息是不會改變已完成編碼的ByteBuf對象的,所以肯定不會出現(xiàn)應答消息被篡改的問題咽袜。

初步分析應該是由于線程模型發(fā)生變更導致的問題丸卷,隨后查驗了Netty 4的線程模型,果然發(fā)生了變化:當調(diào)用outbound向外發(fā)送消息的時候询刹,Netty會將發(fā)送事件封裝成Task谜嫉,投遞到NioEventLoop的任務隊列中異步執(zhí)行,相關代碼如下:

@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線程執(zhí)行,則調(diào)用寫操作蔽挠;否則住闯,執(zhí)行線程安全的寫操作,即將寫事件封裝成Task澳淑,放入到任務隊列中由Netty的I/O線程執(zhí)行比原,業(yè)務調(diào)用返回,流程繼續(xù)執(zhí)行杠巡。

通過源碼分析量窘,問題根源已經(jīng)很清楚:系統(tǒng)升級到Netty 4之后,線程模型發(fā)生變化氢拥,響應消息的編碼由NioEventLoop線程異步執(zhí)行蚌铜,業(yè)務線程返回。這時存在兩種可能:

  1. 如果編碼操作先于修改應答消息的業(yè)務邏輯執(zhí)行嫩海,則運行結果正確冬殃;
  2. 如果編碼操作在修改應答消息的業(yè)務邏輯之后執(zhí)行,則運行結果錯誤出革。

由于線程的執(zhí)行先后順序無法預測造壮,因此該問題隱藏的相當深渡讼。如果對Netty 4和Netty3的線程模型不了解骂束,就會掉入陷阱。

Netty 3版本業(yè)務邏輯沒有問題成箫,流程如下:

![image](http://upload-images.jianshu.io/upload_images/10406068-397da045fdd542a1.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

圖3-1 升級之前的業(yè)務流程線程模型

升級到Netty 4版本之后展箱,業(yè)務流程由于Netty線程模型的變更而發(fā)生改變,導致業(yè)務邏輯發(fā)生問題:

0205006.png

圖3-2 升級之后的業(yè)務處理流程發(fā)生改變

3.3. 問題總結

很多讀者在進行Netty 版本升級的時候蹬昌,只關注到了包路徑混驰、類和API的變更,并沒有注意到隱藏在背后的“暗箭”- 線程模型變更。

升級到Netty 4的用戶需要根據(jù)新的線程模型對已有的系統(tǒng)進行評估栖榨,重點需要關注outbound的ChannelHandler昆汹,如果它的正確性依賴于Netty 3的線程模型,則很可能在新的線程模型中出問題婴栽,可能是功能問題或者其它問題满粗。

4. Netty升級之后性能嚴重下降

4.1. 問題描述

相信很多Netty用戶都看過如下相關報告:

在Twitter,Netty 4 GC開銷降為五分之一:Netty 3使用Java對象表示I/O事件愚争,這樣簡單映皆,但會產(chǎn)生大量的垃圾,尤其是在我們這樣的規(guī)模下轰枝。Netty 4在新版本中對此做出了更改捅彻,取代生存周期短的事件對象,而以定義在生存周期長的通道對象上的方法處理I/O事件鞍陨。它還有一個使用池的專用緩沖區(qū)分配器步淹。

每當收到新信息或者用戶發(fā)送信息到遠程端,Netty 3均會創(chuàng)建一個新的堆緩沖區(qū)诚撵。這意味著贤旷,對應每一個新的緩沖區(qū),都會有一個new byte[capacity]砾脑。這些緩沖區(qū)會導致GC壓力幼驶,并消耗內(nèi)存帶寬:為了安全起見,新的字節(jié)數(shù)組分配時會用零填充韧衣,這會消耗內(nèi)存帶寬盅藻。然而,用零填充的數(shù)組很可能會再次用實際的數(shù)據(jù)填充畅铭,這又會消耗同樣的內(nèi)存帶寬氏淑。如果Java虛擬機(JVM)提供了創(chuàng)建新字節(jié)數(shù)組而又無需用零填充的方式,那么我們本來就可以將內(nèi)存帶寬消耗減少50%硕噩,但是目前沒有那樣一種方式假残。

在Netty 4中,代碼定義了粒度更細的API炉擅,用來處理不同的事件類型辉懒,而不是創(chuàng)建事件對象。它還實現(xiàn)了一個新緩沖池谍失,那是一個純Java版本的 jemalloc (Facebook也在用)】袅現(xiàn)在,Netty不會再因為用零填充緩沖區(qū)而浪費內(nèi)存帶寬了快鱼。

我們比較了兩個分別建立在Netty 3和4基礎上echo協(xié)議服務器颠印。(Echo非常簡單纲岭,這樣,任何垃圾的產(chǎn)生都是Netty的原因线罕,而不是協(xié)議的原因)止潮。我使它們服務于相同的分布式echo協(xié)議客戶端,來自這些客戶端的16384個并發(fā)連接重復發(fā)送256字節(jié)的隨機負載钞楼,幾乎使千兆以太網(wǎng)飽和沽翔。

根據(jù)測試結果,Netty 4:

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

正是看到了相關的Netty 4性能提升報告窿凤,很多用戶選擇了升級仅偎。事后一些用戶反饋Netty 4并沒有跟產(chǎn)品帶來預期的性能提升,有些甚至還發(fā)生了非常嚴重的性能下降雳殊,下面我們就以某業(yè)務產(chǎn)品的失敗升級經(jīng)歷為案例橘沥,詳細分析下導致性能下降的原因。

4.2. 問題定位

首先通過JMC等性能分析工具對性能熱點進行分析夯秃,示例如下(信息安全等原因座咆,只給出分析過程示例截圖):

0205007.png

圖4-1 JMC性能監(jiān)控分析

通過對熱點方法的分析,發(fā)現(xiàn)在消息發(fā)送過程中仓洼,有兩處熱點:

  1. 消息發(fā)送性能統(tǒng)計相關Handler;
  2. 編碼Handler介陶。

對使用Netty 3版本的業(yè)務產(chǎn)品進行性能對比測試,發(fā)現(xiàn)上述兩個Handler也是熱點方法色建。既然都是熱點哺呜,為啥切換到Netty4之后性能下降這么厲害呢?

通過方法的調(diào)用樹分析發(fā)現(xiàn)了兩個版本的差異:在Netty 3中箕戳,上述兩個熱點方法都是由業(yè)務線程負責執(zhí)行某残;而在Netty 4中,則是由NioEventLoop(I/O)線程執(zhí)行陵吸。對于某個鏈路玻墅,業(yè)務是擁有多個線程的線程池,而NioEventLoop只有一個壮虫,所以執(zhí)行效率更低澳厢,返回給客戶端的應答時延就大。時延增大之后囚似,自然導致系統(tǒng)并發(fā)量降低剩拢,性能下降。

找出問題根因之后谆构,針對Netty 4的線程模型對業(yè)務進行專項優(yōu)化裸扶,性能達到預期框都,遠超過了Netty 3老版本的性能搬素。

Netty 3的業(yè)務線程調(diào)度模型圖如下所示:充分利用了業(yè)務多線程并行編碼和Handler處理的優(yōu)勢呵晨,周期T內(nèi)可以處理N條業(yè)務消息。

0205008.png

圖4-2 Netty 3業(yè)務調(diào)度性能模型

切換到Netty 4之后熬尺,業(yè)務耗時Handler被I/O線程串行執(zhí)行摸屠,因此性能發(fā)生比較大的下降:

0205009.png

圖4-3 Netty 4業(yè)務調(diào)度性能模型

4.3. 問題總結

該問題的根因還是由于Netty 4的線程模型變更引起,線程模型變更之后粱哼,不僅影響業(yè)務的功能季二,甚至對性能也會造成很大的影響。

對Netty的升級需要從功能揭措、兼容性和性能等多個角度進行綜合考慮胯舷,切不可只盯著API變更這個芝麻,而丟掉了性能這個西瓜绊含。API的變更會導致編譯錯誤桑嘶,但是性能下降卻隱藏于無形之中,稍不留意就會中招躬充。

對于講究快速交付逃顶、敏捷開發(fā)和灰度發(fā)布的互聯(lián)網(wǎng)應用,升級的時候更應該要當心充甚。

5. Netty升級之后上下文丟失

5.1. 問題描述

為了提升業(yè)務的二次定制能力以政,降低對接口的侵入性,業(yè)務使用線程變量進行消息上下文的傳遞伴找。例如消息發(fā)送源地址信息盈蛮、消息Id、會話Id等技矮。

業(yè)務同時使用到了一些第三方開源容器眉反,也提供了線程級變量上下文的能力。業(yè)務通過容器上下文獲取第三方容器的系統(tǒng)變量信息穆役。

升級到Netty 4之后寸五,業(yè)務繼承自Netty的ChannelHandler發(fā)生了空指針異常,無論是業(yè)務自定義的線程上下文耿币、還是第三方容器的線程上下文梳杏,都獲取不到傳遞的變量值。

5.2. 問題定位

首先檢查代碼淹接,看業(yè)務是否傳遞了相關變量十性,確認業(yè)務傳遞之后懷疑跟Netty 版本升級相關,調(diào)試發(fā)現(xiàn),業(yè)務ChannelHandler獲取的線程上下文對象和之前業(yè)務傳遞的上下文不是同一個擅这。這就說明執(zhí)行ChannelHandler的線程跟處理業(yè)務的線程不是同一個線程狰住!

查看Netty 4線程模型的相關Doc發(fā)現(xiàn),Netty修改了outbound的線程模型霞势,正好影響了業(yè)務消息發(fā)送時的線程上下文傳遞烹植,最終導致線程變量丟失。

5.3. 問題總結

通常業(yè)務的線程模型有如下幾種:

  1. 業(yè)務自定義線程池/線程組處理業(yè)務愕贡,例如使用JDK 1.5提供的ExecutorService草雕;
  2. 使用J2EE Web容器自帶的線程模型,常見的如JBoss和Tomcat的HTTP接入線程等固以;
  3. 隱式的使用其它第三方框架的線程模型墩虹,例如使用NIO框架進行協(xié)議處理,業(yè)務代碼隱式使用的就是NIO框架的線程模型憨琳,除非業(yè)務明確的實現(xiàn)自定義線程模型诫钓。

在實踐中我們發(fā)現(xiàn)很多業(yè)務使用了第三方框架,但是只熟悉API和功能篙螟,對線程模型并不清楚尖坤。某個類庫由哪個線程調(diào)用,糊里糊涂闲擦。為了方便變量傳遞慢味,又隨意的使用線程變量,實際對背后第三方類庫的線程模型產(chǎn)生了強依賴墅冷。當容器或者第三方類庫升級之后纯路,如果線程模型發(fā)生了變更,則原有功能就會發(fā)生問題寞忿。

鑒于此驰唬,在實際工作中,盡量不要強依賴第三方類庫的線程模型腔彰,如果確實無法避免叫编,則必須對它的線程模型有深入和清晰的了解。當?shù)谌筋悗焐壷笈祝枰獧z查線程模型是否發(fā)生變更搓逾,如果發(fā)生變化,相關的代碼也需要考慮同步升級杯拐。

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

通過對三個具有典型性的升級失敗案例進行分析和總結霞篡,我們發(fā)現(xiàn)有個共性:都是線程模型改變?nèi)堑牡?

下面小節(jié)我們就詳細得對Netty3和Netty4版本的I/O線程模型進行對比,以方便大家掌握兩者的差異端逼,在升級和使用中盡量少踩雷朗兵。

6.1 Netty 3.X 版本線程模型

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

  1. Inbound:主要包括鏈路建立事件顶滩、鏈路激活事件余掖、讀事件、I/O異常事件礁鲁、鏈路關閉事件等盐欺;
  2. Outbound:主要包括寫事件赁豆、連接事件、監(jiān)聽綁定事件找田、刷新事件等歌憨。

我們首先分析下Inbound操作的線程模型:

0205010.png

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

從上圖可以看出着憨,Inbound操作的主要處理流程如下:

  1. I/O線程(Work線程)將消息從TCP緩沖區(qū)讀取到SocketChannel的接收緩沖區(qū)中墩衙;
  2. 由I/O線程負責生成相應的事件,觸發(fā)事件向上執(zhí)行甲抖,調(diào)度到ChannelPipeline中漆改;
  3. I/O線程調(diào)度執(zhí)行ChannelPipeline中Handler鏈的對應方法,直到業(yè)務實現(xiàn)的Last Handler;
  4. Last Handler將消息封裝成Runnable准谚,放入到業(yè)務線程池中執(zhí)行挫剑,I/O線程返回,繼續(xù)讀/寫等I/O操作柱衔;
  5. 業(yè)務線程池從任務隊列中彈出消息樊破,并發(fā)執(zhí)行業(yè)務邏輯。

通過對Netty 3的Inbound操作進行分析我們可以看出唆铐,Inbound的Handler都是由Netty的I/O Work線程負責執(zhí)行哲戚。

下面我們繼續(xù)分析Outbound操作的線程模型:

0205011.png

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

從上圖可以看出,Outbound操作的主要處理流程如下:

業(yè)務線程發(fā)起Channel Write操作艾岂,發(fā)送消息顺少;

  1. Netty將寫操作封裝成寫事件,觸發(fā)事件向下傳播王浴;
  2. 寫事件被調(diào)度到ChannelPipeline中脆炎,由業(yè)務線程按照Handler Chain串行調(diào)用支持Downstream事件的Channel Handler;
  3. 執(zhí)行到系統(tǒng)最后一個ChannelHandler,將編碼后的消息Push到發(fā)送隊列中氓辣,業(yè)務線程返回秒裕;
  4. Netty的I/O線程從發(fā)送消息隊列中取出消息,調(diào)用SocketChannel的write方法進行消息發(fā)送钞啸。

6.2 Netty 4.X 版本線程模型

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

0205012.png

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

從上圖可以看出爽撒,Outbound操作的主要處理流程如下:

  1. I/O線程NioEventLoop從SocketChannel中讀取數(shù)據(jù)報入蛆,將ByteBuf投遞到ChannelPipeline,觸發(fā)ChannelRead事件硕勿;
  2. I/O線程NioEventLoop調(diào)用ChannelHandler鏈哨毁,直到將消息投遞到業(yè)務線程,然后I/O線程返回源武,繼續(xù)后續(xù)的讀寫操作扼褪;
  3. 業(yè)務線程調(diào)用ChannelHandlerContext.write(Object msg)方法進行消息發(fā)送想幻;
  4. 如果是由業(yè)務線程發(fā)起的寫操作,ChannelHandlerInvoker將發(fā)送消息封裝成Task话浇,放入到I/O線程NioEventLoop的任務隊列中脏毯,由NioEventLoop在循環(huán)中統(tǒng)一調(diào)度和執(zhí)行。放入任務隊列之后幔崖,業(yè)務線程返回食店;
  5. I/O線程NioEventLoop調(diào)用ChannelHandler鏈,進行消息發(fā)送赏寇,處理Outbound事件吉嫩,直到將消息放入發(fā)送隊列,然后喚醒Selector嗅定,進而執(zhí)行寫操作自娩。

通過流程分析,我們發(fā)現(xiàn)Netty 4修改了線程模型渠退,無論是Inbound還是Outbound操作忙迁,統(tǒng)一由I/O線程NioEventLoop調(diào)度執(zhí)行。

6.3. 線程模型對比

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

我們知道當系統(tǒng)在運行過程中姊扔,如果頻繁的進行線程上下文切換,會帶來額外的性能損耗荠锭。多線程并發(fā)執(zhí)行某個業(yè)務流程旱眯,業(yè)務開發(fā)者還需要時刻對線程安全保持警惕,哪些數(shù)據(jù)可能會被并發(fā)修改证九,如何保護删豺?這不僅降低了開發(fā)效率,也會帶來額外的性能損耗愧怜。

為了解決上述問題呀页,Netty 4采用了串行化設計理念,從消息的讀取拥坛、編碼以及后續(xù)Handler的執(zhí)行蓬蝶,始終都由I/O線程NioEventLoop負責,這就意外著整個流程不會進行線程上下文的切換猜惋,數(shù)據(jù)也不會面臨被并發(fā)修改的風險丸氛,對于用戶而言,甚至不需要了解Netty的線程細節(jié)著摔,這確實是個非常好的設計理念缓窜,它的工作原理圖如下:

0205014.png

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

一個NioEventLoop聚合了一個多路復用器Selector,因此可以處理成百上千的客戶端連接,Netty的處理策略是每當有一個新的客戶端接入禾锤,則從NioEventLoop線程組中順序獲取一個可用的NioEventLoop私股,當?shù)竭_數(shù)組上限之后,重新返回到0恩掷,通過這種方式倡鲸,可以基本保證各個NioEventLoop的負載均衡。一個客戶端連接只注冊到一個NioEventLoop上黄娘,這樣就避免了多個I/O線程去并發(fā)操作它峭状。

Netty通過串行化設計理念降低了用戶的開發(fā)難度,提升了處理性能寸宏。利用線程組實現(xiàn)了多個串行化線程水平并行執(zhí)行宁炫,線程之間并沒有交集偿曙,這樣既可以充分利用多核提升并行處理能力氮凝,同時避免了線程上下文的切換和并發(fā)保護帶來的額外性能損耗。

了解完了Netty 4的串行化設計理念之后望忆,我們繼續(xù)看Netty 3線程模型存在的問題罩阵,總結起來,它的主要問題如下:

  1. Inbound和Outbound實質(zhì)都是I/O相關的操作启摄,它們的線程模型竟然不統(tǒng)一稿壁,這給用戶帶來了更多的學習和使用成本;
  2. Outbound操作由業(yè)務線程執(zhí)行歉备,通常業(yè)務會使用線程池并行處理業(yè)務消息傅是,這就意味著在某一個時刻會有多個業(yè)務線程同時操作ChannelHandler,我們需要對ChannelHandler進行并發(fā)保護蕾羊,通常需要加鎖喧笔。如果同步塊的范圍不當,可能會導致嚴重的性能瓶頸龟再,這對開發(fā)者的技能要求非常高书闸,降低了開發(fā)效率;
  3. Outbound操作過程中利凑,例如消息編碼異常浆劲,會產(chǎn)生Exception,它會被轉換成Inbound的Exception并通知到ChannelPipeline哀澈,這就意味著業(yè)務線程發(fā)起了Inbound操作牌借!它打破了Inbound操作由I/O線程操作的模型,如果開發(fā)者按照Inbound操作只會由一個I/O線程執(zhí)行的約束進行設計割按,則會發(fā)生線程并發(fā)訪問安全問題膨报。由于該場景只在特定異常時發(fā)生,因此錯誤非常隱蔽!一旦在生產(chǎn)環(huán)境中發(fā)生此類線程并發(fā)問題丙躏,定位難度和成本都非常大择示。

講了這么多,似乎Netty 4 完勝 Netty 3的線程模型晒旅,其實并不盡然栅盲。在特定的場景下,Netty 3的性能可能更高废恋,就如本文第4章節(jié)所講谈秫,如果編碼和其它Outbound操作非常耗時,由多個業(yè)務線程并發(fā)執(zhí)行鱼鼓,性能肯定高于單個NioEventLoop線程拟烫。

但是,這種性能優(yōu)勢不是不可逆轉的迄本,如果我們修改業(yè)務代碼硕淑,將耗時的Handler操作前置,Outbound操作不做復雜業(yè)務邏輯處理嘉赎,性能同樣不輸于Netty 3置媳,但是考慮內(nèi)存池優(yōu)化、不會反復創(chuàng)建Event公条、不需要對Handler加鎖等Netty 4的優(yōu)化拇囊,整體性能Netty 4版本肯定會更高。

總而言之靶橱,如果用戶真正熟悉并掌握了Netty 4的線程模型和功能類庫寥袭,相信不僅僅開發(fā)會更加簡單,性能也會更優(yōu)关霸!

6.4. 思考

就Netty 而言传黄,掌握線程模型的重要性不亞于熟悉它的API和功能。很多時候我遇到的功能谒拴、性能等問題尝江,都是由于缺乏對它線程模型和原理的理解導致的,結果我們就以訛傳訛英上,認為Netty 4版本不如3好用等炭序。

不能說所有開源軟件的版本升級一定都勝過老版本,就Netty而言苍日,我認為Netty 4版本相比于老的Netty 3惭聂,確實是歷史的一大進步。

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末相恃,一起剝皮案震驚了整個濱河市辜纲,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖耕腾,帶你破解...
    沈念sama閱讀 206,723評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件见剩,死亡現(xiàn)場離奇詭異,居然都是意外死亡扫俺,警方通過查閱死者的電腦和手機苍苞,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,485評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來狼纬,“玉大人羹呵,你說我怎么就攤上這事×屏穑” “怎么了冈欢?”我有些...
    開封第一講書人閱讀 152,998評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長盈简。 經(jīng)常有香客問我凑耻,道長,這世上最難降的妖魔是什么送火? 我笑而不...
    開封第一講書人閱讀 55,323評論 1 279
  • 正文 為了忘掉前任拳话,我火速辦了婚禮先匪,結果婚禮上种吸,老公的妹妹穿的比我還像新娘。我一直安慰自己呀非,他們只是感情好坚俗,可當我...
    茶點故事閱讀 64,355評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著岸裙,像睡著了一般猖败。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上降允,一...
    開封第一講書人閱讀 49,079評論 1 285
  • 那天恩闻,我揣著相機與錄音,去河邊找鬼剧董。 笑死幢尚,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的翅楼。 我是一名探鬼主播尉剩,決...
    沈念sama閱讀 38,389評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼毅臊!你這毒婦竟也來了理茎?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 37,019評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎皂林,沒想到半個月后朗鸠,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,519評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡础倍,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,971評論 2 325
  • 正文 我和宋清朗相戀三年童社,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片著隆。...
    茶點故事閱讀 38,100評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡扰楼,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出美浦,到底是詐尸還是另有隱情弦赖,我是刑警寧澤,帶...
    沈念sama閱讀 33,738評論 4 324
  • 正文 年R本政府宣布浦辨,位于F島的核電站蹬竖,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏流酬。R本人自食惡果不足惜币厕,卻給世界環(huán)境...
    茶點故事閱讀 39,293評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望芽腾。 院中可真熱鬧旦装,春花似錦、人聲如沸摊滔。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,289評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽艰躺。三九已至呻袭,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間腺兴,已是汗流浹背左电。 一陣腳步聲響...
    開封第一講書人閱讀 31,517評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留页响,地道東北人篓足。 一個月前我還...
    沈念sama閱讀 45,547評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像拘泞,于是被迫代替她去往敵國和親纷纫。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,834評論 2 345

推薦閱讀更多精彩內(nèi)容

  • 前奏 https://tech.meituan.com/2016/11/04/nio.html 綜述 netty通...
    jiangmo閱讀 5,842評論 0 13
  • 何為Reactor線程模型陪腌? Reactor模式是事件驅(qū)動的辱魁,有一個或多個并發(fā)輸入源烟瞧,有一個Service Han...
    未名枯草閱讀 3,483評論 2 11
  • 零、寫在前面 本文雖然是講Netty染簇,但實際更關注的是Netty中的NIO的實現(xiàn)参滴,所以對于Netty中的OIO(O...
    TheAlchemist閱讀 3,284評論 1 34
  • 今天和 cc py pyf 出去玩了一天 超級開心 飯?zhí)貏e好吃 py 起晚了遲到 遭到了炮轟 寶寶去太早了 找不到...
    畫檐聲閱讀 218評論 0 0
  • 《大學豬》 今天給大家講個發(fā)生在動物王國中的故事。 說在2015年的最新一期動物大學的招生里豬獲得了準許入學的資格...
    啞筆閱讀 443評論 0 2