Netty-Mina深入學習與對比(二)

五柳-先生發表於2016-03-21

上文講了對netty-mina的執行緒模型以及任務排程粒度的理解,這篇則主要是講nio程式設計中的注意事項,netty-mina的對這些注意事項的實現方式的差異,以及業務層會如何處理這些注意事項。

1. 資料是如何write出去的

java nio如果是non-blocking的話,在每次write(bytes[N])的時候,並不會將N位元組全部write出去,每次write僅一部分(具體大小和tcp_write_buffer有關)。那麼,mina和netty是怎麼處理這種情況的呢?

1.1 程式碼

  • mina-1.1.7: SocketIoProcessor.doFlush
  • mina-2.0.4: AbstractPollingIoProcessor.flushNow
  • mina-3.0.0.M3-SNAPSHOT: AbstractNioSession.processWrite
  • netty-3.5.8.Final: AbstractNioWorker.write0
  • netty-4.0.6.Final: AbstractNioByteChannel.doWrite

1.2 分析

mina1、2,netty3的方式基本一致。 在傳送端每個session均有一個writeBufferQueue,有這樣一個佇列,可以保證寫入與寫出均有序。在真正write時,大致邏輯均是一一將佇列中的writeBuffer取出,寫入socket。但有一些不同的是,mina1是每次peek一次,當該buffer全部寫出之後再poll(mina3也是這種機制);而mina2、netty3則是直接poll第一個,將其存為currentWriteRequest,直到currentWriteRequest全部寫出之後,才會再poll下一個。這樣的做法是為了省幾次peek的時間麼?

同時mina、netty在write時,有一種spin write的機制,即迴圈write多次。mina1的spin write count為256,寫死在程式碼裡了,表示256有點大;mina2這個機制廢除但程式碼保留;netty3則可以配置,預設為16。netty在這裡略勝一籌!

netty4與netty3的機制差不多,但是netty4為這個事情特意寫了一個ChannelOutboundBuffer類,輸出佇列寫在了該類的flushed:Object[]成員中,但表示ChannelOutboundBuffer這個類的程式碼有點長,就暫不深究了。

2. 資料是如何read進來的

如第三段內容,每次write只是輸出了一部分資料,read同理,也有可能只會讀入部分資料,這樣就是導致讀入的資料是殘缺的。而mina和netty預設不會理會這種由於nio導致的資料分片,需要由業務層自己額外做配置或者處理。

2.1 程式碼

  • nfs-rpc: ProtocolUtils.decode
  • mina-1.1.7: SocketIoProcessor.read, CumulativeProtocolDecoder.decode
  • mina-2.0.4: AbstractPollingIoProcessor.read, CumulativeProtocolDecoder.decode
  • mina-3.0.0.M3-SNAPSHOT: NioSelectorLoop.readBuffer
  • netty-3.5.8.Final: NioWorker.read, FrameDecoder
  • netty-4.0.6.Fianl: AbstractNioByteChannel$NioByteUnsafe.read

2.2 業務層處理

nfs-rpc在協議反序列化的過程中,就會考慮這個的問題,依次讀入每個位元組,當發現當前位元組或者剩餘位元組數不夠時,會將buf的readerIndex設定為初始狀態。具體的實現,有興趣的同學可以學習nfs-rpc:ProtocolUtils.decode

nfs-rpc在decode時,出現錯誤就會將buf的readerIndex設為0,把readerIndex設定為0就必須要有個前提假設:每次decode時buf是同一個,即該buf是複用的。那麼,具體情況是怎樣呢?

2.3 框架層處理

我看讀mina與netty這塊的程式碼,發現主要演進與不同的點在兩個地方:讀buffer的建立與資料分片的處理方式。

mina:

mina1、2的讀buffer建立方式比較土,在每次read之前,會重新allocate一個新的buf物件,該buf物件的大小是根據讀入資料大小動態調整。當本次讀入資料等於該buf大小,下一次allocate的buf物件大小會翻倍;當本次讀入資料不足該buf大小的二分之一,下一次allocate的buf物件同樣會縮小至一半。需要注意的是,*2與/2的程式碼都可以用位運算,但是mina1竟沒用位運算,有意思。

