深入探秘 Netty、Kafka 中的零拷贝技术!

零拷贝,从字面意思理解就是数据不需要来回的拷贝,大大提升了系统的性能 。我们也经常在 JAVA NIO,Netty,Kafka,RocketMQ 等框架中听到零拷贝,它经常作为其提升性能的一大亮点
下面从 I/O 的几个概念开始,进而再分析零拷贝 。
I/O 概念缓冲区
缓冲区是所有 I/O 的基础,I/O 讲的无非就是把数据移进或移出缓冲区;进程执行 I/O 操作,就是向操作系统发出请求,让它要么把缓冲区的数据排干(写),要么填充缓冲区(读) 。
下面看一个 Java 进程发起 Read 请求加载数据大致的流程图:

深入探秘 Netty、Kafka 中的零拷贝技术!

文章插图
 
进程发起 Read 请求之后,内核接收到 Read 请求之后,会先检查内核空间中是否已经存在进程所需要的数据,如果已经存在,则直接把数据 Copy 给进程的缓冲区 。
如果没有内核随即向磁盘控制器发出命令,要求从磁盘读取数据,磁盘控制器把数据直接写入内核 Read 缓冲区,这一步通过 DMA 完成 。
接下来就是内核将数据 Copy 到进程的缓冲区;如果进程发起 Write 请求,同样需要把用户缓冲区里面的数据 Copy 到内核的 Socket 缓冲区里面,然后再通过 DMA 把数据 Copy 到网卡中,发送出去 。
你可能觉得这样挺浪费空间的,每次都需要把内核空间的数据拷贝到用户空间中,所以零拷贝的出现就是为了解决这种问题的 。
关于零拷贝提供了两种方式分别是:
  • mmap+write
  • Sendfile
虚拟内存所有现代操作系统都使用虚拟内存,使用虚拟的地址取代物理地址,这样做的好处是:
  • 一个以上的虚拟地址可以指向同一个物理内存地址 。
  • 虚拟内存空间可大于实际可用的物理地址 。
利用第一条特性可以把内核空间地址和用户空间的虚拟地址映射到同一个物理地址,这样 DMA 就可以填充对内核和用户空间进程同时可见的缓冲区了 。
大致如下图所示:
深入探秘 Netty、Kafka 中的零拷贝技术!

文章插图
 
省去了内核与用户空间的往来拷贝,Java 也利用操作系统的此特性来提升性能,下面重点看看 Java 对零拷贝都有哪些支持 。
mmap+write 方式使用 mmap+write 方式代替原来的 read+write 方式,mmap 是一种内存映射文件的方法,即将一个文件或者其他对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对应关系 。
这样就可以省掉原来内核 Read 缓冲区 Copy 数据到用户缓冲区,但是还是需要内核 Read 缓冲区将数据 Copy 到内核 Socket 缓冲区 。
大致如下图所示:
深入探秘 Netty、Kafka 中的零拷贝技术!

文章插图
 
 
Sendfile 方式Sendfile 系统调用在内核版本 2.1 中被引入,目的是简化通过网络在两个通道之间进行的数据传输过程 。
Sendfile 系统调用的引入,不仅减少了数据复制,还减少了上下文切换的次数,大致如下图所示:
深入探秘 Netty、Kafka 中的零拷贝技术!

文章插图
 
数据传送只发生在内核空间,所以减少了一次上下文切换;但是还是存在一次 Copy,能不能把这一次 Copy 也省略掉?
linux2.4 内核中做了改进,将 Kernel buffer 中对应的数据描述信息(内存地址,偏移量)记录到相应的 Socket 缓冲区当中,这样连内核空间中的一次 CPU Copy 也省掉了 。
Java 零拷贝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() + " ");        }    }}


推荐阅读