netty4 bytebuf zero-copy

原文

大神李林锋 Netty高性能之道:http://www.infoq.com/cn/articles/netty-high-performance/

1、前言

程序员喜欢说一句话:「不要重复造轮子」,但是程序员又不太会践行这句话。这倒也不是坏事,程序员一般而言看他人代码都不会太爽,这也可能是导致程序员的世界有各式各样的轮子的原因吧。

2、ByteBuf与Java NIO Buffer

ByteBuf则是Java NIO Buffer的新轮子,官方列出了一些ByteBuf的特性:

  •  需要的话,可以自定义buffer类型;
  •  通过组合buffer类型,可实现透明的zero-copy;
  •  提供动态的buffer类型,如StringBuffer一样,容量是按需扩展;
  •  无需调用flip()方法;
  •  常常「often」比ByteBuffer快。

 参考地址: Rich Buffer Data Structure

3、ByteBuf实现类

ByteBuf提供了一些较为丰富的实现类,逻辑上主要分为两种:HeapByteBuf和DirectByteBuf,实现机制则分为两 种:PooledByteBuf和UnpooledByteBuf,除了这些之外,Netty还实现了一些衍生 ByteBuf(DerivedByteBuf),如:ReadOnlyByteBuf、DuplicatedByteBuf以及 SlicedByteBuf。

ByteBuf实现类的类图如下:

HeapByteBuf和DirectByteBuf区别在于Buffer的管理方式:HeapByteBuf由Heap管理,Heap是 Java堆的意思,内部实现直接采用byte[] array;DirectByteBuf使用是堆外内存,Direct应是采用Direct I/O之意,内部实现使用java.nio.DirectByteBuffoer。

缓存IO/标准IO:操作系统会将 I/O 的数据缓存在文件系统的页缓存( page cache )中,也就是说,数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间
优点:缓存 I/O 使用了操作系统内核缓冲区,在一定程度上分离了应用程序空间和实际的物理设备;存 I/O 可以减少读盘的次数,从而提高性能。
直接IO:直接应用程序和socket交互,没有操作系统内核的参与,减少cope的次数。
优点:就是通过减少操作系统内核缓冲区和应用程序地址空间的数据拷贝次数,降低了对文件读取和写入时所带来的 CPU 的使用以及内存带宽的占用。这对于某些特殊的应用程序,比如自缓存应用程序来说,不失为一种好的选择。如果要传输的数据量很大,使用直接 I/O 的方式进行数据传输,而不需要操作系统内核地址空间拷贝数据操作的参与,这将会大大提高性能。

Direct I/O
DirectByteBuffer

PooledByteBuf和UnpooledByteBuf,UnpooledByteBuf实现就是普通的ByteBuf了,PooledByteBuf是4.x之后的新特性,稍后再说。

DerivedByteBuf是ByteBuf衍生类,实现采用装饰器模式对原有的ByteBuf进行了一些封装。 ReadOnlyByteBuf是某个ByteBuf的只读引用;DuplicatedByteBuf是某个ByteBuf对象的引 用;SlicedByteBuf是某个ByteBuf的部分内容。

SwappedByteBuf和CompositedByteBuf我觉得也算某种程度的衍生类吧,SwappedByteBuf封装了一个 ByteBuf对象和ByteOrder对象,实现某个ByteBuf对象序列的逆转;CompositedByteBuf内部实现了一个ByteBuf 列表,称之为组合ByteBuf,由于不懂相关的技术业务,无法理解该类的存在意义(官方解释:A user can save bulk memory copy operations using a composite buffer at the cost of relatively expensive random access.)。这两个类从逻辑上似乎完全可以继承于DerivedByteBuf,Trustin大神为啥如此设计呢?

4、简要的ByteBuf的实现机制

ByteBuf有两个指针,readerIndex和writerIndex,用以控制buffer数组的读写。读逻辑较为简单,不考虑边界的情况下,就是`return array[readerIndex++];`。这里简要分析一下HeapByteBuf的读逻辑。

 1. AbstractByteBuf.ensureWritable(minWritableBytes);

 2. calculateNewCapacity(writerIndex + minWritableBytes)

  > 2.1 判断是否超过可写入容量 maxCapacity – writerIndex

  > 2.2 超过则抛异常,否则计算新容量 writerIndex + minWritableBytes

  > 2.3 判断是否超过设定阈值(4MB),超过每次增加按阈值(4MB)递增,否则

  > 2.4 初始大小为64字节(newCapacity),新容量超过newCapacity则翻倍,直到newCapacity大于新容量为止

  > 2.5 返回Min(newCapacity, maxCapacity);

 3. UnpooledHeapByteBuf.capacity(newCapacity);

  > 3.1 确保可访问,有一个`引用计数`的机制,引用计数为0,则抛异常(ensureAccessible) 

  > 3.2 常规操作:判断是否越界

  > 3.3 如果newCapacity比原容量大,则直接创建新数组,并设置。否则

  > 3.4 如果readerIndex小于新容量,将readable bytes拷贝至新的数组,反之将readerIndex和writerIndex均设置为newCapacity。

 4. setByte(writerIndex++, value)

  > 4.1 确保可访问

  > 4.2 设置

5、ByteBuf特殊机制

5.1 Pooled

