kafka(3) kafka那么快-----‘零拷贝’ 技术

目录

先说说零拷贝

聊聊传统IO流程

为什么Kafka这么快?

下面我们来重点探究 kafka两个重要过程、以及是如何利用两个零拷贝技术sendfile和mmap的。

网络数据持久化到磁盘 (Producer 到 Broker)

磁盘文件通过网络发送(Broker 到 Consumer)

总结Kafka快的原因

mmap 和 sendfile总结


首先要有个概念,kafka高性能的背后,是多方面协同后、最终的结果。kafka从宏观架构、分布式partition存储、ISR数据同步、以及“无孔不入”的高效利用磁盘/操作系统特性,这些多方面的协同,是kafka成为性能之王的必然结果。

Kafka提高性能的方式除了消息顺序追加页缓存等技术,Kafka 还使用零拷贝技术来进一步提升性能。

先说说零拷贝

零拷贝并不是不需要拷贝,而是减少不必要的拷贝次数。通常是说在IO读写过程中

实际上,零拷贝是有广义和狭义之分,目前我们通常听到的零拷贝,包括上面这个定义减少不必要的拷贝次数都是广义上的零拷贝。其实了解到这点就足够了。

我们知道,减少不必要的拷贝次数,就是为了提高效率。那零拷贝之前,是怎样的呢?

所谓的零拷贝是指将数据直接从磁盘文件复制到网卡设备中,而不需要经由应用程序之手。零拷贝大大提高了应用程序的性能,减少了内核和用户模式之间的上下文切换。对 Linux 操作系统而言,零拷贝技术依赖于底层的 sendfile() 方法实现。对应于 Java 语言,FileChannal.transferTo() 方法的底层实现就是 sendfile() 方法。
单纯从概念上理解“零拷贝”比较抽象,这里简单地介绍一下它。

聊聊传统IO流程

考虑这样一种常用的情形:你需要将静态内容(类似图片、文件)展示给用户。这个情形就意味着需要先将静态内容从磁盘中复制出来放到一个内存 buf 中,然后将这个 buf 通过套接字(Socket)传输给用户,进而用户获得静态内容。这看起来再正常不过了,但实际上这是很低效的流程,我们把上面的这种情形抽象成下面的过程:

read(file, tmp_buf, len);
 
write(socket, tmp_buf, len);

首先调用 read() 将静态内容(这里假设为文件 A )读取到 tmp_buf,然后调用 write() 将 tmp_buf 写入 Socket,如下图所示。

在这个过程中,文件 A 经历了4次复制的过程:

  1. 将磁盘文件,读取到操作系统内核缓冲区。调用 read() 时,文件 A 中的内容被copy复制到了内核模式下的 Read Buffer 中。
  2. 将内核缓冲区的数据,copy到application应用程序的buffer。CPU 控制将内核模式数据复制到用户模式下。
  3. 将application应用程序buffer中的数据,copy到socket网络发送缓冲区(属于操作系统内核的缓冲区)。调用 write() 时,将用户模式下的内容复制到内核模式下的 Socket Buffer 中。
  4. 将内核模式下的 Socket Buffer 的数据copy复制到网卡设备中,由网卡进行网络传输。

从上面的过程可以看出,数据平白无故地从内核模式到用户模式“走了一圈”,浪费了2次复制过程:第一次是从内核模式复制到用户模式;第二次是从用户模式再复制回内核模式,即上面4次过程中的第2步和第3步。而且在上面的过程中,内核和用户模式的上下文的切换也是4次。

如果采用了零拷贝技术,那么应用程序可以直接请求内核把磁盘中的数据传输给 Socket,如下图所示。

零拷贝技术通过 DMA(Direct Memory Access)技术将文件内容复制到内核模式下的 Read Buffer 中。不过没有数据被复制到 Socket Buffer,相反只有包含数据的位置和长度的信息的文件描述符被加到 Socket Buffer 中。DMA 引擎直接将数据从内核模式中传递到网卡设备(协议引擎)。这里数据只经历了2次复制就从磁盘中传送出去了,并且上下文切换也变成了2次。零拷贝是针对内核模式而言的,数据在内核模式下实现了零拷贝。

