Netty中的零拷贝是怎么实现的?
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Netty中的零拷贝是怎么实现的?相关的知识,希望对你有一定的参考价值。
参考技术ANetty是Java语言中一个高性能的网络通信框架,零拷贝又是这个框架的特色之一,它是如何实现的呢?
在计算机中完成一次数据传输,一般需要经过两个阶段。第一步,操作系统把数据从本地硬盘或网卡拷贝到内核空间的内存;第二步,应用程序再把数据从系统内核空间的内存拷贝到用户空间的内存;接下来才是应用程序中的数据处理工作。
先来看几个名词。
DMA(Direct Memory Access)直接存储器访问,将数据从一个地址空间复制到另一个地址空间。当CPU初始化这个传输动作后,传输动作本身是由DMA控制器(DMAC)来完成的。也就是说在数据传输期间,系统可以并行执行其他任务。CPU拷贝,是由CPU直接处理的数据的传送,数据拷贝时一直占用CPU资源。
从上图中可以看出,传统的IO读写流程,包括4次用户态和内核态的切换,4次上下文切换,4次的数据拷贝,2次CPU拷贝,2次DMA拷贝。
一、什么是零拷贝?
拷贝,是指数据从一个存储区域复制到另一个存储区域。 零,表示次数为0,复制的次数为0,也就是数据不需要从一个存储区域复制到另一个存储区域。
二、为什么需要零拷贝?
零拷贝,就是指从系统内核空间的内存到用户空间的内存,不需要采用传统方式的数据复制。而是将系统内核空间的内存和用户空间的内存实现关联映射(mmap内存映射机制),从而省去了数据传输过程中的复制。
mmap(memory map)内存映射机制,简单来说就是将文件/设备映射到内存中,进程可以通过读写内存的方式,实现对mmap文件的操作。零拷贝并不是完全没有拷贝,而是减少了数据拷贝的次数。
三、零拷贝在Netty中的三种实现。
1.使用堆外内存,也叫直接内存(Direct Memory)。netty的接收和发生都是使用Direct buffer,对应系统底层的mmap机制,直接使用堆外内存进行Socket读写,不需要进行字节缓冲区的二次拷贝。
2.提供了组合buffer对象 (CompositeByteBuf),可以聚合多个ByteBuffer对象,用户只需要像操作一个ByteBuffer一样操作组合ByteBuffer,避免了传统通过内存拷贝的方式将几个buffer合并成一个大buffer,不需要内存拷贝。
3.文件传输采用TransferTo方法,它可以直接将文件缓冲区的数据发送到目标channel,避免了传统通过循环write方式导致的内存拷贝问题。
最后总结
通过整理可以发现,netty的零拷贝并不是完全不拷贝,而是减少了CPU拷贝,也就是数据从系统内核空间的内存到用户空间内存的拷贝。DMA拷贝还是存在的,毕竟它是操作系统所做的事情,不属于应用程序的操作范围。在netty中,目前有三种方式实现的零拷贝。第一种使用堆外内存。第二种,CompositeByteBuf组合buffer对象。第三种,文件传输采用TransferTo方法。
参考文档: https://mp.weixin.qq.com/s/HvdiDbkMMMcGhee5Dhq_Jw
深入探秘 NettyKafka 中的零拷贝技术!
点击蓝色“架构文摘”关注我哟
加个“星标”,每天上午 09:25,干货推送!
来源:https://juejin.im/post/5cad6f1ef265da039f0ef5df
前言
从字面意思理解就是数据不需要来回的拷贝,大大提升了系统的性能;这个词我们也经常在java nio,netty,kafka,RocketMQ等框架中听到,经常作为其提升性能的一大亮点;下面从I/O的几个概念开始,进而在分析零拷贝。
I/O概念
1.缓冲区
缓冲区是所有I/O的基础,I/O讲的无非就是把数据移进或移出缓冲区;进程执行I/O操作,就是向操作系统发出请求,让它要么把缓冲区的数据排干(写),要么填充缓冲区(读);下面看一个java进程发起read请求加载数据大致的流程图:
进程发起read请求之后,内核接收到read请求之后,会先检查内核空间中是否已经存在进程所需要的数据,如果已经存在,则直接把数据copy给进程的缓冲区;如果没有内核随即向磁盘控制器发出命令,要求从磁盘读取数据,磁盘控制器把数据直接写入内核read缓冲区,这一步通过DMA完成;接下来就是内核将数据copy到进程的缓冲区;
如果进程发起write请求,同样需要把用户缓冲区里面的数据copy到内核的socket缓冲区里面,然后再通过DMA把数据copy到网卡中,发送出去;
你可能觉得这样挺浪费空间的,每次都需要把内核空间的数据拷贝到用户空间中,所以零拷贝的出现就是为了解决这种问题的;
关于零拷贝提供了两种方式分别是:mmap+write方式,sendfile方式;
2.虚拟内存
省去了内核与用户空间的往来拷贝,java也利用操作系统的此特性来提升性能,下面重点看看java对零拷贝都有哪些支持。
3.mmap+write方式
4.sendfile方式
sendfile系统调用在内核版本2.1中被引入,目的是简化通过网络在两个通道之间进行的数据传输过程。sendfile系统调用的引入,不仅减少了数据复制,还减少了上下文切换的次数,大致如下图所示:
Java零拷贝
1.MappedByteBuffer
java
nio提供的FileChannel提供了map()方法,该方法可以在一个打开的文件和MappedByteBuffer之间建立一个虚拟内存映射,MappedByteBuffer继承于ByteBuffer,类似于一个基于内存的缓冲区,只不过该对象的数据元素存储在磁盘的一个文件中;调用get()方法会从磁盘中获取数据,此数据反映该文件当前的内容,调用put()方法会更新磁盘上的文件,并且对文件做的修改对其他阅读者也是可见的;下面看一个简单的读取实例,然后在对MappedByteBuffer进行分析:
public class MappedByteBufferTest {
public static void main(String[] args) throws Exception {
File file = new File("D://db.txt");
long len = file.length();
byte[] ds = new byte[(int) len];
MappedByteBuffer mappedByteBuffer = new FileInputStream(file).getChannel().map(FileChannel.MapMode.READ_ONLY, 0,
len);
for (int offset = 0; offset < len; offset++) {
byte b = mappedByteBuffer.get();
ds[offset] = b;
}
Scanner scan = new Scanner(new ByteArrayInputStream(ds)).useDelimiter(" ");
while (scan.hasNext()) {
System.out.print(scan.next() + " ");
}
}
}
复制代码
主要通过FileChannel提供的map()来实现映射,map()方法如下:
public abstract MappedByteBuffer map(MapMode mode,
long position, long size)
throws IOException;
复制代码
分别提供了三个参数,MapMode,Position和size;分别表示:
MapMode:映射的模式,可选项包括:READ_ONLY,READ_WRITE,PRIVATE;
Position:从哪个位置开始映射,字节数的位置;
Size:从position开始向后多少个字节;
重点看一下MapMode,请两个分别表示只读和可读可写,当然请求的映射模式受到Filechannel对象的访问权限限制,如果在一个没有读权限的文件上启用READ_ONLY,将抛出NonReadableChannelException;PRIVATE模式表示写时拷贝的映射,意味着通过put()方法所做的任何修改都会导致产生一个私有的数据拷贝并且该拷贝中的数据只有MappedByteBuffer实例可以看到;该过程不会对底层文件做任何修改,而且一旦缓冲区被施以垃圾收集动作(garbage
collected),那些修改都会丢失;大致浏览一下map()方法的源码:
public MappedByteBuffer map(MapMode mode, long position, long size)
throws IOException
{
...省略...
int pagePosition = (int)(position % allocationGranularity);
long mapPosition = position - pagePosition;
long mapSize = size + pagePosition;
try {
// If no exception was thrown from map0, the address is valid
addr = map0(imode, mapPosition, mapSize);
} catch (OutOfMemoryError x) {
// An OutOfMemoryError may indicate that we've exhausted memory
// so force gc and re-attempt map
System.gc();
try {
Thread.sleep(100);
} catch (InterruptedException y) {
Thread.currentThread().interrupt();
}
try {
addr = map0(imode, mapPosition, mapSize);
} catch (OutOfMemoryError y) {
// After a second OOME, fail
throw new IOException("Map failed", y);
}
}
// On Windows, and potentially other platforms, we need an open
// file descriptor for some mapping operations.
FileDescriptor mfd;
try {
mfd = nd.duplicateForMapping(fd);
} catch (IOException ioe) {
unmap0(addr, mapSize);
throw ioe;
}
assert (IOStatus.checkAll(addr));
assert (addr % allocationGranularity == 0);
int isize = (int)size;
Unmapper um = new Unmapper(addr, mapSize, isize, mfd);
if ((!writable) || (imode == MAP_RO)) {
return Util.newMappedByteBufferR(isize,
addr + pagePosition,
mfd,
um);
} else {
return Util.newMappedByteBuffer(isize,
addr + pagePosition,
mfd,
um);
}
}
复制代码
2.DirectByteBuffer
DirectByteBuffer继承于MappedByteBuffer,从名字就可以猜测出开辟了一段直接的内存,并不会占用jvm的内存空间;上一节中通过Filechannel映射出的MappedByteBuffer其实际也是DirectByteBuffer,当然除了这种方式,也可以手动开辟一段空间:
ByteBuffer directByteBuffer = ByteBuffer.allocateDirect(100);
复制代码
如上开辟了100字节的直接内存空间;
3.Channel-to-Channel传输
经常需要从一个位置将文件传输到另外一个位置,FileChannel提供了transferTo()方法用来提高传输的效率,首先看一个简单的实例:
public class ChannelTransfer {
public static void main(String[] argv) throws Exception {
String files[]=new String[1];
files[0]="D://db.txt";
catFiles(Channels.newChannel(System.out), files);
}
private static void catFiles(WritableByteChannel target, String[] files)
throws Exception {
for (int i = 0; i < files.length; i++) {
FileInputStream fis = new FileInputStream(files[i]);
FileChannel channel = fis.getChannel();
channel.transferTo(0, channel.size(), target);
channel.close();
fis.close();
}
}
}
复制代码
通过FileChannel的transferTo()方法将文件数据传输到System.out通道,接口定义如下:
public abstract long transferTo(long position, long count,
WritableByteChannel target)
throws IOException;
几个参数也比较好理解,分别是开始传输的位置,传输的字节数,以及目标通道;transferTo()允许将一个通道交叉连接到另一个通道,而不需要一个中间缓冲区来传递数据;
注:这里不需要中间缓冲区有两层意思:第一层不需要用户空间缓冲区来拷贝内核缓冲区,另外一层两个通道都有自己的内核缓冲区,两个内核缓冲区也可以做到无需拷贝数据;
Netty零拷贝
netty提供了零拷贝的buffer,在传输数据时,最终处理的数据会需要对单个传输的报文,进行组合和拆分,Nio原生的ByteBuffer无法做到,netty通过提供的Composite(组合)和Slice(拆分)两种buffer来实现零拷贝;看下面一张图会比较清晰:
TCP层HTTP报文被分成了两个ChannelBuffer,这两个Buffer对我们上层的逻辑(HTTP处理)是没有意义的。
但是两个ChannelBuffer被组合起来,就成为了一个有意义的HTTP报文,这个报文对应的ChannelBuffer,才是能称之为”Message”的东西,这里用到了一个词”Virtual
Buffer”。
可以看一下netty提供的CompositeChannelBuffer源码:
public class CompositeChannelBuffer extends AbstractChannelBuffer {
private final ByteOrder order;
private ChannelBuffer[] components;
private int[] indices;
private int lastAccessedComponentId;
private final boolean gathering;
public byte getByte(int index) {
int componentId = componentId(index);
return components[componentId].getByte(index - indices[componentId]);
}
...省略...
components用来保存的就是所有接收到的buffer,indices记录每个buffer的起始位置,lastAccessedComponentId记录上一次访问的ComponentId;CompositeChannelBuffer并不会开辟新的内存并直接复制所有ChannelBuffer内容,而是直接保存了所有ChannelBuffer的引用,并在子ChannelBuffer里进行读写,实现了零拷贝。
其他零拷贝
RocketMQ的消息采用顺序写到commitlog文件,然后利用consume
queue文件作为索引;RocketMQ采用零拷贝mmap+write的方式来回应Consumer的请求;
同样kafka中存在大量的网络数据持久化到磁盘和磁盘文件通过网络发送的过程,kafka使用了sendfile零拷贝方式;
总结
零拷贝如果简单用java里面对象的概率来理解的话,其实就是使用的都是对象的引用,每个引用对象的地方对其改变就都能改变此对象,永远只存在一份对象。
推荐阅读:
如有收获,点个在看,诚挚感谢
以上是关于Netty中的零拷贝是怎么实现的?的主要内容,如果未能解决你的问题,请参考以下文章
#私藏项目实操分享# Java深层系列「技术盲区」让我们一起探索一下Netty(Java)底层的“零拷贝Zero-Copy”技术(上)