Netty零拷贝
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Netty零拷贝相关的知识,希望对你有一定的参考价值。
参考技术A netty的零拷贝技术主要基于以下几点:1. 堆外内存,也叫直接内存
2. Composite Buffers
3. 文件传输基于linux的sendfile机制
Linux的设计的初衷:给不同的操作给与不同的“权限”。Linux操作系统就将权限等级分为了2个等级,分别就是 内核态和用户态。
内核态是属于cpu的特权工作模式,可以操作计算机设备中的任何元件,包括网卡、硬盘、内存等等。
用户态是应用程序的工作模式,只能操作已申请的内存空间,无法操作外围设备。当应用程序需要与网卡、硬盘等外围设备进行交互时,需要通过系统提供的接口,来调用外围设备。
堆内存中的数据如果需要发送到外围设备,需要调用系统的接口,将数据拷贝到堆外内存中,发送到外围设备中。
而Netty的ByteBuffer不经过堆内存,直接在堆外内存中进行读写,省去一步拷贝操作。
需要注意的是,堆外内存只能通过主动调用回收或者Full GC回收,如果使用不当,容易造成内存溢出。
Composite Buffers
Netty提供了Composite Buffers来组合多个buffer。传统的buffer如果要合并的话,需要新建一个buffer,将原来的buffer拷贝到新的buffer中进行合并。而Composite Buffers相当于buffer的集合,保存了每个buffer对象,使物理的buffer合并变为逻辑上的buffer的合并。
文件传输
Netty的文件传输是依赖于操作系统的零拷贝技术。
一般我们读取文件都是调用操作系统接口,操作系统在应用程序读取文件时,会首先判断文件是否在内核缓冲区中,如果不在,则需要将文件从磁盘或socket读取到内核缓冲区中。
在写入文件时,操作系统会将文件先写入内核缓冲区,再写入到socket中。
我们传统做文件拷贝或传输时,会先在应用程序内存中构建一个缓冲区,通过这个缓冲区与操作系统做数据交换。这样无疑会增加了文件的多次拷贝。
传统的文件传输过程如下:
1. 构建byte[]数组来缓冲文件
2. 切换到内核态,将文件先在内核缓冲区中缓存
3. 将内核缓冲区的数据拷贝到应用程序缓冲区的byte[]数组中
4. 切换回用户态
5. 执行写入操作,切换回内核态
6. 将数据再拷贝一份到内核中的socket缓冲区
7. 切换回用户态
8. 操作系统将数据异步刷新到网卡
传统的文件传输过程,会造成操作系统在用户态和内核态多次切换,非常影响性能。
而linux在内核2.1中引入了sendfile操作,过程如下:
1. 读取数据时,sendfile系统调用导致文件内容通过DMA模块被复制到内核缓冲区中
2. 写入数据时,数据直接复制到socket关联的缓冲区(linux内核2.4已删除这一步,取而代之的是,只有记录数据位置和长度的描述符被加入到socket缓冲区中。DMA模块将数据直接从内核缓冲区传递给协议引擎)
3. 最后将socket buffer中的数据copy到网卡设备中(protocol buffer)发送
netty的FileRegion 包下的FileChannel.tranferTo即是基于sendfile机制来实现文件传输的
Netty面试常驻题:你知道Netty的零拷贝机制吗?
理解零拷贝 零拷贝是Netty的重要特性之一,而究竟什么是零拷贝呢?WIKI中对其有如下定义:
"Zero-copy" describes computer operations in which the CPU does not perform the task of copying data from one memory area to another.
从WIKI的定义中,我们看到“零拷贝”是指计算机操作的过程中,CPU不需要为数据在内存之间的拷贝消耗资源。而它通常是指计算机在网络上发送文件时,不需要将文件内容拷贝到用户空间(User Space)而直接在内核空间(Kernel Space)中传输到网络的方式。
Non-Zero Copy方式:
Zero Copy方式:
从上图中可以清楚的看到,Zero Copy的模式中,避免了数据在用户空间和内存空间之间的拷贝,从而提高了系统的整体性能。Linux中的sendfile()以及Java NIO中的FileChannel.transferTo()方法都实现了零拷贝的功能,而在Netty中也通过在FileRegion中包装了NIO的FileChannel.transferTo()方法实现了零拷贝。
而在Netty中还有另一种形式的零拷贝,即Netty允许我们将多段数据合并为一整段虚拟数据供用户使用,而过程中不需要对数据进行拷贝操作,这也是我们今天要讲的重点。我们都知道在stream-based transport(如TCP/IP)的传输过程中,数据包有可能会被重新封装在不同的数据包中,例如当你发送如下数据时:
有可能实际收到的数据如下:
因此在实际应用中,很有可能一条完整的消息被分割为多个数据包进行网络传输,而单个的数据包对你而言是没有意义的,只有当这些数据包组成一条完整的消息时你才能做出正确的处理,而Netty可以通过零拷贝的方式将这些数据包组合成一条完整的消息供你来使用。而此时,零拷贝的作用范围仅在用户空间中。
以Netty 3.8.0.Final的源代码来进行说明 ###ChannelBuffer接口 Netty为需要传输的数据制定了统一的ChannelBuffer接口。该接口的主要设计思路如下:
1.使用getByte(int index)方法来实现随机访问
2.使用双指针的方式实现顺序访问
每个Buffer都有一个读指针(readIndex)和写指针(writeIndex)
在读取数据时读指针后移,在写入数据时写指针后移
定义了统一的接口之后,就是来做各种实现了。Netty主要实现了HeapChannelBuffer,ByteBufferBackedChannelBuffer等等,下面我们就来讲讲与Zero Copy直接相关的CompositeChannelBuffer类。###CompositeChannelBuffer类 CompositeChannelBuffer类的作用是将多个ChannelBuffer组成一个虚拟的ChannelBuffer来进行操作。
为什么说是虚拟的呢,因为CompositeChannelBuffer并没有将多个ChannelBuffer真正的组合起来,而只是保存了他们的引用,这样就避免了数据的拷贝,实现了Zero Copy。下面我们来看看具体的代码实现,首先是成员变量
private int readerIndex;
private int writerIndex;
private ChannelBuffer[] components;
private int[] indices;
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时被调用。
/**
* Setup this ChannelBuffer from the list
*/
private void setComponents(List<ChannelBuffer> newComponents) {
assert !newComponents.isEmpty();
// Clear the cache.
lastAccessedComponentId = 0;
// Build the component array.
components = new ChannelBuffer[newComponents.size()];
for (int i = 0; i < components.length; i ++) {
ChannelBuffer c = newComponents.get(i);
if (c.order() != order()) {
throw new IllegalArgumentException(
"All buffers must have the same endianness.");
}
assert c.readerIndex() == 0;
assert c.writerIndex() == c.capacity();
components[i] = c;
}
// Build the component lookup table.
indices = new int[components.length + 1];
indices[0] = 0;
for (int i = 1; i <= components.length; i ++) {
indices[i] = indices[i - 1] + components[i - 1].capacity();
}
// Reset the indexes.
setIndex(0, capacity());
}
通过代码可以看到该方法的功能就是将一个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)的实现如下:
public byte getByte(int index) {
int componentId = componentId(index);
return components[componentId].getByte(index - indices[componentId]);
}
从代码我们可以看到,在随机查找时会首先通过index获取这个字节所在的componentId既字节所在的子Buffer序列,然后通过index - indices[componentId]计算出它在这个子Buffer中的第几个字节,然后返回结果。
下面再来看一下componentId(int index) 的实现:
private int componentId(int index) {
int lastComponentId = lastAccessedComponentId;
if (index >= indices[lastComponentId]) {
if (index < indices[lastComponentId + 1]) {
return lastComponentId;
}
// Search right
for (int i = lastComponentId + 1; i < components.length; i ++) {
if (index < indices[i + 1]) {
lastAccessedComponentId = i;
return i;
}
}
} else {
// Search left
for (int i = lastComponentId - 1; i >= 0; i --) {
if (index >= indices[i]) {
lastAccessedComponentId = i;
return i;
}
}
}
throw new IndexOutOfBoundsException("Invalid index: " + index + ", maximum: " + indices.length);
}
从代码中我们发现,Netty以lastComponentId既上次访问的子Buffer序号为中心,向左右两边进行搜索,这样做的目的是,当我们两次随机查找的字符序列相近时(大部分情况下都是这样),可以最快的搜索到目标索引的componentId。
写在最后
欢迎大家关注我的公众号【风平浪静如码】,海量Java相关文章,学习资料都会在里面更新,整理的资料也会放在里面。
觉得写的还不错的就点个赞,加个关注呗!点关注,不迷路,持续更新!!!
以上是关于Netty零拷贝的主要内容,如果未能解决你的问题,请参考以下文章