mina1、2處理資料分片可以繼承CumulativeProtocolDecoder,該decoder會在session中存入(BUFFER, cumulativeBuffer)。decode過程為:1)先將message追加至cumulativeBuffer;2)呼叫具體的decode邏輯;3)判斷cumulativeBuffer.hasRemaining(),為true則壓縮cumulativeBuffer,為false則直接刪除(BUFFER, cumulativeBuffer)。實現業務的decode邏輯可以參考nfs-rpc中MinaProtocolDecoder的程式碼。

mina3在處理讀buffer的建立與資料分片比較巧妙,它所有的讀buffer共用一個buffer物件(預設64kb),每次均會將讀入的資料追加至該buffer中,這樣即省去了buffer的建立與銷燬事件,也省去了cumulativeDecoder的處理邏輯,讓程式碼很清爽啊!

netty:

netty3在讀buffer建立部分的程式碼還是挺有意思的,首先,它建立了一個SocketReceiveBufferAllocator的allocate物件,名字為recvBufferPool,但是裡面程式碼完全和pool扯不上關係;其次,它每次建立buffer也會動態修改初始大小的機制,它設計了232個大小檔位,最大值為Integer.MAX_VALUE,沒有具體考究,這種實現方式似乎比每次大小翻倍優雅一點,具體程式碼可以參考:AdaptiveReceiveBufferSizePredictor

對應mina的CumulativeProtocolDecoder類,在netty中則是FrameDecoder和ReplayingDecoder,沒深入只是大致掃了下程式碼,原理基本一致。BTW,ReplayingDecoder似乎挺強大的,有興趣的可以看看這兩篇:

High speed custom codecs with ReplayingDecoder
An enhanced version of ReplayingDecoder for Netty

netty4在讀buffer建立部分機制與netty3大同小異,不過由於netty有了ByteBufAllocator的概念,要想每次不重新建立銷燬buffer的話,可以採用PooledByteBufAllocator。

在處理分片上,netty4抽象出了Message這樣的概念,我的理解就是,一個Message就是業務可讀的資料,轉換Message的抽象類:ByteToMessageDecoder,當然也有netty3中的ReplayingDecoder,繼承自ByteToMessageDecoder,具體可以研究程式碼。

3. ByteBuffer設計的差異

3.1 自建buffer的原因

mina:

需要說明的是,只有mina1、2才有自己的buffer類,mina3內部只用nio的原生ByteBuffer類(提供了一個組合buffer的代理類-IoBuffer)。mina1、2自建buffer的原因如下:

  • It doesn’t provide useful getters and putters such as fill,get/putString, and get/putAsciiInt()enough.
  • It is difficult to write variable-length data due to its fixed capacity

第一條比較好理解,即提供了更為方便的方法用以操作buffer。第二條則是覺得nio的ByteBuffer是定長的,無法自動擴容或者縮容,所以提供了自動擴/縮容的方法:IoBuffer.setAutoExpand, IoBuffer.setAutoShrink。但是擴/縮容的實現,也是基於nio的ByteBuffer,重新ByteBuffer.allocate(capacity),再把原有的資料拷貝過去。

netty:

在我前面的博文(Netty 4.x學習筆記 – ByteBuf)我已經提到這些原因:

  • 需要的話,可以自定義buffer型別
  • 通過組合buffer型別,可實現透明的zero-copy
  • 提供動態的buffer型別,如StringBuffer一樣(擴容方式也是每次double),容量是按需擴充套件
  • 無需呼叫flip()方法
  • 常常「often」比ByteBuffer快

以上理由來自netty3的API文件:Package org.jboss.netty.buffer,netty4沒見到官方的說法,但是我覺得還得加上一個更為重要也是最為重要的理由,就是可以實現buffer池化管理。

3.2 實現的差異

mina:

mina的實現較為基礎,僅僅只是在ByteBuffer上的一些簡單封裝。

netty:

netty3與netty4的實現大致相同(ChannlBuffer -> ByteBuf),具體可以參見:Netty 4.x學習筆記 – ByteBuf,netty4實現了PooledByteBufAllocator,傳聞是可以大大減少GC的壓力,但是官方不保證沒有記憶體洩露,我自己壓測中也出現了記憶體洩露的警告,建議生產中謹慎使用該功能。

netty5.x有一個更為高階的buffer洩露跟蹤機制,PooledByteBufAllocator也已經預設開啟,有機會可以嘗試使用一下。

相關文章