kafka为什么这么快

目前kafka是应用比较火的消息队列,都说kafka吞吐量大读写快,那到底是怎么快的。这里记录下不断学习的过程。

1 kafka的架构

kafka架构
相关的术语:
Broker:每一个kafka实例(或者说每台kafka服务器节点)就是一个broker,一个broker可以有多个topic
Topic:消息根据topic进行归类,topic其本质是一个目录,即将同一主题消息归类到同一个目录
Partition:Partititon指物理上的分区,每个topic包含多个partititon
Producer:消息生产者,产生的消息将会被发送到某个topic
Consumer:消息消费者,消费的消息内容来自某个topic
Consumer Group:每个consumer属于consumer group

2 拓扑结构

拓扑结构
Kafka 通过 Zookeeper 管理集群配置,选举 leader,以及在 Consumer Group 发生变化时进行 rebalance。
Producer 使用 push 模式将消息发布到 broker,Consumer 使用 pull 模式从 broker 订阅并消费消息。

3 kafka为什么快

kafka是用scala编写的同时支持离线数据和实时数据处理的分布式的、基于发布订阅的消息系统。

3.1 kafka的生产者

生产者(producer)是负责向Kafka提交数据的,Kafka会把收到的消息都写入到硬盘中,它绝对不会丢失数据。为了优化写入速度Kafak采用了两个技术,顺序写入和MMFile

3.1.1 顺序写入

因为硬盘是机械结构,每次读写都会寻址→写入,其中寻址是一个“机械动作”,它是最耗时的。所以硬盘最“讨厌”随机I/O,最喜欢顺序I/O。为了提高读写硬盘的速度,Kafka就是使用顺序I/O。
顺序写入
上图就展示了Kafka是如何写入数据的,每一个Partition其实都是一个文件,收到消息后Kafka会把数据插入到文件末尾。
但是这种顺序写入有个缺点:无法删除。所以Kafka是不会删除数据的,它会把所有的数据都保留下来,每个消费者(Consumer)对每个Topic都有一个offset用来表示读取到了第几条数据。
消费者组
上图中有两个消费者,Consumer1有两个offset分别对应Partition0、Partition1(假设每一个Topic一个Partition);Consumer2有一个offset对应Partition2。这个offset是由客户端SDK负责保存的,Kafka的Broker完全无视这个东西的存在;一般情况下SDK会把它保存到zookeeper里面。(所以需要给Consumer提供zookeeper的地址)。
长时间下去硬盘也会满,针对硬盘的也有两种删除策略:基于时间和基于partition文件大小。

3.1.2 Memory Mapped Files

硬盘的IO读写速度是不如内存的,因此即使顺序写磁盘,硬盘的访问速度还是略慢。kafka的数据并不是实时写入硬盘的,它是通过计算机系统的分页存储来利用内存提高IO效率的。
Memory Mapped Files(后面简称mmap)也被翻译成内存映射文件,在32位操作系统中一般可以表示4G的数据文件,它的工作原理是直接利用操作系统的Page来实现文件到物理内存的直接映射。完成映射之后你对物理内存的操作会被同步到硬盘上。
通过内存映射mmap技术,进程读写内存的时候不需关注内存的大小,因为这时候是读写的虚拟内存。
使用这种方式可以获取很大的I/O提升,省去了用户空间到内核空间复制的开销(调用文件的read会把数据先放到内核空间的内存中,然后再复制到用户空间的内存中)。
但是也有一个很明显的缺陷——不可靠,写到mmap中的数据并没有被真正的写到硬盘,操作系统会在程序主动调用flush的时候才把数据真正的写到硬盘。
Kafka提供了一个参数——producer.type来控制是不是主动flush,如果Kafka写入到mmap之后就立即flush然后再返回Producer叫同步(sync);写入mmap之后立即返回Producer不调用flush叫异步(async)。
在linux中的有函数mmap来实现内存映射,在java中也有mappedbytebuffer类来实现内存映射。关于内存映射可以单独细述。

