写在前面
这个问题经常在面试中出现,本文将详细介绍它的底层原理
零拷贝是什么意思
零拷贝并不是不需要拷贝,而是减少不必要的拷贝次数。通常是说在 IO 读写过程中。
实际上,零拷贝是有广义和狭义之分,目前我们通常听到的零拷贝,包括上面这个定义减少不必要的拷贝次数都是广义上的零拷贝。其实了解到这点就足够了。
我们知道,减少不必要的拷贝次数,就是为了提高效率。那零拷贝之前,是怎样的呢?
聊聊传统 IO 流程
比如:读取文件,再用 socket 发送出去。
传统方式实现:
先读取、再发送,实际经过 1~4 四次 copy。
分别是:
- 第一次:将磁盘文件,读取到操作系统内核缓冲区;
- 第二次:将内核缓冲区的数据,copy 到 application 应用程序的 buffer;
- 第三步:将 application 应用程序 buffer 中的数据,copy 到 socket 网络发送缓冲区(属于操作系统内核的缓冲区);
- 第四次:将 socket buffer 的数据,copy 到网卡,由网卡进行网络传输。
传统方式,读取磁盘文件并进行网络发送,经过的四次数据 copy 是非常繁琐的。实际 IO 读写,需要进行 IO 中断,需要 CPU 响应中断(带来上下文切换),尽管后来引入 DMA 来接管 CPU 的中断请求,但四次 copy 是存在“不必要的拷贝”的。
kafka零拷贝
kafka 作为 MQ 也好,作为存储层也好,无非是两个重要功能,一是 Producer 生产的数据存到 broker,二是 Consumer 从 broker 读取数据;我们把它简化成如下两个过程:
- 网络数据持久化到磁盘 (Producer 到 Broker)
- 磁盘文件通过网络发送(Broker 到 Consumer)
数据写入
Kafka 会把收到的消息都写入到硬盘中,它绝对不会丢失数据。为了优化写入速度 Kafka 采用了两个技术, 顺序写入和 MMFile(Memory Mapped File)。
顺序写入
磁盘读写的快慢取决于你怎么使用它,也就是顺序读写或者随机读写。在顺序读写的情况下,磁盘的顺序读写速度和内存持平。
因为硬盘是机械结构,每次读写都会寻址->写入,其中寻址是一个“机械动作”,它是最耗时的。
所以硬盘最讨厌随机 I/O,最喜欢顺序 I/O。为了提高读写硬盘的速度,Kafka 就是使用顺序 I/O。
而且 Linux 对于磁盘的读写优化也比较多,包括 read-ahead 和 write-behind,磁盘缓存等。
MMFile
即便是顺序写入硬盘,硬盘的访问速度还是不可能追上内存。所以 Kafka 的数据并不是实时的写入硬盘 ,它充分利用了现代操作系统分页存储来利用内存提高 I/O 效率。
Memory Mapped Files(后面简称 mmap)也被翻译成内存映射文件 ,在 64 位操作系统中一般可以表示 20G 的数据文件,它的工作原理是直接利用操作系统的 Page 来实现文件到物理内存的直接映射。
完成映射之后你对物理内存的操作会被同步到硬盘上(操作系统在适当的时候)。
通过 mmap,进程像读写硬盘一样读写内存(当然是虚拟机内存),也不必关心内存的大小,有虚拟内存为我们兜底。
使用这种方式可以获取很大的 I/O 提升,省去了用户空间到内核空间复制的开销。(调用文件的 Read 会把数据先放到内核空间的内存中,然后再复制到用户空间的内存中)
但也有一个很明显的缺陷——不可靠,写到 mmap 中的数据并没有被真正的写到硬盘,操作系统会在程序主动调用 Flush 的时候才把数据真正的写到硬盘。
Kafka 提供了一个参数 producer.type 来控制是不是主动 Flush:
如果 Kafka 写入到 mmap 之后就立即 Flush,然后再返回 Producer 叫同步 (Sync)。
如果 Kafka 写入 mmap 之后立即返回 Producer 不调用 Flush 叫异步 (Async)。
sendfile
数据写入只是kafka优化数据持久化的速度,跟零拷贝没有关系,零拷贝主要是通过操作系统提供的sendfile系统调用实现的。
Linux 2.4+ 内核通过 sendfile 系统调用,提供了零拷贝。磁盘数据通过 DMA(Direct Memory Access) 拷贝到内核态 Buffer 后,直接通过 DMA 拷贝到 NIC Buffer(socket buffer),无需 CPU 拷贝。这也是零拷贝这一说法的来源。除了减少数据拷贝外,因为整个读文件 - 网络发送由一个 sendfile 调用完成,整个过程只有两次上下文切换,因此大大提高了性能。零拷贝过程如下图所示。
运行流程如下:
- Sendfile 系统调用,文件数据被 Copy 至内核缓冲区。
- 再从内核缓冲区 Copy 至内核中 Socket 相关的缓冲区。
- 最后再 Socket 相关的缓冲区 Copy 到协议引擎。
相较传统 Read/Write 方式,2.1 版本内核引进的 Sendfile 已经减少了内核缓冲区到 User 缓冲区,再由 User 缓冲区到 Socket 相关缓冲区的文件 Copy。
而在内核版本 2.4 之后,文件描述符结果被改变,Sendfile 实现了更简单的方式,再次减少了一次 Copy 操作。
Kafka 把所有的消息都存放在一个一个的文件中,当消费者需要数据的时候 Kafka 直接把文件发送给消费者,配合 mmap 作为文件读写方式,直接把它传给 Sendfile。
可见零拷贝减少了系统内核到cpu的拷贝。并且从文件到系统内核的拷贝利用了mmap技术,加快了速度。因此可以实现快速文件通过网络发送。
总结
kafka 总结
- partition 顺序读写,充分利用磁盘特性,这是基础;
- Producer 生产的数据持久化到 broker,采用 mmap 文件映射,实现顺序的快速写入;
- Customer 从 broker 读取数据,采用 sendfile,将磁盘文件读到 OS 内核缓冲区后,直接转到 socket buffer 进行网络发送。
mmap 和 sendfile 总结
- 都是 Linux 内核提供、实现零拷贝的 API;
- sendfile 是将读到内核空间的数据,转到 socket buffer,进行网络发送;
- mmap 将磁盘文件映射到内存,支持读和写,对内存的操作会反映在磁盘文件上。