4.x开发了Pooled Buffer,实现了一个高性能的buffer池,分配策略则是结合了buddy allocation和slab allocation的jemalloc变种,代码在io.netty.buffer.PoolArena。暂未深入研读。

官方说提供了以下优势:

  • 频繁分配、释放buffer时减少了GC压力;
  • 在初始化新buffer时减少内存带宽消耗(初始化时不可避免的要给buffer数组赋初始值);
  • 及时的释放direct buffer。

当然,官方也说了不保证没有内存泄露,所以默认情况下还是采用的UnpooledByteBufAllocator。5.x还处于beta版, 看它的「 new and noteworthy 」文档也没说有啥变化,哈哈哈哈 ,查看最新的「 new and noteworthy 」文档,PooledByteBufAllocator已经设置为默认的Allocator (revised in 2014-01-16)。

5.2 Reference Count

ByteBuf的生命周期管理引入了Reference Count的机制,感觉让我回到了CPP时代。可以通过简单的继承SimpleChannelInboundHandler实现自动释放 reference count。SimpleChannelInboundHandler的事件方法如下,在消费完毕msg后,可以AutoRelease之:

public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {

        boolean release = true;

        try {

            if (acceptInboundMessage(msg)) {

  @SuppressWarnings("unchecked")

  I imsg = (I) msg;

  messageReceived(ctx, imsg);

            } else {

  release = false;

  ctx.fireChannelRead(msg);

            }

        } finally {

            if (autoRelease && release) {

  ReferenceCountUtil.release(msg);

            }

        }

    }

这一小节可以单独拎出来和Pooled放在一起深入研读研读,有兴趣的可以先看看官方文档: Reference counted objects

5.3 Zero Copy

Zero-copy与传统意义的 zero-copy 不太一样。传统的zero-copy是IO传输过程中,数据无需中内核态到用户态、用户态到内核态的数据拷贝,减少拷贝次数。而Netty的zero- copy则是完全在用户态,或者说传输层的zero-copy机制,可以参考下图。由于协议传输过程中,通常会有拆包、合并包的过程,一般的做法就是 System.arrayCopy了,但是Netty通过ByteBuf.slice以及Unpooled.wrappedBuffer等方法拆分、合并 Buffer无需拷贝数据。

如何实现zero-copy的呢。slice实现就是创建一个SlicedByteBuf对象,将this对象,以及相应的数据指针传入即可,wrappedBuffer实现机制类似。

零拷贝延伸阅读:

很多用户都听说过Netty具有“零拷贝”功能,但是具体体现在哪里又说不清楚,本小节就详细对Netty的“零拷贝”功能进行讲解。

Netty的“零拷贝”主要体现在如下三个方面:

1) Netty的接收和发送ByteBuffer采用DIRECT BUFFERS,使用堆外直接内存进行Socket读写,不需要进行字节缓冲区的二次拷贝。如果使用传统的堆内存(HEAP BUFFERS)进行Socket读写,JVM会将堆内存Buffer拷贝一份到直接内存中,然后才写入Socket中。相比于堆外直接内存,消息在发送 过程中多了一次缓冲区的内存拷贝。

2) Netty提供了组合Buffer对象,可以聚合多个ByteBuffer对象,用户可以像操作一个Buffer那样方便的对组合Buffer进行操作,避免了传统通过内存拷贝的方式将几个小Buffer合并成一个大的Buffer。

3) Netty的文件传输采用了transferTo方法,它可以直接将文件缓冲区的数据发送到目标Channel,避免了传统通过循环write方式导致的内存拷贝问题。

下面,我们对上述三种“零拷贝”进行说明,先看Netty 接收Buffer的创建:

图2-5 异步消息读取“零拷贝”

每循环读取一次消息,就通过ByteBufAllocator的ioBuffer方法获取ByteBuf对象,下面继续看它的接口定义:

图2-6 ByteBufAllocator 通过ioBuffer分配堆外内存

当进行Socket IO读写的时候,为了避免从堆内存拷贝一份副本到直接内存,Netty的ByteBuf分配器直接创建非堆内存避免缓冲区的二次拷贝,通过“零拷贝”来提升读写性能。

下面我们继续看第二种“零拷贝”的实现CompositeByteBuf,它对外将多个ByteBuf封装成一个ByteBuf,对外提供统一封装后的ByteBuf接口,它的类定义如下:

图2-7 CompositeByteBuf类继承关系

通过继承关系我们可以看出CompositeByteBuf实际就是个ByteBuf的包装器,它将多个ByteBuf组合成一个集合,然后对外提供统一的ByteBuf接口,相关定义如下:

图2-8 CompositeByteBuf类定义

添加ByteBuf,不需要做内存拷贝,相关代码如下:

图2-9 新增ByteBuf的“零拷贝”

最后,我们看下文件传输的“零拷贝”:

图2-10 文件传输“零拷贝”

Netty文件传输DefaultFileRegion通过transferTo方法将文件发送到目标Channel中,下面重点看FileChannel的transferTo方法,它的API DOC说明如下:

图2-11 文件传输 “零拷贝”

对于很多操作系统它直接将文件缓冲区的内容发送到目标Channel中,而不需要通过拷贝的方式,这是一种更加高效的传输方式,它实现了文件传输的“零拷贝”。

 参考地址: Combining and Slicing ChannelBuffers

posted @ 2015-03-26 12:11  xiongjianjun  阅读(1039)  评论(0编辑  收藏  举报