下面介绍下DMA技术

DMA(Direct Memory Access,直接内存存取) 是所有现代电脑的重要特色,它的出现就是为了解决批量数据的输入/输出问题。它允许不同速度的硬件装置来沟通,而不需要依赖于 CPU 的大量中断负载。否则,CPU 需要从来源把每一片段的资料复制到暂存器,然后把它们再次写回到新的地方。在这个时间中,CPU 对于其他的工作来说就无法使用。

传统的内存访问,所有的请求都会发送到 CPU ,然后再由 CPU 来完成相关调度工作。如下图所示:

 当 DMA 技术的出现,数据文件在各个层之间的传输,则可以直接绕过CPU,使得外围设备可以通过DMA控制器直接访问内存。与此同时,CPU可以继续执行程序。如下图:

在现代电脑中,很多硬件都是支持 DMA 技术的,这里面其中就包括我们此处用到的网卡。还有其他硬件也都是支持 DMA 技术的,例如:磁盘、显卡、声卡等其他硬件。  有了 DMA 技术的,通过网卡直接去访问系统的内存,就可以实现现绝对的零拷贝了

----补充点1------

为什么Kafka这么快?

kafka作为MQ也好,作为存储层也好,无非是两个重要功能,一是Producer生产的数据存到broker,二是 Consumer从broker读取数据。我们把它简化成如下两个过程:
1、网络数据持久化到磁盘 (Producer 到 Broker)
2、磁盘文件通过网络发送(Broker 到 Consumer)

下面,先给出“kafka用了磁盘,还速度快”的结论

1、顺序读写
磁盘顺序读或写的速度400M/s,能够发挥磁盘最大的速度。
随机读写,磁盘速度慢的时候十几到几百K/s。这就看出了差距。
kafka将来自Producer的数据,顺序追加在partition,partition就是一个文件,以此实现顺序写入。
Consumer从broker读取数据时,因为自带了偏移量,接着上次读取的位置继续读,以此实现顺序读。
顺序读写,是kafka利用磁盘特性的一个重要体现。

2、mmap文件映射
虚拟映射只支持文件;
在进程 的非堆内存开辟一块内存空间,和OS内核空间的一块内存进行映射
kafka数据写入、是写入这块内存空间,但实际这块内存和OS内核内存有映射,也就是相当于写在内核内存空间了。且这块内核空间,内核直接能够访问到,直接落入磁盘。
这里,我们需要清楚的是:内核缓冲区的数据,flush就能完成落盘 

3、零拷贝 sendfile(in,out)

数据直接在内核完成输入和输出,不需要拷贝到用户空间再写出去。

下面我们来重点探究 kafka两个重要过程、以及是如何利用两个零拷贝技术sendfile和mmap的。

网络数据持久化到磁盘 (Producer 到 Broker)

传统方式实现:

data = socket.read()// 读取网络数据 
File file = new File() 
file.write(data)// 持久化到磁盘 
file.flush()

先接收生产者发来的消息,再落入磁盘。
实际会经过四次copy,如下图的四个箭头。

数据落盘通常都是非实时的,kafka生产者数据持久化也是如此。Kafka的数据并不是实时的写入硬盘,它充分利用了现代操作系统分页存储来利用内存提高I/O效率。

对于kafka来说,Producer生产的数据存到broker,这个过程读取到socket buffer的网络数据,其实可以直接在OS内核缓冲区,完成落盘。并没有必要将socket buffer的网络数据,读取到应用进程缓冲区。

在此特殊场景下:接收来自socket buffer的网络数据,应用进程不需要中间处理、直接进行持久化时。——可以使用mmap内存文件映射

 Memory Mapped Files

简称mmap,简单描述其作用就是:将磁盘文件映射到内存, 用户通过修改内存就能修改磁盘文件。
它的工作原理是直接利用操作系统的Page来实现文件到物理内存的直接映射。完成映射之后你对物理内存的操作会被同步到硬盘上(操作系统在适当的时候)。

通过mmap,进程像读写硬盘一样读写内存(当然是虚拟机内存),也不必关心内存的大小有虚拟内存为我们兜底。
使用这种方式可以获取很大的I/O提升,省去了用户空间到内核空间复制的开销

