Netty 中的 ByteBuf

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

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 缓冲区当中,由于复制内容少,所以占用时间忽略不计。
所以,认为使用该方式的拷贝为零拷贝

  

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值