3.2 kafka消费者的零拷贝

消费者读取文件数据,kafka写磁盘文件,通常情况速度应该比内存慢,实际上kafka中关于读取数据有关键技术:Zero Copy
首先可用看下传统的数据拷贝(读取)过程:
四次拷贝
一共产生了四次数据拷贝,即使使用DMA来处理硬件之间的通信,也有两次CPU的数据拷贝过程,同时还有用户态和内核态之间的上下文切换。
DMA是一种无序CPU参与就可让外设和系统内存之间进行双向数据传输的硬件机制。
实际的数据读取过程,根本没修改文件的任何内容,发生的上下文切换和copy过程都是不必要的,那么最主要的就是不要进入用户态。
这里可以讨论下linux的零拷贝的几种做法

3.2.1 mmap减少拷贝次数

我们减少拷贝次数的一种方法是调用mmap()来代替read调用:
应用程序调用mmap(),磁盘上的数据会通过DMA被拷贝的内核缓冲区,接着操作系统会把这段内核缓冲区与应用程序共享,这样就不需要把内核缓冲区的内容往用户空间拷贝。应用程序再调用write(),操作系统直接将内核缓冲区的内容拷贝到socket缓冲区中,这一切都发生在内核态,最后,socket缓冲区再把数据发到网卡去。
mmap
mmap替代read很明显减少了一次拷贝,当拷贝数据量很大时,无疑提升了效率。但是使用mmap是有代价的。
当你使用mmap时,你可能会遇到一些隐藏的陷阱。例如,当你的程序map了一个文件,但是当这个文件被另一个进程截断(truncate)时, write系统调用会因为访问非法地址而被SIGBUS信号终止。SIGBUS信号默认会杀死你的进程并产生一个coredump,如果你的服务器这样被中止了,产生不可预期的损失。
解决办法:为SIGBUS信号增加信号处理程序
使用文件租借锁

3.2.2 使用sendfile(kafka使用方式)

内核2.1版本开始,linux支持使用sendfile来简化操作步骤,kafka也是利用该技术实现零拷贝

1
2
#include<sys/sendfile.h>
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);

参数解释:
out_fd:输出文件描述符
in_fd:输入文件描述符
offset:in_fd的偏移值,表示从何处开始读取
count:代表读取的内容大小
这里也有一些限制:in_fd指向的文件必须是可以mmap的
out_map必须指向一个套接字
这也说明了sendfile的方向是单向的,只能从mmap的文件传输数据到套接字上。
所以用户态和内核的数据拷贝传输过程如下:
sendfile系统调用过程
上面过程仍然存在一次拷贝,就是页缓存到socket缓存的拷贝。实际上,我们仅仅需要把缓冲区描述符传到socket缓冲区,再把数据长度传过去,这样DMA控制器直接将页缓存中的数据打包发送到网络中就可以了。
所以sendfile系统调用利用DMA引擎将文件内容拷贝到内核缓冲区去,然后将带有文件位置和长度信息的缓冲区描述符添加socket缓冲区去,这一步不会将内核中的数据拷贝到socket缓冲区中,
DMA引擎会将内核缓冲区的数据拷贝到协议引擎中去,避免了最后一次拷贝。
sendfile系统调用过程1
当然kafka针对数据的读取有两种方式:pull和push。
目前实现零拷贝的技术还有很多,比如linux 2.6.17开始支持使用splice系统调用,有兴趣的可以看参考链接。

4 kafka总结

总的来说kafka快的原因:mmap技术提高操作磁盘IO速度,写文件是末尾顺序写入,速度快;
读文件sendfile实现零拷贝。

参考链接:https://www.infoq.cn/article/kafka-analysis-part-1
https://toutiao.io/posts/508935/app_preview
https://www.jianshu.com/p/fad3339e3448