Kafka 问答

242 阅读15分钟

1. 什么是Kafka?

Kafka是由Apache软件基金会开发的一个开源流处理平台,由Scala和Java编写,是一种高吞吐量的分布式发布订阅消息系统。

2. Kafka有什么作用?

Kafka主要有三个作用,异步、解耦、削峰。

异步

Kafka可以实现上下游服务的异步消息处理,提高系统响应速度和吞吐量。

引入Kafka前:

A 服务生产消息后,调用 B 服务的接口进行通知。

引入Kafka后:

A 服务生产消息后,只需要发送给Kafka即可,不再需要通知 B 服务。

解耦

Kafka可以对上下游服务进行解耦,使得各服务独立开发迭代,不再相互影响。

引入Kafka前:

A 服务生产消息后,如果B 服务需要对消息进行消费,就需要 A 服务调用 B 服务的接口,通知 B 服务进行消费。如果某天 B 服务的接口改变了,或者不再消费 A 服务生产的消息了,或者又出现了 C 服务需要消费 A 服务的消息,那么 A 服务就需要不断地适配修改。

引入Kafka后:

A 服务生产的消息,只需要发送给Kafka,由Kafka进行存储,B 服务只需要根据自己的情况从Kafka中进行拉取即可,不再影响 A 服务。即使是又出现了 C 服务,A 服务也不感知。

削峰

Kafka可以以稳定的系统资源应对突发的流量冲击,从而防止服务的崩溃,生产者的生产速度和消费者的消费速度不再需要强匹配。

引入Kafka前:

A 服务生产消息后,就会通知 B 服务进行消费,如果 B 服务的消费速度跟不上 A 服务的生产速度,就会导致 B 服务遭受到大流量的冲击,严重的可能导致 B 服务宕机。

引入Kafka后:

A 服务生产的消息发送给了Kafka,B 服务可以根据自身的消费速度从Kafka中拉取即可。

3. Kafka为什么高性能(高吞吐)?

(1) 顺序写

Kakfa采用顺序写入磁盘的方式,避免了随机磁盘存储时的磁头寻址的消耗。

(2) 页缓存

Kafka利用了操作系统的页缓存实现数据的写入,Kafka仅仅写入内存,由操作系统负责将数据写入磁盘。

(3) 零拷贝

在发送数据时,往往需要把数据从磁盘先拷贝到内核态的页缓存中,然后拷贝到用户态的内存,再拷贝到内核态的Socket缓冲区中,最后拷贝到网卡进行发送。

image.png

Kafka利用了零拷贝机制,数据先从磁盘文件拷贝到内核态的页缓存中,操作系统会把这块缓存与用户程序共享,这样就不需要再把数据往用户态缓存中拷贝了。之后,操作系统直接将页缓存中的数据拷贝到socket缓冲区中,最后拷贝到网卡中进行发送。 image.png

在Linux2.4版本之后,甚至只需要将要发送的数据在内核态的页缓存中的位置和偏移量发送给Socket缓冲区即可,再根据这个偏移量将数据直接拷贝到网卡上即可。 image.png

(以上图片均来自这里。)

一句话总结:Kafka利用了操作系统的零拷贝机制,数据可以直接从磁盘通过内核态的页缓存拷贝到网卡进行发送,避免了内核态和用户态之间进行上下文切换的开销

(4) 批量消息压缩

Kafka可以将消息进行批量压缩之后再发送,减少了网络IO的消耗。

(5) 分区分段存储 + 索引

Kafka采用了分区分段存储的方式,通过索引可以快速获取到消息的物理地址,提高了消息搜索的效率。

4. Kafka的组成结构是什么样的?

image.png

(1) Kafka集群

Kafka集群中包含多个Broker,每个Broker中有多个分区(Partition),分区可以设置多个副本(Replica),存储在不同的Broker上。每个分区相当于一个文件夹,里面包含多个文件段(Segment),每个Segment中主要包含一个log文件和两个索引文件(位移索引.index和时间戳索引.timeindex)。

