RocketMQ中的零拷贝

零拷贝

什么是零拷贝?
所谓零拷贝就是避免数据在内核空间缓冲区和用户空间缓冲区之间的复制,避免CPU拷贝对CPU资源的消耗。

零拷贝的两种实现方式:

  • mmap + write方式
    优点:即使频繁调用,使用小块文件传输,效率也很高。
    缺点:不能很好的利用DMA方式,会比sendfile多消耗 CPU,内存安全性控制复杂,需要避免JVM Crash问题。
  • sendfile方式

    优点:可以利用DMA方式,消耗CPU较少,大块文件传输效率高,无内存安全性问题。
    缺点:小块文件效率低于mmap方式,只能是BIO方式传输,不能使用NIO。

RocketMQ零拷贝:使用mmap+write的方式;
Kafka的索引文件使用mmap+write方式,data文件使用sendfile方式。

下面我们从传统的服务器发送文件数据至客户端的过程,来了解下为什么要使用零拷贝。

服务器发送文件数据至客户端

一台服务器把本机磁盘文件发送到客户端,一般分为两步步骤:
read:读取本地文件数据;
write:将读卡的文件数据通过网络发送出去;

这两个操作发生了两次系统调用,每次系统调用都得先从用户态切换到内核态,等内核完成任务后,再从内核态切换回用户态。也就是消息发送过程中一共发生了4次用户态和内核态的上下文切换;另外发生了4次数据拷贝,其中两次DMA的拷贝,两次CPU拷贝。
如下图示:

具体步骤:
(1)DMA把数据从磁盘拷贝到内核态缓冲区;
(2)CPU把数据从内核态缓冲区拷贝到用户缓冲区;
(3)CPU把数据从用户缓冲区拷贝到内核的网络驱动的socket缓冲区;
(4)DMA把数据从网络驱动的socket缓冲区拷贝到网卡的缓冲区中;

在数据发送的过程中,数据发生了4次拷贝,过多的数据拷贝存在冗余的上下文切换和数据拷贝,无疑会消耗CPU资源,大大降低了系统性能。这在高并发系统里是非常糟糕的。

所以,要想提高文件传输的性能,就需要减少用户态和内核态的上下文切换和内存拷贝的次数。

通过上面的分析,可以看出,第2,3次拷贝(也就是从内核空间到用户空间的来回复制)是没有意义的,数据应该可以直接从内核缓冲区直接拷贝至Socket缓冲区,零拷贝机制就实现了这一点。

零拷贝的两种实现方式:

  • mmap + write
  • sendfile

mmap + write方式

mmap()系统调用函数在调用进程的虚拟地址空间中创建一个新映射。这个映射会直接把内核缓冲区里的数据映射到用户空间,这样就不用从内核空间到用户空间来回复制数据了。
将对文件的操作转换为对内存地址进行操作,从而极大的提高了文件的读写效率。

发送过程变为如下所示:

rocketmq-mmap-write.jpgrocketmq-mmap-write.jpg

那么之前的过程就变为:
(1)应用进程调用mmap(),DMA将数据从磁盘拷贝到内核缓冲区;
(2)应用进程调用write(),CPU直接将内核缓冲区中的数据拷贝到socket缓冲区;
(3)DMA把数据从socket缓冲区拷贝到网卡的缓冲区;

通过上面的分析,我们发现比起原过程,mmap+write的方式依然需要4次用户态和内核态的上下文切换,但是少了一次内存拷贝。

sendfile方式

通过使用sendfile(),数据可以直接在内核空间进行传输,因此避免了用户空间和内核空间之间来回复制拷贝,同时由于使用sendfile()替代了read+write从而节省了一次系统调用,也就是2次用户态与内核态的上下文切换。

如下图示:

rocketmq-sendfile.jpgrocketmq-sendfile.jpg

整个过程发生了2次用户态与内核态的上下文切换和3次内存拷贝,具体流程如下:
1、应用进程调用sendfile(),DMA控制器把数据从磁盘拷贝到读缓冲区,上下文从用户态切换到内核态;
2、CPU将数据从内核缓冲区拷贝到socket缓冲区;
3、DMA把数据从socket缓冲区拷贝到网卡的缓冲区,上下文从内核态切换回用户态,sendfile()调用返回。

如果网卡支持SG-DMA技术(和普通的DMA不同),我们可以进一步减少通过CPU把内核缓冲区中的数据拷贝到socket缓冲区的过程。

从Linux内核2.4版本开始,在网卡支持SG-DMA技术的情况下,sendfile()系统调用的过程发生了点变化,具体过程如下:

rocketmq-sendfile-sgdma.jpgrocketmq-sendfile-sgdma.jpg

1、DMA将磁盘上的数据拷贝到内核缓冲区;
2、缓冲区描述符和数据长度传到socket缓冲区,这样网卡的SG-DMA控制器就可以直接将内核缓冲区中的数据拷贝到网卡的缓冲区,此过程不需要将数据从内核缓冲区拷贝到socket缓冲区,这样就减少了一次数据拷贝。

也就是说变化后为2次用户态与内核态的上下文切换和2次内存拷贝。

最后修改于:2022年11月27日 12:09

添加新评论