Kafka为什么这么快 sequence IO、PageCache、SendFile的应用详解

kafka作为MQ也好,作为存储层也好,无非是两个重要功能,一是Producer生产的数据存到broker,二是 Consumer从broker读取数据;我们把它简化成如下两个过程:

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

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

大家都知道Kafka是将数据存储于磁盘的,而磁盘读写性能往往很差,但Kafka官方测试其数据读写速率能达到600M/s,那么为什么Kafka性能会这么高呢?

首先producer往broker发送消息时,采用batch的方式即批量而非一条一条的发送,这种方式可以有效降低网络IO的请求次数,提升性能。此外这些批次消息会"暂存"在缓冲池中,避免频繁的GC问题。批量发送的消息可以进行压缩并且传输的时候可以进行高效的序列化,从而减少数据大小。

Kafka除了在producer发送消息方面做了很多优化,还有很多其他的优化,比如Kafka利用了sequence IO、PageCache、SendFile这3种处理方案:

sequence IO

首先来了解一下磁盘的特性:快速顺序读写、慢速随机读写。因为磁盘是典型的IO块设备,每次读写都会经历寻址,其中寻址中寻道是比较耗时的。随机读写会导致寻址时间延长,从而影响磁盘的读写速度。

关于磁盘I/O的性能,引用一组Kafka官方给出的测试数据

Sequence I/O: 600MB/s

Random I/O: 100KB/s

kafka将来自Producer的数据,顺序追加在partition,partition就是一个文件,以此实现顺序写入。 Consumer从broker读取数据时,因为自带了偏移量,接着上次读取的位置继续读,以此实现顺序读。 顺序读写,是kafka利用磁盘特性的一个重要体现。

大家有没有想过MapReduce进行shuffle的时候,为什么map端和reduce端要进行排序,不排序不也不影响正常业务的处理,排序反而因为消耗资源增加了处理时间?

以map端为例,执行过程中会产生很多小文件,这些小文件要经历归并排序等一系列处理后才会被reduce端进行处理。提前对未合并的文件进行排序正是利用了磁盘快速顺序读写的特性来提高归并排序的速度。

而Kafka在将数据持久化到磁盘时,采用只追加的顺序写,有效降低了寻址时间,提高效率。下图展示了Kafka写入数据到partition的方式:
在这里插入图片描述
可以看到Kafka会将数据插入到文件末尾,并且Kafka不会"直接"删除数据,而是把所有数据保存到磁盘,每个consumer会指定一个offset来记录自己订阅的topic的partition中消费的位置。当然我们可以设置策略来清理数据,比如通过参数log.retention.hours指定过期时间,当达到过期时间时,Kafka会清理数据。

PageCache

PageCache是系统级别的缓存,它把尽可能多的空闲内存当作磁盘缓存使用来进一步提高IO效率,同时当其他进程申请内存,回收PageCache的代价也很小。

当上层有写操作时,操作系统只是将数据写入PageCache,同时标记Page属性为Dirty。当读操作发生时,先从PageCache中查找,如果发生缺页才进行磁盘调度,最终返回需要的数据。

PageCache同时可以避免在JVM内部缓存数据,避免不必要的GC、以及内存空间占用。对于In-Process Cache,如果Kafka重启,它会失效,而操作系统管理的PageCache依然可以继续使用。

对应到Kafka生产和消费消息中:

producer把消息发到broker后,数据并不是直接落入磁盘的,而是先进入PageCache。PageCache中的数据会被内核中的处理线程采用同步或异步的方式写回到磁盘。

Consumer消费消息时,会先从PageCache获取消息,获取不到才回去磁盘读取,并且会预读出一些相邻的块放入PageCache,以方便下一次读取

如果Kafka producer的生产速率与consumer的消费速率相差不大,那么几乎只靠对broker PageCache的读写就能完成整个生产和消费过程,磁盘访问非常少。

SendFile (零拷贝)