mmap也有一个很明显的缺陷——不可靠,写到mmap中的数据并没有被真正的写到硬盘,操作系统会在程序主动调用flush的时候才把数据真正的写到硬盘。Kafka提供了一个参数——producer.type来控制是不是主动flush;如果Kafka写入到mmap之后就立即flush然后再返回Producer叫同步(sync);写入mmap之后立即返回Producer不调用flush叫异步(async)。

Java NIO对文件映射的支持

Java NIO,提供了一个 MappedByteBuffer 类可以用来实现内存映射。
MappedByteBuffer只能通过调用FileChannel的map()取得,再没有其他方式。
FileChannel.map()是抽象方法,具体实现是在 FileChannelImpl.c 可自行查看JDK源码,其map0()方法就是调用了Linux内核的mmap的API。

使用 MappedByteBuffer类要注意的是:mmap的文件映射,在full gc时才会进行释放。当close时,需要手动清除内存映射文件,可以反射调用sun.misc.Cleaner方法。

磁盘文件通过网络发送(Broker 到 Consumer)

传统方式实现:
先读取磁盘、再用socket发送,实际也是进过四次copy(如上述文章内容)。

buffer = File.read 
Socket.send(buffer)

而 Linux 2.4+ 内核通过 sendfile 系统调用,提供了零拷贝。磁盘数据通过 DMA 拷贝到内核态 Buffer 后,直接通过 DMA 拷贝到 NIC Buffer(socket buffer),无需 CPU 拷贝。这也是零拷贝这一说法的来源。除了减少数据拷贝外,因为整个读文件 - 网络发送由一个 sendfile 调用完成,整个过程只有两次上下文切换,因此大大提高了性能。零拷贝过程如下图所示。

相比于对传统IO 4步拷贝的分析,sendfile将第二次、第三次拷贝,一步完成。

其实这项零拷贝技术,直接从内核空间(DMA的)到内核空间(Socket的)、然后发送网卡。
应用的场景非常多,如Tomcat、Nginx、Apache等web服务器返回静态资源等,将数据用网络发送出去,都运用了sendfile。
简单理解 sendfile(in,out)就是,磁盘文件读取到操作系统内核缓冲区后、直接扔给网卡,发送网络数据。

Java NIO对sendfile的支持

就是FileChannel.transferTo()/transferFrom()。
fileChannel.transferTo( position, count, socketChannel);
把磁盘文件读取OS内核缓冲区后的fileChannel,直接转给socketChannel发送;底层就是sendfile。消费者从broker读取数据,就是由此实现。 

 具体来看,Kafka 的数据传输通过 TransportLayer 来完成,其子类 PlaintextTransportLayer 通过Java NIO 的 FileChannel 的 transferTo 和 transferFrom 方法实现零拷贝。

@Override
public long transferFrom(FileChannel fileChannel, long position, long count) throws IOException {
   return fileChannel.transferTo(position, count, socketChannel);
}

注: transferTo 和 transferFrom 并不保证一定能使用零拷贝。实际上是否能使用零拷贝与操作系统相关,如果操作系统提供 sendfile 这样的零拷贝系统调用,则这两个方法会通过这样的系统调用充分利用零拷贝的优势,否则并不能通过这两个方法本身实现零拷贝。

总结Kafka快的原因

总的来说:
1、partition顺序读写,充分利用磁盘特性,这是基础;
2、Producer生产的数据持久化到broker,采用mmap文件映射,实现顺序的快速写入;
3、Customer从broker读取数据,采用sendfile,将磁盘文件读到OS内核缓冲区后,直接转到socket buffer进行网络发送。

mmap 和 sendfile总结

1、都是Linux内核提供、实现零拷贝的API;
2、sendfile 是将读到内核空间的数据,转到socket buffer,进行网络发送;
3、mmap将磁盘文件映射到内存,支持读和写,对内存的操作会反映在磁盘文件上。
 

参考文章:

Kafka零拷贝 - 知乎

猜你喜欢

转载自blog.csdn.net/CoderTnT/article/details/121062197