(2) 生产者(Producer)

用于生产消息,每个生产者每次都会把消息发送到某一个Broker的某一个分区上。

(3) 消费者(Consumer)

用于消费消息,每个消费者都归属于某一个消费者组,一个消费者组中所有消费者都订阅相同的Topic,并且不能消费同一个分区。

(4) Zookeeper

在老版本的Kafka中,需要依赖Zookeeper为其提供元数据的管理,比如Broker的信息、Topic数据和分区副本数据等等。还有一些选举、扩容等机制也都依赖Zookeeper,比如控制器的选举:每个Broker启动时都尝试在Zookeeper创建临时节点,第一个创建临时节点的Broker会被指定为控制器Controller,竞争失败的节点也会依赖Watcher机制监听controller,一旦控制器宕机,那么其他Broker会继续来竞争。

在新版本的Kafka中,已经可以不再需要依赖Zookeeper了,而是使用自己的Topic存储元数据信息,并使用基于Raft协议的KRaft模式来处理控制器选举等问题。

为什么要去掉Zookeeper呢?
一个原因是想要解耦:Kafka作为一个消息队列,需要依赖另一个分布式协调系统,耦合过于严重;
另一个原因是Zookeeper自身问题:Zookeeper非常笨重,容易成为性能瓶颈,为了保证一致性,就牺牲了可用性(CAP定理)。

5. Kafka中的消息存储和查找机制?

Kakfa中的每个消息都属于一个主题(Topic),一个Topic的消息可以在多个Partition中存放。 Kafka中的消息是以Segment的形式存储的,每个Segment中主要包含一个log文件和两个索引文件(位移索引.index和时间戳索引.timeindex)。

Kafka中的消息会写入日志文件(.log)中,如果文件到达指定的大小(默认1G)时,会创建一个新的文件。每个消息都有自己的偏移量(offset),该日志文件中的消息中最小的偏移量的值作为该日志文件的文件名,除了日志文件外,还会创建同名的.index文件和.timeindex文件。

-rw-r--r--  1 root root    1835920 10月 11 19:18 00000000000000000000.index  
-rw-r--r--  1 root root 1073741684 10月 11 19:18 00000000000000000000.log  
-rw-r--r--  1 root root    2737884 10月 11 19:18 00000000000000000000.timeindex  
-rw-r--r--  1 root root    1828296 10月 11 19:30 00000000000003257573.index  
-rw-r--r--  1 root root 1073741513 10月 11 19:30 00000000000003257573.log  
-rw-r--r--  1 root root    2725512 10月 11 19:30 00000000000003257573.timeindex  
-rw-r--r--  1 root root    1834744 10月 11 19:42 00000000000006506251.index  
-rw-r--r--  1 root root 1073741771 10月 11 19:42 00000000000006506251.log  
-rw-r--r--  1 root root    2736072 10月 11 19:42 00000000000006506251.timeindex  

