Netty 中的 ByteBuf

    科技2022-07-16  114

    所有的缓冲区,都是为了平衡了 数据产生方 和 数据消费方 的处理效率差异。

    ByteBuf 基础 API

    本段内容出自 ByteBuf 类中的 Java Doc

    ByteBuf 内存结构:

    +-------------------+------------------+------------------+ | discardable bytes | readable bytes | writable bytes | | | (CONTENT) | | +-------------------+------------------+------------------+ | | | | 0 <= readerIndex <= writerIndex <= capacity

    名称解释:

    discardable bytes:已经被读取过的数据,一般情况下可以理解为无效区域。readable bytes:未读取数据,readable bytes数据区的数据是满的,都是等待读取的数据。writable bytes:空闲区域,可以往这块区域写数据。capacity:表示当前 ByteBuf 容量。readerIndex:读数据起点指针,当需要读数据时,就以当前指针为起点往后读取数据。writerIndex:写数据起点指针,当需要写数据时,就以当前指针为起点往后写数据。

    调用 byteBuf.discardReadBytes() 回收已读的空间:

    BEFORE discardReadBytes() +-------------------+------------------+------------------+ | discardable bytes | readable bytes | writable bytes | +-------------------+------------------+------------------+ | | | | 0 <= readerIndex <= writerIndex <= capacity AFTER discardReadBytes() +------------------+--------------------------------------+ | readable bytes | writable bytes (got more space) | +------------------+--------------------------------------+ | | | readerIndex (0) <= writerIndex (decreased) <= capacity

      移动 readable 空间到 bytebuf 的起始位置,bytebuf总容量不变,可写的容量增加。一般不调用该方法来复制缓冲区的内容。

    @Override public ByteBuf discardReadBytes() { if (readerIndex == 0) { ensureAccessible(); return this; } if (readerIndex != writerIndex) { setBytes(0, this, readerIndex, writerIndex - readerIndex);//复制readable bytes区域内容到bytebuf的最前面 writerIndex -= readerIndex; adjustMarkers(readerIndex); readerIndex = 0; } else { ensureAccessible(); adjustMarkers(readerIndex); writerIndex = readerIndex = 0; } return this; }

    调用 byteBuf.clear() 重置 readerIndex 和 writerIndex:

    BEFORE clear() +-------------------+------------------+------------------+ | discardable bytes | readable bytes | writable bytes | +-------------------+------------------+------------------+ | | | | 0 <= readerIndex <= writerIndex <= capacity AFTER clear() +---------------------------------------------------------+ | writable bytes (got more space) | +---------------------------------------------------------+ | | 0 = readerIndex = writerIndex <= capacity

      它不清除缓冲区内容,而只是清除两个指针的值。

    派生缓冲区

    duplicate() slice() slice(int, int) readSlice(int) retainedDuplicate() retainedSlice() retainedSlice(int, int) readRetainedSlice(int)

      以上方法可以在已有的一个 byteBuf 上创建一个视图,共享原来 ByteBuf 的数据,但拥有独立的 readerIndex 等标记。   duplicate() 与 copy() 不同,copy() 将复制一个全新的缓冲区。

    用例:

    //中文在utf-8字符集中共占3字节 ByteBuf copiedBuffer = Unpooled.copiedBuffer("hi word中", Charset.forName("utf-8")); ByteBuf byteBuf2 = UnpooledByteBufAllocator.DEFAULT.directBuffer(); byteBuf2.writeByte(2); //hasArray()判断使用的是direct buffer还是heap buffer。heap buffer底层使用的是字节数组,所以会返回true System.out.println("copiedBuffer.hasArray=" + copiedBuffer.hasArray()+" byteBuf2.hasArray=" + byteBuf2.hasArray());//copiedBuffer.hasArray()=true byteBuf2.hasArray=false System.out.println("copiedBuffer.capacity="+copiedBuffer.capacity()+" byteBuf2.capacity="+byteBuf2.capacity());//copiedBuffer.capacity=24 byteBuf2.capacity=256 System.out.println(copiedBuffer.getCharSequence(5, 9, Charset.forName("utf-8")));//rd中

    三种类型的 ByteBuf

    Heap Buffer(堆缓冲区)

      将 ByteBuf 的数据用 byte array 的方式存储在 JVM 的堆空间中。

    优点: 可以利用 JVM 提供的 GC 机制,快速的创建、释放对应的 byte array ,并且提供了直接访问内部字节数组的方法。

    缺点: 程序向外部写出数据时,需要先从 用户空间(即堆中) 拷贝数据到 内核空间(即直接内存),再拷贝数据到网卡、磁盘等。不清楚的可以看下操作系统数据拷贝的过程

    Direct Buffer(直接缓冲区)

      使用的是直接内存(JVM参数 -XX:MaxDirectMemorySize=xxM),由操作系统在本地内存进行数据分配。

    优点: 由于数据存在于直接内存,避免了从用户空间拷贝数据到内核空间的步骤,可提高系统的数据吞吐量。

    缺点:

    直接缓冲区不支持通过字节数组的方式来访问数据。使用直接内存时,空间的分配与释放比堆空间更加复杂,所以处理的速度要慢一些。为了解决这个问题,Netty 使用了内存池。

    以上两者如何选用:

      业务消息的编解码中(即各个 handler 中消息的传递),推荐使用 HeapBuf 类型;对于 I/O 通讯线程在读写数据时,推荐使用 DirectBuf 类型,达到零拷贝的效果。

    Composite Buffer(复合缓冲区)

      Composite Buffer 中将多个 Heap Buffer 或 Direct Buffer 合并为一个逻辑上的 ByteBuf ,形成一个视图,避免了各个 ByteBuf 在组合时的拷贝。

    CompositeByteBuf 与 ByteBuf 组合使用:

    //中文在utf-8字符集中共占3字节 ByteBuf copiedBuffer = Unpooled.copiedBuffer("hi word中", Charset.forName("utf-8")); ByteBuf byteBuf2 = UnpooledByteBufAllocator.DEFAULT.directBuffer(); byteBuf2.writeByte(2); CompositeByteBuf compositeByteBuf = Unpooled.compositeBuffer(); System.out.println("init compositeByteBuf.hasArray() " + compositeByteBuf.hasArray());//true compositeByteBuf.addComponents(true, byteBuf2, copiedBuffer); // compositeByteBuf.addComponent(byteBuf2);//两步操作和addComponents效果一样 // compositeByteBuf.addComponent(copiedBuffer); System.out.println(compositeByteBuf.getByte(0));//2 System.out.println("compositeByteBuf.hasArray() " + compositeByteBuf.hasArray());//false

    ByteBuf 的改进

      Netty 的 ByteBuf ,使用了外观模式,结合 零拷贝、内存池加速、读写索引 对 JDK 的 ByteBuffer 进行了优化。

    ByteBuffer 与 ByteBuf 优缺点

    JDK 中 ByteBuffer 的缺点:

    用作存储数据的 byte array 被 final 修饰,一旦被初始化后就不能修改,没有自动扩容功能,需要开发者自主完成。当存入的数据超过初始化 byte array 的大小时,将抛出 BufferOverflowException 。

    内部只使用了一个 position ,在进行读写切换时,需要调用 flip 或 rewind

    Netty 中 ByteBuf 的改进:

    Heap Buffe 类型的 ByteBuf 底层使用的是数组,在写入数据的方法中,封装了自动扩容的操作。ByteBuf 使用了读写索引,使用时较 ByteBuffer 更加方便。

    写入数据时的扩容源码:

    io.netty.buffer.AbstractByteBuf: @Override public ByteBuf writeByte(int value) { ensureWritable0(1); _setByte(writerIndex++, value); return this; } ... ... final void ensureWritable0(int minWritableBytes) { final int writerIndex = writerIndex(); final int targetCapacity = writerIndex + minWritableBytes; if (targetCapacity <= capacity()) { ensureAccessible(); return; } if (checkBounds && targetCapacity > maxCapacity) { ensureAccessible(); throw new IndexOutOfBoundsException(String.format( "writerIndex(%d) + minWritableBytes(%d) exceeds maxCapacity(%d): %s", writerIndex, minWritableBytes, maxCapacity, this)); } // Normalize the target capacity to the power of 2. final int fastWritable = maxFastWritableBytes(); int newCapacity = fastWritable >= minWritableBytes ? writerIndex + fastWritable : alloc().calculateNewCapacity(targetCapacity, maxCapacity);//计算得到新的数组容量 // Adjust to the new capacity. capacity(newCapacity);//拷贝原来的 byte array 内容到新的、容量更大的 byte array 中去 }

    CompositeByteBuf

    内存池

      Netty 出发点作为一款高性能的 RPC 框架必然涉及到频繁的内存分配销毁操作,框架自己实现了一套创建、回收堆外内存池的相关功能。

    为什么要使用内存池?


    操作系统中的 零拷贝

    为什么需要零拷贝?

      应用程序从磁盘或网卡上获取数据时,为了合规的在不同的缓冲区域操作数据,需要来回拷贝数据,在拷贝的过程中,会消耗一部分时间,还可能会占用 CPU ,进而影响系统的处理效率。为了提高应用的吞吐量,我们应该尽量避免数据拷贝的过程出现。

      使用零拷贝方式,不仅仅带来更少的数据复制,还能带来其他的性能优势,例如更少的上下文切换,更少的 CPU 缓存伪共享以及无 CPU 校验和计算。

    网络数据拷贝流程的演变:

    传统的数据读入和写出流程: DMA: direct memory access 直接内存拷贝(不使用 CPU)

    mmap(Memory Mapping)内存映射: 减少一次内核到用户态的数据拷贝 sendFile 优化 减少一次上下文切换,优化了内核中 cpu 拷贝读写缓冲区内容,缩短了拷贝时间 Sendfile 系统调用在内核版本 2.4 后,将 Kernel buffer 中对应的数据描述信息(内存地址,偏移量)使用 CPU Copy 到相应的 Socket 缓冲区当中,由于复制内容少,所以占用时间忽略不计。 所以,认为使用该方式的拷贝为零拷贝

      

    Processed: 0.008, SQL: 8