数据直接在内核完成输入和输出,不需要拷贝到用户空间再写出去。 kafka数据写入磁盘前,数据先写到进程的内存空间。

传统的网络I/O过程:
1.操作系统从磁盘把数据读到内核区

2.用户进程把数据从内核区copy到用户区

3.然后用户进程再把数据写入到socket,数据流入内核区的Socket Buffer上

4.最后把数据从socket Buffer中发送到到网卡,这样完成一次发送

可以发现,同一份数据在内核Buffer与用户Buffer之间拷贝两次:

在这里插入图片描述
但是通过SendFile(又称zero copy)优化后,直接把数据从内核区copy到socket,然后发送到网卡,避免了在内核Buffer与用户Buffer来回拷贝的弊端:
在这里插入图片描述
不仅是Kafka,Java的NIO提供的FileChannle,它的transferTo、transferFrom方法也利用了这种在内核区完成数据传输的功能。

Memory Mapped Files(mmap文件映射)

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

简称mmap,简单描述其作用就是:将磁盘文件映射到内存, 用户通过修改内存就能修改磁盘文件。 它的工作原理是直接利用操作系统的Page来实现文件到物理内存的直接映射。完成映射之后你对物理内存的操作会被同步到硬盘上(操作系统在适当的时候),写到mmap中的数据并没有被真正的写到硬盘,操作系统会在程序主动调用flush的时候才把数据真正的写到硬盘。。

通过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。

Zero Copy对应的是sendfile这个函数(以Linux为例),这个函数接受

  • out_fd作为输出(一般及时socket的句柄)
  • in_fd作为输入文件句柄
  • off_t表示in_fd的偏移(从哪里开始读取)
  • size_t表示读取多少个

没错,Kafka是用mmap作为文件读写方式的,它就是一个文件句柄,所以直接把它传给sendfile;偏移也好解决,用户会自己保持这个offset,每次请求都会发送这个offset。(还记得吗?放在zookeeper中的);数据量更容易解决了,如果消费者想要更快,就全部扔给消费者。如果这样做一般情况下消费者肯定直接就被压死了;所以Kafka提供了的两种方式——Push,我全部扔给你了,你死了不管我的事情;Pull,好吧你告诉我你需要多少个,我给你多少个。

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

通过以上的介绍不难看出,Kafka的设计初衷是尽一切努力在内存中完成数据交换,无论是对外作为一整个消息系统,或是内部同底层操作系统的交互。如果Producer和Consumer之间生产和消费进度上配合得当,完全可以实现数据交换零I/O。这也就是为什么说Kafka使用”磁盘”并没有带来过多性能损失的原因。下面是在生产环境中采到的一些指标(20 Brokers, 75 Partitions per Broker, 110k msg/s)。 此时的集群只有写,没有读操作。10M/s左右的Send的流量是Partiton之间进行Replicate而产生的。从recv和writ的速率比较可以看出,写盘是使用Asynchronous+Batch的方式,底层OS可能还会进行磁盘写顺序优化。而在有Read Request进来的时候分为两种情况,第一种是内存中完成数据交换。 Send流量从平均10M/s增加到了平均60M/s,而磁盘Read只有不超过50kb/s。PageCache降低磁盘I/O的效果非常明显。 接下来读一些收到了一段时间,已经从内存中被交换出刷写到磁盘上的老数据。 其他指标还是原样,而磁盘Read已经飙高到40+MB/s。此时全部的数据都已经是走磁盘了(对磁盘的顺序读取OS层会进行Prefill PageCache的优化)。依然没有任何性能问题。

Kafka总结

总的来说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将磁盘文件映射到内存,支持读和写,对内存的操作会反映在磁盘文件上。
RocketMQ 在消费消息时,使用了 mmap。kafka 使用了 sendFile。

一个具有注脚的文本。1


  1. 参考 https://www.jianshu.com/p/1c27da322767 ↩︎

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值