百萬并發(fā)「零拷貝」技術系列之經典案例Netty

源 / 碼農神說 文 / 韓旭

Netty在零拷貝思想上的實現可以理解為是廣義的,它和wiki對零拷貝寬泛的定義特別吻合“CPU 不需要將數據從一塊內存拷貝到另一塊內存”,因為Netty主要是在用戶空間盡量減少內存的拷貝次數,而非系統(tǒng)層面的用戶空間和內核空間數據的拷貝。
Netty作為Java界知名的NIO網絡通訊框架,憑借其高性能木秀于mina、twisted,其因素之一就如官方所述:“減少了不必要的內存拷貝”。
在零拷貝實現上,它有借助于Java NIO的tranferTo實現的FileRegion用于文件傳輸,也有通過巧妙設計buffer數據結構來避免由于拆分、組合而帶來的拷貝。尤其是后者,因為buffer是用來化零為整降低I/O操作頻率的重要技術手段,對性能的影響至關重要。
FileRegion的零拷貝是體現在系統(tǒng)層面的,它包裝了Java NIO的FileChannel.tranferTo方法進行文件傳輸,從FileRegion的默認實現類DefaultFileRegion可以一探究竟。

tranferTo在上一篇推文有較詳細的講解,此處不再累述。
Netty使用了它自己封裝的buffer API替代了Java NIO的ByteBuffer:ByteBuf。官方列出了它的一些比較酷的特性
You can define your buffer type if necessary.根據需要可以定制自己的buffer類型。
Transparent zero copy is achieved by built-in composite buffer type.通過內建的組合類型可以實現透明的零拷貝。
A dynamic buffer type is provided out-of-the-box, whose capacity is expanded on demand, just like `StringBuffer`.它是一個開箱即用可根據需求動態(tài)擴展的buffer,就像`StringBuffer`。
There's no need to call the `flip()` method anymore.不再需要調用`flip()` 方法。
It is often faster than `ByteBuffer`.通常比`ByteBuffer`更快速。

拿網絡傳輸來說,對于傳統(tǒng)的read/write的I/O方式,一般情況而言是這樣的:Java堆內存—>用戶空間的堆外內存—>內核socket緩沖區(qū)—>DMA—>網卡—>網卡—>DMA—>內核socket緩沖區(qū)—>用戶空間的堆外內存—>Java堆內存。

DirectByteBuffer的使用是有一定的風險的,可能會造成OutOfMemory,官方是這樣描述的
allocating many short-lived direct NIO buffers often causes an OutOfMemoryError.
堆內和堆外內存各有優(yōu)勢和劣勢,需要根據場景自行選擇

網絡傳輸的過程中對數據的拆包、組包等操作十分常見也很頻繁,Netty提供了warp、Composite和slice方法來減少數據的拷貝,達到性能的提升的目標。

通常將一個對象比如byte數組轉換成一個ByteBuffer,傳統(tǒng)的作法是把數組拷貝到ByteBuffer對象中,而wrap的方式無須拷貝,它們共用同一塊內存。
byte[] tmp=new byte[]{1,2};
//Java NIO
//傳統(tǒng)方式(拷貝)
ByteBuffer byteBuffer=ByteBuffer.allocate(2);
byteBuffer.put(tmp);
//wrap方式
byteBuffer=ByteBuffer.wrap(tmp);
//Netty ByteBuf
//傳統(tǒng)方式(拷貝)
ByteBuf byteBuf = Unpooled.buffer();
byteBuf.writeBytes(tmp);
//wrap方式
byteBuf=Unpooled.wrappedBuffer(tmp);
CompositeByteBuf將多個ByteBuf組合成一個ByteBuf而不需要數據拷貝,每個ByteBuf都是獨立存在的,只是在邏輯上的組合,提高性能的同時可以統(tǒng)一使用ByteBuf的API。假設有一個數據包是由三部分組成header、body、footer,它們可能是由不同的模塊創(chuàng)建的,組合示意和代碼如下

ByteBuf header = Unpooled.wrappedBuffer(tmp);
ByteBuf body = Unpooled.wrappedBuffer(tmp);
ByteBuf footer = Unpooled.wrappedBuffer(tmp);
//不建議的作法
ByteBuf wholeBuf = Unpooled.buffer(header.readableBytes()
+ body.readableBytes()+footer.readableBytes());
wholeBuf.writeBytes(header);
wholeBuf.writeBytes(body);
wholeBuf.writeBytes(footer);
//建議使用組合
CompositeByteBuf compositeByteBuf=Unpooled.compositeBuffer();
//第一個參數increaseWriterIndex,為true會自動增加writerIndexss
compositeByteBuf.addComponents(true,header,body,footer);
slice將一個ByteBuf分解為多個ByteBuf,但沒有數據拷貝,而是共享同一個存儲區(qū)域的,這在拆分包操作時非常有用,如上例數據包由header、body、footer三部分組成,拆分示意和代碼如下

ByteBuf byteBuf4slice=.....;
ByteBuf header=byteBuf4slice.slice(0,5);
ByteBuf body=byteBuf4slice.slice(5,15);
ByteBuf footer=byteBuf4slice.slice(15,20);
Netty 4.x提供了池化的Buffer,類似于線程池或數據庫連接池的思想,避免了Buffer頻繁的創(chuàng)建和釋放帶來的性能低效及GC壓力。池化和非池化的性能對比如下
int loop = 3000000;
byte[] content="this is a test".getBytes();
//池化buffer
long startTime = System.currentTimeMillis();
ByteBuf pooledBuf = null;
for (int i = 0; i < loop; i++) {
pooledBuf= PooledByteBufAllocator.DEFAULT.buffer(1024);
pooledBuf.writeBytes(content);
pooledBuf.release();
}
long pooledTime=System.currentTimeMillis()-startTime;
System.out.println("3百萬次池化buffer消耗的時間:"+pooledTime);
//非池化buffer
startTime = System.currentTimeMillis();
ByteBuf unPooledBuf = null;
for (int i = 0; i < loop; i++) {
unPooledBuf= Unpooled.buffer(1024);
unPooledBuf.writeBytes(content);
unPooledBuf.release();
}
long unPooledTime=System.currentTimeMillis()-startTime;
System.out.println("3百萬次池化buffer消耗的時間:"+unPooledTime);
//性能提升
System.out.println("池化buffer性能提升:"+Double.valueOf(
String.format("%.2f",(unPooledTime-pooledTime)/(double)unPooledTime))*100
+"%");
執(zhí)行后從輸出可見,池化后的buffer性能提升20%左右,非常可觀
3百萬次池化buffer消耗的時間:766
3百萬次池化buffer消耗的時間:989
池化buffer性能提升:23.0%
— 完 —
一鍵三連「分享」、「點贊」和「在看」
技術干貨與你天天見~
