引入
在Java架构直通车——Kafka介绍和高性能原因一节中,介绍了Kafka的Zero Copy技术。本文将深入探究一下Zero Copy的缘起和原理。
零拷贝,其实说的不是真的没有拷贝,文件传输必然经历从磁盘到网卡的过程,这个过程无论如何也会有拷贝的。零拷贝说的是CPU不参与到拷贝过程。
在了解零拷贝之前,需要知道什么是DMA、什么是PageCache。
DMA
在没有 DMA 技术前,I/O 的过程是这样的:
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210525150032562.png?x-ss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L05vX0dhbWVfTm9fTGlmZV8=,size_16,color_FFFFFF,t_70)
可以看到整个过程需要CPU参与搬运数据的过程,而且这个过程,CPU 是被占用的。由此可见其效率的低下。
据此,引入了DMA技术:
通过这个 DMA 控制器,CPU 只需要告诉 DMA 控制器,我们要传输什么数据,从哪里来,到哪里去,就可以放心离开了。后续的实际数据传输工作,都会由 DMA 控制器来完成,CPU 不需要参与数据传输的工作。
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210525150723142.png?x-os-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L05vX0dhbWVfTm9fTGlmZV8=,size_16,color_FFFFFF,t_70)
从上面的图可以看出,磁盘到内存缓冲区是由DMA来拷贝完成的,而CPU只需要完成从内核态缓冲区到用户态缓冲区数据的拷贝,用图来表示是这样的:
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210525150922447.png?x-ss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L05vX0dhbWVfTm9fTGlmZV8=,size_16,color_FFFFFF,t_70)
将磁盘上的文件读取出来,然后通过网络协议发送给客户端,这个过程就如上图所示,代码通常如下,一般会需要两个系统调用:
read(file, tmp_buf, len);
write(socket, tmp_buf, len);
这个过程中,共发生了 4 次用户态与内核态的上下文切换,因为发生了两次系统调用, 一次是 read()
,一次是 write()
,每次系统调用都得先从用户态切换到内核态,等内核完成任务后,再从内核态切换回用户态。其次,还发生了 4 次数据拷贝,其中两次是 DMA 的拷贝,另外两次则是通过 CPU 拷贝的。
上下文切换到成本并不小,一次切换需要耗时几十纳秒到几微秒,虽然时间看上去很短,但是在高并发的场景下,这类时间容易被累积和放大,从而影响系统的性能。
所以,要想提高文件传输的性能,就需要减少「用户态与内核态的上下文切换」和「内存拷贝」的次数。
PageCache
回顾前面说道文件传输过程,其中第一步都是先需要先把磁盘文件数据拷贝「内核缓冲区」里,这个「内核缓冲区」实际上是磁盘高速缓存(PageCache)。
零拷贝使用了 PageCache 技术,可以使得零拷贝进一步提升了性能。
PageCache是根据程序的局部性原理来设计的, PageCache 缓存最近被访问的数据,当空间不足时淘汰最久未被访问的缓存。
另外,读取磁盘数据的时候,需要找到数据所在的位置,但是对于机械磁盘来说,就是通过磁头旋转到数据所在的扇区,再开始「顺序」读取数据,但是旋转磁头这个物理动作是非常耗时的,为了降低它的影响,PageCache 使用了「预读功能」。
比如,假设 read 方法每次只会读 32 KB 的字节,虽然 read 刚开始只会读 0 ~ 32 KB 的字节,但内核会把其后面的 32~64 KB 也读取到 PageCache,这样后面读取 32~64 KB 的成本就很低,如果在 32~64 KB 淘汰出 PageCache 前,进程读取到它了,收益就非常大。
PageCache 的优点主要是两个:
零拷贝
零拷贝技术实现的方式通常有 2 种:
下面就谈一谈,它们是如何减少「上下文切换」和「数据拷贝」的次数。
mmap
mmap()
系统调用函数会直接把内核缓冲区里的数据「映射」到用户空间,这样,操作系统内核与用户空间就不需要再进行任何的数据拷贝操作。
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210525152559467.png?x-os-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L05vX0dhbWVfTm9fTGlmZV8=,size_16,color_FFFFFF,t_70)
我们可以看到这只减少了一次拷贝,仍然需要 4 次上下文切换。
sendfile
sendfile()可以替代前面的 read() 和 write() 这两个系统调用,这样就可以减少一次系统调用,也就减少了 2 次上下文切换的开销。
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210525152726981.png?x-os-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L05vX0dhbWVfTm9fTGlmZV8=,size_16,color_FFFFFF,t_70)
sendfile()也就减少了 2 次上下文切换的开销,减少了1次拷贝。
SG-DMA
但是上述拷贝还不是真正的零拷贝技术,如果网卡支持 SG-DMA(The Scatter-Gather Direct Memory Access)技术(和普通的 DMA 有所不同),我们可以进一步减少通过 CPU 把内核缓冲区里的数据拷贝到 socket 缓冲区的过程。
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210525152933698.png?x-os-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L05vX0dhbWVfTm9fTGlmZV8=,size_16,color_FFFFFF,t_70)
这就是所谓的零拷贝(Zero-copy)技术,因为我们没有在内存层面去拷贝数据,也就是说全程没有通过 CPU 来搬运数据,所有的数据都是通过 DMA 来进行传输的。
零拷贝技术的文件传输方式相比传统文件传输的方式,减少了 2 次上下文切换和数据拷贝次数,只需要 2 次上下文切换和数据拷贝次数,就可以完成文件的传输,而且 2 次的数据拷贝过程,都不需要通过 CPU,2 次都是由 DMA 来搬运。
使用零拷贝技术的项目
如果你追溯 Kafka 文件传输的代码,你会发现,最终它调用了 Java NIO 库里的 transferTo 方法:
@Overridepublic
long transferFrom(FileChannel fileChannel, long position, long count) throws IOException {
return fileChannel.transferTo(position, count, socketChannel);
}
如果 Linux 系统支持 sendfile() 系统调用,那么 transferTo() 实际上最后就会使用到 sendfile() 系统调用函数。
另外,Nginx 也支持零拷贝技术,一般默认是开启零拷贝技术,这样有利于提高文件传输的效率,是否开启零拷贝技术的配置如下:
http {
...
sendfile on
...
}
sendfile 配置的具体意思:
设置为 on 表示,使用零拷贝技术来传输文件:sendfile ,这样只需要 2 次上下文切换,和 2 次数据拷贝。
设置为 off 表示,使用传统的文件传输技术:read + write,这时就需要 4 次上下文切换,和 4 次数据拷贝。