在消息队列中,Kafka一直被称为是 吞吐量最大 的消息队列,那么它究竟为什么能够做到传输效率那么快呢?本文带你一探究竟
Kafka的基础架构
在介绍具体原因前,我们先来了解一下Kafka的基础架构,如下图:
如果你对其他某种类型的消费队列架构比较熟悉,那么看到这张图你应该不会觉得陌生,它只不过是吧一些组件重命名了一下,这些具体组件的含义如下:
Topic:逻辑队列,实际开发中会将一个业务中的信息放到一个Topic中进行存储
Cluster(Broker):物理集群,相当于Broker集群,每个集群中包含多个Topic
Producer:生产者,负责将消息发送到Topic
Consumer:消费者,负责从Topic中消费已经发送过来的消息
Partition:通常topic会有多个分片,不同的分片直接消息可以 并发 来进行处理,相当于RocketMQ中Topic下面的 MessageQueue
ZooKeeper:相当于Kafka的注册中心,Kafka集群的服务节点使用信息以及注册中心会注册到ZooKeeper中,可以管理Kafka的集群节点信息
Offset:在图里并没有展现,它主要用于从分区Partition中取数据,在Kafka中,偏移量Offset是针对一个分区Partition来设置的
为了防止数据丢失,Kafka也会为一份Partition数据复制多份拷贝,这些拷贝会分散到多个Broker下面进行存储,来防止某个集群突然失效时造成数据丢失
同时这些同一份数据的拷贝也会区分 Leader节点 与 Follower节点,Leader节点主要负责 负责读写请求,Follower节点主要负责 从主节点同步数据,保持数据的完整性:
Producer投递数据
批量发送数据
一般地消息投递消费过程很简单:
这个过程很简单,对于 Producer端:Producer向Broker投递消息,如果Broker接收成功会返回ACK,对于 Consumer端,Consumer发出请求向Broker取消息,Broker将消息发送给Consumer,Consumer发送成功后会返回ACK,由此可见,单单一条消息的投递与取出就要进行 五次 网络的请求与调用,在高并发环境下这样的调用肯定很浪费时间,因此Kafka应用了 批量发送 一次发送多条数据,减少网络请求次数:
数据压缩
而在进行一次网络请求的过程中,如果网络传输携带了过多的信息数据,也会造成性能的下降,为此Kafka引入了 数据压缩 来减少网络传输过程中的信息量,从而提高网络调用的性能,常见的压缩算法有Gzip(对存储空间要求高)、Snappy(对实时性要求较好)、Zstd(新型算法,对性能要求高)等
因此,在Producer端,主要应用 批量发送 与 数据压缩 来提高消息发送效率
Broker处理消息
顺序写文件
接下来到了Broker这里对于消息的处理,我们知道在Broker中存储着多个Topic分别代表不同业务的所需消息,而消息从Producer投递过来后,首先会被存储到 日志缓冲区 后,一定时间Kafka会通过操作系统的后台进程,将日志缓冲区中的内容刷回到磁盘中:
而由于每个日志文件分处于磁盘不同的磁道上,如果每次写入文件不固定,则 读写磁头 需要一直旋转进行寻道,因此Broker对于消息的写入方式为 顺序写,以此来减少每次写入消息时磁头的旋转次数,从而减少写入时间
索引文件
可是在处理Consumer的消费请求时,Broker难免要读取磁盘的数据将来返回给Consumer,那么Broker又是如何快速从磁盘上读取数据的呢,这时就要提到上面图中的两个索引文件 偏移量索引文件 与 时间戳索引文件 了:
偏移量索引文件:存储索引值与偏移量之间的映射关系,必须是从小到大进行存储的:
时间戳索引文件:时间戳实在Kafka的0.11.0版本引入的,它实际上就是多建立了一层关于时间戳与索引之间的映射文件:
通过这两个索引文件的映射,就能够快速根据指定偏移量找到对应位置的文件了,从而满足快速查找
零拷贝技术
要想真正了解零拷贝技术,我们首先来看一般情况下消费进程从磁盘获取文件的流程:
我们可以看到,一般情况下,文件会从磁盘读取到内核空间的 Read Buffer 后,再次经过 内核态到用户态 的拷贝到应用空间中,之后再从 应用空间拷贝到内核空间,一般来说,从内核态到用户态的切换时很费时的,而零拷贝就是减少了内核态到用户态的两次无用转换,因为文件描述符会存储在内核态中,因此零拷贝的 核心思想 就是:在从磁盘读取文件后,直接将文件传递给消费者的目标文件描述符,从而减少拷贝次数,提升网络传输效率,从这里我们也能发现,零拷贝并不是真正实现了没有文件的拷贝,它只是实现了零CPU拷贝,即CPU并不参与文件的拷贝工作,而一个典型的实现就是Linux的 sendfile
正常情况下,获取并发送文件要经历一次read与一次write调用,发生 四次 文件拷贝以及 四次 用户态到内核态的切换,使用了sendfile后,只需要进行 两次 内核态与用户态的切换,而文件拷贝次数也减少为了 两次
而Kafka在进行文件传输时,就会应用零拷贝减少文件拷贝次数,从而提高消息处理效率
mmap内核文件映射
一般情况下,如果我们要操作磁盘上的文件,必须从内核态中获取页表地址来获取最终数据,如果操作系统应用的是多级页表,在计算数据所在页表地址甚至要经历好几次I/O,而 mmap 的引入就是为了很好地解决这个问题,在Kafka中,用户态映射了磁盘上的文件在内核态的页表地址,通过将文件或者设备的一部分映射到进程的页表缓存中,下次获取数据可以直接从页表缓存中获取数据,从而减少磁盘与内核态之间的I/O次数
其实不光在Kafka中应用了mmap思想,在RocketMQ中也应用了这个方法,具体可以看我的这篇文章,又从源码分析mmap映射的运用: https://study.disign.me/article/202504/11.rocketmq-introduction.md
mmap相对于sendfile方法就稍逊一筹了,它会调用一次mmap获取文件与一次write发送文件,在用户态与内核态之间会发生 四次 用户态与内核态切换,而文件拷贝次数也减少了一次用户态到内核态的拷贝,变为了 三次
Consumer选择服务集群获取消息
在Consumer获取消息时,也不是随意选择一个服务集群来获取数据,而是每个服务集群会负责指定部分的数据,而当消费节点增多或者减少时,往往需要进行负载均衡策略来重新分配分片消息
Rebalance重新分配分片节点
一般情况下,每个Consumer服务集群负责的分片消息都是平均分配的,而在Kafka的早先版本,对于服务节点的Partition分配还需要依靠手动分配来完成,而在之后,Kafka引入了自动分配策略来实现Partition的分配,其主要思想是在Broker集群中设置消费协调者节点来分配节点,具体步骤如下:
首先如果有Consumer消费节点的加入,所有Consumer会随机向Broker节点发送 寻找协调者请求:
之后Broker会将对应的协调Broker集群告诉对应的Consumer节点,之后所有Consumer会继续向新的协调者Broker集群发送分配请求:
之后协调者Broker集群会发送回消费者节点选举出 Consumer Leader节点,它会负责进行服务节点与Partition分片信息之间的分配:
接下来消费者集群的Leader节点会根据负载均衡策略的不同将分片节点分配策略发送给Broker协调节点进行核验是否合理:
在分配方案核验通过后,Consumer节点就会持续向Broker协调者发送心跳请求,来实时更新服务节点信息了:
至此,Kafka消费者集群负载均衡的大致实现流程就介绍完毕了,其本质还是在于 Broker协调者的选举 与 消费者Leader负载均衡策略的分配
而Kafka消费如此之快的原因也介绍得差不多了,总结起来就是这几个方面:
Producer(生产者端):批量发送消息,消息压缩
Broker(服务集群):顺序写策略,索引文件,零拷贝,mmap策略
Consumer(消费者端):Rebalance策略
那么我们到这里思考一下,为什么Kafka消息吞吐量最高可达17w+/s,但是参考了Kafka架构的RocketMQ吞吐量只有10w+/s,其原因就在于:
Kafka使用了senfile来减少文件拷贝次数,而RocketMQ使用mmap减少文件拷贝次数,这样的话,RocketMQ就必须要多进行 两次用户态到内核态切换 与 一次文件拷贝,也因此造成了性能的差异
好了,这就是本文的全部内容了,希望对你有所帮助!!!