.index文件是依offset建立的稀疏索引,其中包含了消息的相对偏移量和其在日志文件中的物理地址的映射 <relativeOffset, position>,而offset = 基准偏移量(就是文件名的值) + 相对偏移量。查找消息时先根据二分法确定所属的segment,然后计算相对偏移量,再根据.index文件中的映射关系获取到不大于该offset的物理地址,最后从该物理地址开始查找该offset对应的消息。
image.png (图片来自这里

例如,当需要读取offset=170416的消息时,先使用二分法获取所属的segment,其对应的.index文件为00000000000000170410.index(起始偏移为170410+1=170411)。然后计算其相对偏移量170416 - 170410 = 6,根据映射关系获取到不大于该offset的物理地址为 <4, 476>,然后从476开始,查找offset为170416的消息。

image.png (图片来自这里

.timeindex文件和.index文件类似,其中包含了时间戳和消息的相对偏移量的映射 <timestamp, relativeOffset>,也就是说先遍历寻找segment中最大时间戳大于该时间戳的.timeindex文件,然后再根据时间戳获取到不大于该时间戳的最大偏移量,根据这个最大偏移量找到不大于该偏移量的物理地址,最后从该物理地址开始进行查找不小于该时间戳的消息。

例如,当需要读取时间戳为1526384718288开始的消息时,首先要找到对应的segment,其.timeindex文件为00000000000000000000.timeindex,找到不大于该时间戳的映射关系 <1526384718283, 28>,根据获取到的相对偏移量,从00000000000000000000.index中找到不大于该相对偏移量的映射关系 <26,838>,然后从838开始,查找时间戳不小于1526384718288的消息。

6. Kafka中副本间的同步机制?

Kafka中一个分区可以有多个副本(Replica),副本分为主副本(Leader)和从副本(Follower)。Leader负责读写数据,Follower只负责进行数据的冗余存储,需要从Leader同步数据。处于同步状态的副本集合称为ISR,不处于同步状态的副本集合称为OSR(replica.lag.time.max.ms用于配置Follower和Leader通信的最大时延,用于判断是否处于同步状态,默认为10s)。

副本同步有三个重要的属性:

LEO:日志末端位移(log end offset),标识当前日志文件中下一条待写入的消息的offset。
Remote LEO:Leader保存的其他follower的LEO。
HW:高水位(High Watermark),定义了消息可见性,标识了一个特定的消息偏移量offset,消费者只能拉取到HW之前的消息,即ISR中最小的LEO。

image.png
(图片来自这里

同步过程大致如下:

  1. Follower需要不断从Leader进行数据同步,保持其处于ISR中,初始所有值都是0;

  2. 生产者发送一条消息至Leader,Leader持久化到磁盘后,其LEO + 1 = 1,HW = 0;

  3. Follower同步Leader的数据并持久化到磁盘后,其LEO + 1 = 1,HW = 0;

  4. Follower再次同步Leader的数据时,Leader的RemoteLEO + 1 = 1,HW = 1;Follower收到Leader的HW,更新其HW = 1.

    image.png (图片来自这里

7. Kafka如何做选主?

选主分为两部分,一个是从Broker中选出一个Controller,另一个是从分区副本中选出一个Leader。

(1) 从Broker中选择Controller

在依赖Zookeeper时,每个Broker启动时都尝试在Zookeeper创建临时节点,第一个创建临时节点的Broker会被指定为控制器Controller。如果Controller宕机,Zookeeper上的临时节点就会消失,其他的Broker通过Watcher机制监听到Controller下线的消息后,会继续按照先到先得的原则来竞争Controller。

新版本不依赖Zookeeper时,利用的是基于Raft算法的KRaft机制。
Raft算法共有三种成员身份:

领导者(Leader):数据一切以领导者为准,它是与客户端交互的唯一角色,用来处理请求,管理日志的同步,同时还不断发送心跳信息给跟随者,不断刷新跟随者节点的超时时间,防止跟随者们发起新的选举;
跟随者(Follower):在不发生选举时,跟随者只是充当数据冗余的作用,当领导者心跳超时,跟随者就会主动推荐自己成为候选人;
候选人(Candidate):成为候选人后,就会向其他节点发送请求,以获取其他节点的投票,如果获得了大多数投票,就会成为领导者。

详细的选举过程可以查看这里

当一个Broker成为Controller后,它就会负责获取和管理Broker、Topic、Partition以及副本主从信息,并监听其变化。

(2) 从分区副本中选择Leader

只有处于ISR中的副本才有资格竞选Leader,如果Leader宕机,Kafka默认选择ISR中第一个副本成为Leader,其他的Follower会把高于HW的消息截取掉,重新从新的Leader进行同步。如果是Follower宕机,会被从ISR中剔除,当它恢复后,会重新从它的HW的位置进行同步。

8. Kafka的分区分配策略?

(1) 生产者如何选择消息要发送到的分区?

  • 如果指定了分区ID,则选择该分区;
  • 如果没有指定分区ID,但存在key,则将key的hash值与分区数取余,选择对应分区;
  • 如果没有指定分区也不存在key,则使用粘性分区策略:生产者发送消息时,会将消息放到一个Batch中打包发送,粘性分区策略会优先填满某一个Batch,然后随机发送到一个分区中。

(2) 消费者的分区分配策略是什么?

  • Range:针对每个Topic来说,先计算每个消费者平均分配多少个分区,剩下的分区由靠前的消费者多分配一个的方式来消费;
  • Round-Robin:针对所有Topic来说,将所有Topic的分区进行轮询分配;
  • 粘性策略:尽可能均匀的分配,消费者间分区数最多相差一个;如果发生ReBalance,尽量保证与上一次分配的结果相同,即未宕机的consumer所消费的分区不会被分配给其他的consumer上。

(3) 如果新增/减少Broker,如何进行分区分配?

当Broker宕机或者新增Broker时,需要手动执行脚本进行分区重分配,对分区进行迁移。

9. Kafka再平衡(Rebalance)的条件?

  • 消费者组中成员数量变更:比如新增或删除某个消费者,导致其所消费的分区需要分配到组内的其他消费者上;
  • 消费者组订阅的topic数量发生变化:比如订阅时采用的是正则表达式的形式,当又创建了一个匹配的topic时,那么这个新topic上的分区也需要进行分配;
  • 消费者组订阅的topic的分区数量发生变化:比如创建或删除某个分区,也会导致Rebalance。

总的来说,如果分区数或者消费者数发生变化,就会导致Rebalance

10. Kafka如何解决消息丢失?

消息丢失可以从三个方面来分析:

(1) 生产者

原因:生产者生产的消息由于网络不可用等原因,没有发送到Broker上,导致消息丢失。 解决:通过ACK机制来解决。

  • 0:生产者不等待响应,消息可能会丢失;
  • 1:Leader收到消息后就进行响应,如果Leader还未同步给Follower就已经宕机,则会丢失消息;
  • -1/all:ISR中所有的副本收到消息后再进行响应,不会丢失消息。

(2) Broker

原因:如果Broker不可用了,则消费者无法拉取到消息,相当于消息丢失。
解决:通过冗余副本的方式解决,提高容灾能力。如果Leader不可用,则会从ISR中选择一个副本作为Leader。

(3) 消费者

原因:如果消费者先提交offset后进行消费,那么在真正消费之前消费者宕机,则会导致消息丢失。
解决:关闭自动提交,先进行消费再手动提交offset。(这样有可能会导致重复消费)

11. Kafka如何解决消息重复?

消息重复可以从两个方面来分析:

(1) 生产者

原因:生产者没有收到来自Broker的响应,重新进行了发送,则会造成消息重复。
解决:Kafka开启幂等性配置:每个生产者初始化时,会被分配一个唯一的ProducerId;每个生产者发送到每个分区的消息,都对应一个从0开始的单调递增的SequenceNumber值;Broker根据缓存的SequenceNumber的信息,只接受SequenceNumber + 1的消息,这样就实现了消息的幂等性。
(该方案通过ProducerId、分区号以及SequenceNumber只能实现了单分区内的幂等性,如果要实现多分区的幂等性,需要引入事务,具体可以查看这里这里。)

(2) 消费者

原因:消费者先进行消费,在手动提交offset前,消费者宕机,则会导致消息重复消费。
解决:在业务中实现幂等性。比如可以通过消息表、状态机等方式进行去重。

12. Kafka如何解决消息积压?

原因:生产者的生产速度和消费者的消费速度不匹配(生产速度太快而消费速度太慢)。
解决:① 增加分区数和消费者数量;② 增加每次poll的消息数量/提高poll频率;③ 把消费者逻辑改为直接把消息写入新的Topic,然后用更多的消费者来消费新Topic中的消息。

13. Kafka如何保证消息顺序?

(1) 生产者 --> Broker

开启幂等性配置:生产者指定发送到同一个Topic的同一个分区,由幂等性中的SequenceNumber保证消息顺序。

(2) Broker --> 消费者

由一个消费者组中的一个消费者进行消费。