在一些中间件中,比如:Kafka、Netty、Rocketmq、Nginx,总是频繁的提到零拷贝技术,而与零拷贝关联的词汇往往是高性能,那么我们就来看看零拷贝怎么做到高性能。
在讲零拷贝之前,我们先明确几个概念:
- 零拷贝使用中的内存模型,比如网络IO模型:
磁盘的数据先进入文件缓冲区,然后进入应用程序的缓冲区,当应用程序处理完相关的数据后,会把数据发送到socket缓冲区。这里注意一下内核态和用户态的切换。
- DMA技术
直接存储器访问。CPU是计算机的大脑,当需要把数据从一个设备拷贝到另一个设备时,早期的计算机需要CPU参与,造成CPU中断。我们知道IO操作是很耗费时间的,CPU长时间的不可用会造成其他程序无法响应。而数据的拷贝和传输是一个耗时但是不那么复杂的操作。所以就有了DMA技术,建立自己的通路去传输数据而不需要CPU的参与,从而把CPU解放出来。
了解完以上概念之后我们来看一下一个典型的场景:把磁盘上的数据加载的应用程序后,应用程序处理加工,最后通过网络发送数据。
基本流程如下:
总计四次拷贝:
两次DMA拷贝,两次CPU拷贝。
总计四次用户/内核态切换:
用户读取数据:用户态切换到内核态
数据读取完:内核态切换到用户态
用户发送数据:用户态切换到内核态
数据发送完毕:内核态切换到用户态
那么我们来看下Linux等操作系统中的零拷贝技术:
- MMAP
将磁盘上的文件位置和应用程序缓冲区进行映射,根据映射关系将文件直接拷贝到应用程序缓冲区。在读取阶段只进行了一次拷贝。
总计三次拷贝:
两次DMA拷贝,一次CPU拷贝
总计四次用户/内核态切换
- SendFile
当磁盘文件dma拷贝到文件缓冲区中后,CPU可以直接将内容拷贝到socket buffer。注意,这里如果dma设备支持,可以将文件的起始位置和大小直接传递给协议引擎,这样就少了一次拷贝,但是需要dma硬件设备的支持。
总计三次拷贝:
两次DMA拷贝,一次CPU拷贝
总计两用户/内核态切换:
发送阶段用户态切换到内核态,发送完毕后内核态切换回用户态。
- Slice
linux2.6之后,可以不用硬件支持,原理是文件缓冲区和socket buffer直接建立管道pipeline,也就是共享内存区域。
这样经过最后一次cpu拷贝也省去了,最终经历了2次上下文切换和2次DMA拷贝。
java生态中的零拷贝技术:
Kafka:
主要有两点:
- Producer生产的数据存到broker
- Consumer从broker读取数据
对于1,broker使用mmap文件映射,再加上顺序写入,可以快速把消息保存到磁盘
对于2,broker使用sendfile,可以快速读取磁盘文件,发送到socket buffer。
在加上顺序读写,效率非常高。
NIO:
MappedByteBuffer,底层使用linxu的mmap()实现。
FileChannel,transferTo 和 transferFrom 两个方法,使用sendFile可以直接拷贝到socketBuffer。
Netty:
ByteBuf支持slice 操作,因此可以将ByteBuf分解为多个共享同一个存储区域的ByteBuf,避免了内存的拷贝。
Netty 的通过FileRegion包装的FileChannel.tranferTo实现文件传输。