98300

查看: 44|回复: 0

理解Netty中的零拷贝(Zero-Copy)机制

[复制链接]
  • TA的每日心情
    擦汗
    2019-9-22 20:46
  • 签到天数: 12 天

    [LV.3]偶尔看看II

    601

    主题

    601

    帖子

    2516

    积分

    管理员

    Rank: 9Rank: 9Rank: 9

    积分
    2516
    QQ
    发表于 2019-9-1 16:44:16 | 显示全部楼层 |阅读模式
    理解零拷贝 零拷贝是Netty的重要特性之一,而究竟什么是零拷贝呢? WIKI中对其有如下定义:
    1. "Zero-copy" describes computer operations in which the CPU does not perform the task of copying data from one memory area to another.
    复制代码
    Non-Zero Copy方式:
    Zero Copy方式:
    从上图中可以清楚的看到,Zero Copy的模式中,避免了数据在用户空间和内存空间之间的拷贝,从而提高了系统的整体性能。Linux中的sendfile()以及Java NIO中的FileChannel.transferTo()方法都实现了零拷贝的功能,而在Netty中也通过在FileRegion中包装了NIO的FileChannel.transferTo()方法实现了零拷贝。
    112132_6ASE_859646.png
    112212_qD2w_859646.png
    113714_EoXP_859646.png
    105411_Hrta_859646.png
    定义了统一的接口之后,就是来做各种实现了。Netty主要实现了HeapChannelBuffer,ByteBufferBackedChannelBuffer等等,下面我们就来讲讲与Zero Copy直接相关的CompositeChannelBuffer类。 ###CompositeChannelBuffer类CompositeChannelBuffer类的作用是将多个ChannelBuffer组成一个虚拟的ChannelBuffer来进行操作。为什么说是虚拟的呢,因为CompositeChannelBuffer并没有将多个ChannelBuffer真正的组合起来,而只是保存了他们的引用,这样就避免了数据的拷贝,实现了Zero Copy。 下面我们来看看具体的代码实现,首先是成员变量

    1. private int readerIndex;
    2. private int writerIndex;
    3. private ChannelBuffer[] components;
    4. private int[] indices;
    5. private int lastAccessedComponentId;
    复制代码
    以上这里列出了几个比较重要的成员变量。其中readerIndex既读指针和writerIndex既写指针是从AbstractChannelBuffer继承而来的;然后components是一个ChannelBuffer的数组,他保存了组成这个虚拟Buffer的所有子Buffer,indices是一个int类型的数组,它保存的是各个Buffer的索引值;最后的lastAccessedComponentId是一个int值,它记录了最后一次访问时的子Buffer ID。从这个数据结构,我们不难发现所谓的CompositeChannelBuffer实际上就是将一系列的Buffer通过数组保存起来,然后实现了ChannelBuffer 的接口,使得在上层看来,操作这些Buffer就像是操作一个单独的Buffer一样。
    创建 接下来,我们再看一下CompositeChannelBuffer.setComponents方法,它会在初始化CompositeChannelBuffer时被调用。
    1. /**
    2. * Setup this ChannelBuffer from the list
    3. */
    4. private void setComponents(List<ChannelBuffer> newComponents) {
    5.     assert !newComponents.isEmpty();

    6.     // Clear the cache.
    7.     lastAccessedComponentId = 0;

    8.     // Build the component array.
    9.     components = new ChannelBuffer[newComponents.size()];
    10.     for (int i = 0; i < components.length; i ++) {
    11.         ChannelBuffer c = newComponents.get(i);
    12.         if (c.order() != order()) {
    13.             throw new IllegalArgumentException(
    14.                     "All buffers must have the same endianness.");
    15.         }

    16.         assert c.readerIndex() == 0;
    17.         assert c.writerIndex() == c.capacity();

    18.         components[i] = c;
    19.     }

    20.     // Build the component lookup table.
    21.     indices = new int[components.length + 1];
    22.     indices[0] = 0;
    23.     for (int i = 1; i <= components.length; i ++) {
    24.         indices[i] = indices[i - 1] + components[i - 1].capacity();
    25.     }

    26.     // Reset the indexes.
    27.     setIndex(0, capacity());
    28. }
    复制代码
    通过代码可以看到该方法的功能就是将一个ChannelBuffer的List给组合起来。它首先将List中得元素放入到components数组中,然后创建indices用于数据的查找,最后使用setIndex来重置指针。这里需要注意的是setIndex(0, capacity())会将读指针设置为0,写指针设置为当前Buffer的长度,这也就是前面需要做assert c.readerIndex() == 0和assert c.writerIndex() == c.capacity()这两个判断的原因,否则很容易会造成数据重复读写的问题,所以Netty推荐我们使用ChannelBuffers.wrappedBuffer方法来进行Buffer的合并,因为在该方法中Netty会通过slice()方法来确保构建CompositeChannelBuffer是传入的所有子Buffer都是符合要求的。
    数据访问 CompositeChannelBuffer.getByte(int index)的实现如下:
    1. public byte getByte(int index) {
    2.     int componentId = componentId(index);
    3.     return components[componentId].getByte(index - indices[componentId]);
    4. }
    复制代码
    从代码我们可以看到,在随机查找时会首先通过index获取这个字节所在的componentId既字节所在的子Buffer序列,然后通过index - indices[componentId]计算出它在这个子Buffer中的第几个字节,然后返回结果。
    下面再来看一下componentId(int index)的实现:
    1. private int componentId(int index) {
    2.     int lastComponentId = lastAccessedComponentId;
    3.     if (index >= indices[lastComponentId]) {
    4.         if (index < indices[lastComponentId + 1]) {
    5.             return lastComponentId;
    6.         }

    7.         // Search right
    8.         for (int i = lastComponentId + 1; i < components.length; i ++) {
    9.             if (index < indices[i + 1]) {
    10.                 lastAccessedComponentId = i;
    11.                 return i;
    12.             }
    13.         }
    14.     } else {
    15.         // Search left
    16.         for (int i = lastComponentId - 1; i >= 0; i --) {
    17.             if (index >= indices[i]) {
    18.                 lastAccessedComponentId = i;
    19.                 return i;
    20.             }
    21.         }
    22.     }

    23.     throw new IndexOutOfBoundsException("Invalid index: " + index + ", maximum: " + indices.length);
    24. }
    复制代码
    从代码中我们发现,Netty以lastComponentId既上次访问的子Buffer序号为中心,向左右两边进行搜索,这样做的目的是,当我们两次随机查找的字符序列相近时(大部分情况下都是这样),可以最快的搜索到目标索引的componentId。
    参考资料:

    104957_UW6E_859646.png
    104700_qc4H_859646.png
    回复

    使用道具 举报

    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则

    QQ|Archiver|手机版|小黑屋| 98300.NET

    GMT+8, 2019-10-18 21:55 , Processed in 0.125264 second(s), 9 queries , MemCache On.

    Powered by Discuz! X

    © 2015-2016 98300.NET

    快速回复 返回顶部 返回列表