Kafka零拷贝详解


一、数据读写

1. 传统方式

首先让我们看一下不做任何特殊处理的数据流读取方式,以文件下载请求为例,其大致流程如下:

  • 用户端发送 read() 请求读取数据;
  • CPU 在接收到该请求后向磁盘发出 IO 请求;
  • 磁盘接收到 CPU 发出的 IO 请求后读取数据并存入磁盘缓存区,并在完成后向 CPU 发送响应;
  • CPU 接收到响应后读取磁盘缓存区数据至 Page Cache;
  • 读取至 Page Cache 后需要再次拷贝至用户缓存区;

了解大致流程之后你可能会疑惑,为什么数据读取至磁盘缓冲区之后 CPU 不直接拷贝至用户缓存区,而是要先拷贝到 Page Cache?原因其实也很简单,因为用户缓存区权限相对较低,无法直接与磁盘交互,必须通过内核层进行中转,而 Page Cache 则是内核态的一种缓存优化方式。

2. DMA技术

在上述的流程中可以看到,在整个数据的拷贝过程中 CPU 都占据着主导地位,即干了所有的重活并一直处于阻塞状态,在此期间 CPU 将无法处理其它任务。很显然这种方式相对较为低效,因此 DMA(Direct Memory Access) 应运而生,它接替了 CPUIO 操作中大部分繁重搬运任务,而 CPU 的核心任务则为确定的需要传输的数据以及数据的去向。

同样以之前的 read() 读取为例,与之前的不同的区别如下:

  • CPU 接收到 IO 请求后转发至 DMA,由其向磁盘发出 IO 请求;
  • 磁盘读取完成后通知 DMA 执行拷贝,此过程 CPU 不参与搬运任务;

可以看到以一次读取为例,CPU 的拷贝频次由 2 次降为 1 次,而通常读取伴随着写入操作,也就意味着一次完整的读写操作就能让 CPU 闲置出 2 两次搬运操作从而执行其它任务。

3. 数据缓冲

上述提到的两种方式中都涉及到一个概念——内核缓冲区 (Page Cache),我们都知道内存的读取效率远超于磁盘。因此,若能够在内存中操作磁盘数据效率将大大提升,而 Page Cache 则是磁盘数据在内存中的暂存空间。

通过 Page Cache 将磁盘上的数据缓存在内存中,系统可以避免频繁地访问慢速的磁盘,从而减少了 I/O 操作的次数,加快了数据的访问速度。当进程再次访问相同的数据时,系统可以直接从 Page Cache 中读取,而不必再次从磁盘中加载,从而节省了大量的时间。

Page Cache 是一种按需加载的缓存机制,它会根据系统内存的可用情况来管理缓存。当系统内存不足时,操作系统会根据一定的策略来释放 Page Cache 中的部分数据,以确保系统的正常运行。

二、模式优化

1. 内存映射

虽然 DMA 技术降低了 CPU 负载压力,让其能够执行更多的系统任务,但针对于 IO 操作读写任务而言,整体流程上的搬运工作并没有减轻,一次完成的读写操作仍需要搬运 4 次。因此,想要提高 IO 流程的效率,需要的是减少重复数据拷贝工作。

由于用户层与内核、磁盘之间的权限问题,导致同一份数据在此期间执行了多次搬运拷贝,以之前的提到的图示为例,如果用户层能够直接读取内核层数据,那么 CPU 即可省去将数据由内核层拷贝至用户层这一操作。

mmap(Memory-mapped file support) 内存映射则正是实现了这一点,其将内核缓冲区里的数据映射到用户层,从而实现在用户层即可访问内核层数据,也就节省了一次 CPU 数据拷贝。

2. sendfile

linux 中提供了专门的文件传输函数 sendfile() 可用于替代 read()write() 操作。该函数能够直接实现两个内核态的数据拷贝,从而减少了 1 次用户态层级切换,在 JavaNIO 中也涉及到该函数的封装调用,后面会详细介绍。

Nginx 的配置文件中经常可以看到下述配置,其即代表使用 sendfile 执行传输。

http {
    sendfile        on;
}

三、Java实现

1. NIO接口

在上述提到 linux 中提供了 sendfile() 替换了 read()write() 从而减少状态的切换,在 NIO 中的 FileChannel 正利用了这一方式提高了传输效率。

FileChannelImpltransferTo() 方法中正是封装利用这一特性执行数据的传输,通过下述图示了解其类的层级结构。

transferTo() 方法的内部实现逻辑此处不具体展开描述,感兴趣的朋友可以自行查看 JDk 源码,这里以简洁的图示描述其执行调用路径。

2. 源码解读

NIOtransferTo() 方法调用执行最终定格在 transferTo0() 方法,可以看到该方法为 native 关键字声明,表明其为原生 c++ 方式实现。

为了查看其具体内容,我们需要查看 openjdk 的源码,此版本为开源版本,而日常使用的并不开源也就无法查看具体 native 实现逻辑,具体的实现代码内容链接:c++源码实现

这里为了更方便查看我截图部分关键代码,可以看到在 Linux 系统下其正是通过调用 sendfile() 实现。

四、大文件传输

1. 直接IO

之前提到了 Page Cache 技术通过内存方式提高了数据读取效率,但对于大文件的场景下,显然此方式不太合适。若在传输的大文件时仍采取 Page Cache 暂存,显然内存空间是及其有限的且不论能否完整存入,其还可能触发空间的回收导致其他任务缓存的小文件内容遭到回收。

因此,对于大文件的数据读取传输,更为高效的方式即通过 直接IO 将磁盘缓冲区文件直接拷贝至用户缓存区。而针对大文件的直接 IO 方式通常使用异步进行,当完成数据到用户缓冲区的拷贝后通知用户读取数据,在此之前 CPU 可执行其它进程任务。


参考链接

  1. 傻瓜三歪让我教他「零拷贝」

文章作者: 烽火戏诸诸诸侯
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 烽火戏诸诸诸侯 !
  目录