Kafka高性能探秘

2,423 阅读7分钟
      kafka众所周知是一款消息队列产品,相比较于其他的传统mq产品,kafka的单机吞吐量可达可以维持在十万级别,甚至可以达到百万级,这是传统mq产品不可比拟的。以时间复杂度为O(1)的方式提供消息持久化能力,即使对TB级以上数据也能保证常数时间复杂度的访问性能。

Kafka 高吞吐量的秘诀

1.顺序写磁盘

磁盘大多数都还是机械结构(SSD不在讨论的范围内),如果将消息以随机写的方式存入磁盘,就需要按柱面、磁头、扇区的方式寻址,缓慢的机械运动(相对内存)会消耗大量时间,导致磁盘的写入速度与内存写入速度差好几个数量级。为了规避随机写带来的时间消耗,Kafka 采取了顺序写的方式存储数据,如下图所示:

 每条消息都被append 到该 partition 中,属于顺序写磁盘,因此效率非常高。这种设计所带的的缺陷就是Kafka是不会删除数据,所以Kafka采用消费端记录读写位移(offset )的形式来记录对topic的读的记录。对于这个offset的话,Kafka是完全无视这个值得,这个状态完全由消费端SDK去维护,SDK将这个值保存在zookper上。当然如果不删除消息,硬盘肯定会被撑满,所以 Kakfa 提供了两种策略来删除数据。一是基于时间,二是基于 partition 文件大小,具体配置可以参看它的配置文档。即使是顺序写,过于频繁的大量小 I/O 操作一样会造成磁盘的瓶颈,所以 Kakfa 在此处的处理是把这些消息集合在一起批量发送,这样减少对磁盘 I/O 的过度操作,而不是一次发送单个消息。

2. 内存映射文件

即便是顺序写入硬盘,硬盘的访问速度还是不可能追上内存。所以 Kafka 的数据并不是实时的写入硬盘,它充分利用了现代操作系统分页存储来利用内存提高I/O效率。内存映射文件,它的工作原理是直接利用操作系统的 Page 来实现文件到物理内存的直接映射,完成映射之后对物理内存的操作会被同步到硬盘上(由操作系统在适当的时候); Kafka充分利用操作系统的这一个特性,通过内存映射文件(Memory Mapped Files )像读写内存一样的去读写硬盘,可以获取很大的 I/O 提升,因为它省去了用户空间到内核空间复制的开销(调用文件的 read 函数会把数据先放到内核空间的内存中,然后再复制到用户空间的内存中); 但这样也有一个很明显的缺陷——不可靠,写到 mmap 中的数据并没有被真正的写到硬盘,操作系统会在程序主动调用 flush 的时候才把数据真正的写到硬盘。所以 Kafka 提供了一个参数—— producer.type 来控制是不是主动 flush,如果Kafka 写入到 mmap 之后就立即 flush 然后再返回 Producer 叫同步(sync);如果写入 mmap 之后立即返回,Producer 不调用 flush ,就叫异步(async)。

3. 读数据零拷贝

传统模式下我们从硬盘读取一个文件是这样的;

 


(1)操作系统将数据从磁盘读到内核空间的页缓存区
(2)应用将数据从内核空间读到用户空间的缓存中
(3)应用将数据写会内核空间的套接字缓存中
(4)操作系统将数据从套接字缓存写到网卡缓存中,以便将数据经网络发出

这样做明显是低效的,这里有四次拷贝,两次系统调用。针对这种情况 Unix 操作系统提供了一个优化的路径,用于将数据从页缓存区传输到 socket。在 Linux 中,是通过 sendfile 系统调用来完成的。Java提供了访问这个系统调用的方法:FileChannel.transferTo API。这种方式只需要一次拷贝:操作系统将数据直接从页缓存发送到网络上,在这个优化的路径中,只有最后一步将数据拷贝到网卡缓存中是需要的。



Kafka通过mmap 提高 I/O的书写粒度,在文件大小一样的情况下,减少了IO的交互次数,同时集合顺讯写磁盘的优势,大大减少了磁盘寻址的复杂度。Kafka读取数据主要是调取系统底层的sendfile函数,把消息变成一个文件直接暴力输出,Kafka 的这种暴力的做法已经脱了 MQ 的底裤,更像是一个暴力的数据传送器。


Kafka的PUSH与PULL

介绍前,先解释几个名词

1.producer:
消息生产者,发布消息到 kafka 集群的终端或服务。
2.broker:
kafka 集群中包含的服务器。
3.topic:
每条发布到 kafka 集群的消息属于的类别,即 kafka 是面向 topic 的。
4.partition:
partition 是物理上的概念,每个 topic 包含一个或多个 partition。kafka 分配的单位是 partition。
5.consumer:
从 kafka 集群中消费消息的终端或服务。
6.Consumer group:
high-level consumer API 中,每个 consumer 都属于一个 consumer group,每条消息只能被 consumer group 中的一个 Consumer 消费,但可以被多个 consumer group 消费。
7.replica:
partition 的副本,保障 partition 的高可用。
8.leader:
replica 中的一个角色, producer 和 consumer 只跟 leader 交互。
9.follower:
replica 中的一个角色,从 leader 中复制数据。
10.controller:
kafka 集群中的其中一个服务器,用来进行 leader election 以及 各种 failover。
12.zookeeper:
kafka 通过 zookeeper 来存储集群的 meta 信息。


1.PUSH 

Producer将数据推送到Kafka机器的过程采用PUSH机制。起写入的流程如下:


producer 采用 push 模式将消息发布到 broker,每条消息都被 append 到 patition 中,属于顺序写磁盘(顺序写磁盘效率比随机写内存要高,保障 kafka 吞吐率)。消息路由到patition采用的是规则如下:

1. 指定了 patition,则直接使用;
2. 未指定 patition 但指定 key,通过对 key 的 value 进行hash 选出一个 patition
3. patition 和 key 都未指定,使用轮询选出一个patition。

确定了patition后,消息就会写入该patition下的leader副本中,其规则如下:

1. producer 先从 zookeeper 的 "/brokers/.../state" 节点找到该 partition 的 leader
2. producer 将消息发送给该 leader
3. leader 将消息写入本地 log
4. followers 从 leader pull 消息,写入本地 log 后 leader 发送 ACK
5. leader 收到所有 ISR 中的 replica 的 ACK 后,增加 HW(high watermark,最后 commit 的 offset) 并向 producer 发送 ACK

2.PULL 

Consumer 从Kafka消费数据的方式采用PULL的机制,如下图所示



        Kafka的消费模型中,定义了消费组的概念,Kafka规定一个topic有多个partition,但是每个partition中的信息只会下发给某一个消费组中的某一个消费者,这是Kafka消费模式跟传统mq的区别之一。在上述机制中,如果要实现一听歌"发布-订阅"模式的消费模型,我们就需要吧订阅的消费者放到每个不同的消费组里面才能这种模式。如果要实现一个queue模式的消费模式,也就是点对点模型,我们需要把消费者放到一个消费者里面,一个组内就可以对一个partition进行消费了,消息将会在consumers之间负载均衡,且一个消费组消费完,就不会再被重复消费了。

         我么可以总结一下,在kafka中,一个partition中的消息只会被group中的一个consumer消费;每个group中consumer消息消费互相独立;我们可以认为一个group是一个"订阅"者,一个Topic中的每个partions,只会被一个"订阅者"中的一个consumer消费,不过一个consumer可以消费多个partitions中的消息.kafka只能保证一个partition中的消息被某个consumer消费时,消息是顺序的.事实上,从Topic角度来说,消息仍不是有序的.