kafka 高级深入知识点总结

本文深入探讨Kafka的架构原理,涵盖生产者、消费者工作流程,数据持久化策略,以及副本同步机制。解析Kafka如何通过页缓存和零拷贝技术优化性能,介绍主题分区、副本管理和选举流程。此外,文章还讨论了Kafka在不同场景下的调优策略和可靠性保障。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

1.producer生产者原理

2.kafka生产者如何实现exactly-once?

3.kafka速度快的原因

4.kafka的留存策略

5.Kafka的分区Leader的选举场景和选举机制

6.Broker 的 Heap Size 如何设置?

7.在 Kafka 中,ZooKeeper 的作用是什么?

8.kafka能手动删除消息吗?

9.如何估算 Kafka 集群的机器数量?

10.Leader 总是 -1,怎么破?

11.kafkaController为什么使用单线程模型

12.如何处理延时请求/kafka的时间轮

13.如何判断副本是否应该属于 ISR

14.LEO,lSO , AR , ISR , HW 表示的含义

15.kafka的日志结构

16.kafka的页缓存技术

17. kafka的零拷贝技术/和kafka的零拷贝使用场景

18.kafka 索引文件及底层实现原理

19.kafka的稀疏索引

20.kafka是如何查找消息的

21.kafka日志段切分条件

22.kafka的ISR副本复制机制

23.kafka的生产,消费,分区的负载均衡

24.kafka ISR的伸缩

25.kafka的ACK机制

26.Leader和Follower副本之间是如何同步消息的(副本的备份同步机制)(Leader和Follower之间是怎么更新HW的和存在的问题)

27.基于HW副本同步,为了保证数据一致性的Epoch原理

28.为什么kafka不支持读写分离?

29.Log对象什么时候会更新LEO

30.Coordinator协作器

31.__consumer_offsets 是做什么用的?

32.Controller 发生网络分区(Network Partitioning)时,Kafka 会怎么样?

33.Java Consumer 为什么采用单线程来获取消息?

34.如何调优 Kafka?

35.消费者的rebalance (负载均衡)

36.kafka的可靠性分析,如何保证不丢失数据

37.kafka CAP理论

38.如何选择合适的分区数?

39.kafka的副本机状态机和分区状态机。

40.kafkaController的作用?

41.kafkaController的选举流程和场景

42.消费者分区分配策略

43.kafka二分法改进

44.kafka的选举

45.kafka数据迁移

46.kafka的请求处理模块

47.producersend线程唤醒的时机

48.kafka生产端的recordaccumulator(内存池的设计)

49.kafka Producer send线程加载元数据

50.producer batch什么条件下可以发送出去?




1.producer生产者原理

       producer是线程安全的。

       生产者客户端由两条线程协调运行,分别是主线程和sender线程(发送线程)。

       主线程通过KafkaProducer创建消息(ProducerRecord),然后通过拦截器序列化器分区器(默认DefaultPartitioner,默认进行hash%分区数求分区,如果key为null,就通过每条数据进行通过计数器累加,最大值是可用分区个数,轮询方式取余数发往可用的分区中)的作用之后将消息缓存到消息累加器中(RecordAccumulator,默认大小32M,决定参数是buffer.memory)。消息累加器的内部为每个分区维护了一个双端队列Deque<ProducerBatch>ProducerBatch代表为一个消息批次,封装了多个ProducerRecord,消息批次保证了减少了网络请求次数,提高吞吐量。每次有消息要写入消息累加器中的时候,会先去寻址消息所对应的分区的双端队列。因为队列内部有很多个ProducerBatch消息批次会从队列中尾部获取一个ProducerBatch消息批次,如果消息的大小在被写入ProducerBatch的大小范围内,则写入,如果没找到ProducerBatch或者大于ProducerBatch范围,则会在队列Deque尾新建一个ProducerBatch(producerBatch 默认大小16KB,决定参数batch.size) 后续sender线程从消息累加器中获取缓存的消息进行进一步封装,然后获取元数据,通过KafkaClient进行IO操作,写入到页缓存中。。


 

2.kafka生产者如何实现exactly-once?

幂等性保证

在kafkaV2版本后,kafka引入了幂等的概念。因为kafka在发送的消息上增加了2个字段,分别是produceridfirstSequence

每个新的生产者实例都会有服务端分配一个pid,生产者发送的每条数据上面都会有一个pid和单调递增的序列号,server也会针对每个分区<PID, Topic, Partition>为key保存对应的pid和序列号,每次server收到消息就会通过server的序列号加1来判断等不等于producer的序列号。

只需要 producer 配置 enable.idempotence=true

只能保证producer进程的一次运行,当重启producer进程之后,这种幂等性保证就丧失了,因为kafka会认为是两个producer实例。

事务性保证:(两阶段提交)可以保证一个producer的多个分区的消息实现原子性。

应用程序需要提供一个重启后不变的唯一ID,也就是transactionID, 与pid一一对应。区别在于Transaction ID由用户提供,而PID是内部的实现对用户透明。

用来保证相同的producer实例重启之后还可以通过transactionid获取之前的pid.

 

kafka为了实现事务的功能,还引入了事务协调器,可以类比消费者的组协调器GroupCoordinator),每个生产者会被指派一个特定的事务协调器,事务协调器会将关于持久化到内部主题_transaction_state)中。 

主要实现就是生产者会首先查找事务协调器所在位置,通过发送请求给事务协调器,事务协调器将信息等保存在内部主题中,随后producer会向事务协调器发送一个请求以开始两阶段提交协议。其中的关于消息的事务状态都会保存在内部主题中以保证持久化。

生产者发送请求给kafka,在kafka收到查找事务协调器的请求后,会根据transactionID进行计算哈希得出要操作的分区的leader所在的broker,即事务协调器,和GroupCordinator如出一辙。负责将对应的<transactionid,pid>保存到内部主题中。事务协调器会在内存保存每个事务的状态,并将状态写入到事务日志也就是内部主题中。该主题采用的日志压缩。所以事务结束后只需要将消息设为墓碑消息即可。

 

3.kafka速度快的原因

  1. 顺序写入磁盘比随机写入内存要快,避免了磁盘随机寻址的开销,内存顺序写>磁盘顺序写>内存随机>磁盘随机写
  2. kafka数据文件为二进制文件,相对文本文件会小一些,可以减少数据传输,提高速度。
  3. 页缓存,利用操作系统自身的内存而不是JVM空间内存,避免GC,生产者写入kafka磁盘的时候,会先写入页缓存中,然后操作系统负责将页缓存中的数据进行刷盘。读取的时候也会先读取页缓存,如果命中则不需要磁盘IO。
  4. 零拷贝技术。linux提供的内核技术,将数据从页缓存传输到broker的Socket buffer,在通过网络传输。

 

4.kafka的留存策略

每个kafka broker启动的时候,都会在后台开启一个定时任务,定期检查执行所有topic日志的留存。

kafka的留存策略包括2种。可以同时设置两种留存策略 通过log.cleanup.policy进行设置。可以设置为"delete,compact"

1.delete  待删除的目标是日志段,也就是.log文件。

基于空间维度。基于日志段级别。kafka定期为那些超过磁盘空间阈值的topic进行日志段的删除(log.retention.bytes=-1)默认没开启。

基于时间维度。基于日志段级别。kafka定期为那些超过时间阈值的topic进行日志段的删除(log.retention.ms),默认7天

activeSegment不会被删除。如果activeSegment的数据也都过期,会切分出一个新的日志分段作为activeSegment,然后执行删除。基于时间戳索引文件最大后一条索引项的时间戳值进行判断。

基于起始位移值  。基于分区级别。0.11.0之后新增的功能,为了检查每个日志段。如果下一段日志段的起始位移小于当前维护的起始偏移值则会删除该日志段。

2.compact。(相同的key的不同value值只会保留最后一个版本)可以类比Redis的RDB模式。activeSegment不会产于Compact..kafka中用于保存消费偏移的主题_consumer_offsets使用的就是Compact策略。

kafka会为每个日志目录 保存一个“clean-offset-checkpoint”的文件,这个文件就是清理检查点文件。用来记录每个主题的每个分区中已清理的偏移量,通过检查点cleaner checkpoint来划分一个已经清理过的clean部分和未清理过的dirty部分。在日志清理的同时,客户端也可以读取日志中的消息。clean部分的消息偏移量是断续的,dirty部分的消息偏移量是逐一递增的。

注意Log Compaction是针对key的。所以要注意每个消息的key值不为null.。每个broker会启动日志清理线程负责执行清理任务。线程会选择污浊率最高的日志文件进行清理。在每个清理线程中会有一块构建key与offset的哈希表。,日志清理需要遍历两次日志文件。第一次遍历把每个key的哈希值和offset保存在map中,第二次遍历会检查每个消息是否符合保留条件。对于valuenull的数据,会标记为墓碑消息,并保留一段时间后清理。

也就是说,执行compact前,kafka会自行选择合适的日志文件进行清理操作。compact会生成新的日志段文件。日志分段中每条消息的物理位置会重新按照新文件来组织。


 

5.Kafka的分区Leader的选举场景和选举机制

 

在每个broker进程启动的时候,会在创建Controller的过程中,创建分区状态机(ZkPartitionStateMachine)实例。而只有Controller组件所在的broker才会启动它。分区状态机用来管理分区状态

kafka有4种场景进行分区leader选举。

  1. 每当有分区上线的时候,就需要执行Leader选举。(有可能是加入新分区,有可能是之前下线的分区重新上线)
  2. 手动运行 kafka-reassign-partitions 分区副本重分配的命令  controller负责实现的。
  3. 手动运行 kafka-preferred-replica-election 命令,或自动触发了 Preferred Leader 选举时,用于对Leader进行重新负载均衡
  4. Broker 正常关闭时,该 Broker 上的所有 Leader 副本都会下线,因此,需要为受影响的分区执行相应的 Leader 选举

这 4 类选举策略的大致思想是类似的,即从 AR(assignments Seq[Int] ) 列表中挑选首个副本并且也在 ISR (Seq[Int])中的副本,优先副本策略 作为新 Leader


 

6.Broker 的 Heap Size 如何设置?

如何设置 Heap Size 的问题,其实和 Kafka 关系不大,它是一类非常通用的面试题目。一旦你应对不当,面试方向很有可能被引到 JVM 和 GC 上去,那样的话,你被问住的几率就会增大。因此,我建议你简单地介绍一下 Heap Size 的设置方法,并把重点放在 Kafka Broker 堆大小设置的最佳实践上。比如,你可以这样回复:任何 Java 进程 JVM 堆大小的设置都需要仔细地进行考量和测试。一个常见的做法是,以默认的初始 JVM 堆大小运行程序,当系统达到稳定状态后,手动触发一次 Full GC,然后通过 JVM 工具查看 GC 后的存活对象大小。之后,将堆大小设置成存活对象总大小的 1.5~2 倍。对于 Kafka 而言,这个方法也是适用的。不过,业界有个最佳实践,那就是将 Broker 的 Heap Size 固定为 6GB。经过很多公司的验证,这个大小是足够且良好的。


 

7.在 Kafka 中,ZooKeeper 的作用是什么?

目前,Kafka 使用 ZooKeeper 存放集群元数据成员管理Controller 选举,以及其他一些管理类任务。之后,等 KIP-500 提案完成后,Kafka 将完全不再依赖于 ZooKeeper。


 

8.kafka能手动删除消息吗?

kafka支持手动删除消息。

我们可以使用 kafka-delete-records 命令,需要配置json文件,指定要删除消息的位置分区,位置信息,或编写程序调用 Admin.deleteRecords 方法来删除消息。这两种方法殊途同归,底层都是调用 Admin 的 deleteRecords 方法,通过将分区 Log Start Offset 值抬高的方式间接删除消息。


 

9.如何估算 Kafka 集群的机器数量?

通常来说,CPU 内存资源充足是比较容易保证的,因此,你需要从磁盘空间带宽占用两个维度去评估机器数量。

预估磁盘的占用时,要记得计算副本同步的开销。如果一条消息占用 1KB 的磁盘空间,那么,在有 3 个副本的主题中,你就需要 3KB 的总空间来保存这条消息。

对于评估带宽来说,常见的带宽有 1Gbps 和 10Gbps,但你要切记,这两个数字仅仅是最大值。因此,你最好和面试官确认一下给定的带宽是多少。然后,明确阐述出当带宽占用接近总带宽的 90% 时,丢包情形就会发生。


 

10.Leader 总是 -1,怎么破?

删除 ZooKeeper 节点 /controller触发 Controller 重选举。Controller 重选举能够为所有主题分区重刷分区状态,可以有效解决因不一致导致的 Leader 不可用问题。


 

11.kafkaController为什么使用单线程模型

 

因为比如添加分区,控制器需要负责创建这些分区的同时,还需要更新上下文信息,并且要同步到其他节点,不管是监听器触发的事件,还是定时任务触发的事件,或者是其他事件,都需要拉取或者更新上下文信息
所以就涉及到多线程的同步,但是,单纯使用锁来控制线程安全,性能肯定会下降,所以采用单线程模型,将每个事件按顺序都暂存到LinkedBlockingQueue中,然后通过一个专属线程ControllerEventThread按照FIFO的原则来顺序处理各个事件。

controller会为每个broker创建一个请求阻塞队列,同时Controller也 会为集群中的每个 Broker 都创建一个对应的 RequestSendThread 线程。该线程每次从对应的阻塞队列中获取请求进行处理,每个请求都会阻塞,待完成response之后继续后续请求处理。这样的好处就是隔离broker的交互,代码实现容易,且没有额外线程安全的开销。


 

12.如何处理延时请求/kafka的时间轮

NettyZookeeperKafka等各种框架中,甚至Linux内核中都有用到时间轮。
时间轮有简单时间轮分层时间轮,kafka用的是分层时间轮。

用于kafka中大量的延迟操作,比如延迟生产, 延迟拉取,延迟删除等。

延时队列

在kafka中延迟生产操作中可以通过延时队列来实现延迟的操作。但是基于延时队列将不同的延时消息划分为不同的延时等级,发送给不同的内部延时主题进行保存,针对不同延时级别的主题有不同的线程进行消息的拉取,然后延时队列会进行消息暂存。发往真实的主题,但是适合延时精度不那么高的场景。因为延时消息会按照不同的延时等级进行更正。比如:17s的延时消息发送给20秒的延时主题,会以延时20秒的情况发往真实主题。但是基于延时队列的插入删除操作时间复杂度是O(logn),而基于时间轮的插入删除操作的时间复杂度为O(1)。

时间轮
kafka时间轮:TimingWheel  

kafka的时间轮是一个存储定时任务的环形队列。底层是数组。kafka的一个时间轮(TimeingWheel)中存放了多个bucket(双向循环链表,root表示头部节点,TimerTaskList) ,每个bucket也就是链表中保存了多个延时请求(TimerTaskEntry中的TimerTask)

tickMs:滴答一次的时长。

tick : 滴答,向前推进

wheelSize : 每一层时间轮上的Bucket数量。第一层Bucket数量是20.

interval: 这层时间轮的总时长,等于滴答时长×wheelSize。第一层默认是20毫秒。由于下一层时间轮的滴答时长就是上一层的总时长。因此第二层的滴答时长就是20毫秒。总时长400毫秒。以此类推。

overflowWheel : 按需求创建上层时间轮。比如有新的定时任务到达h会尝试放入第一层时间轮,如果第一层的interval总时长无法容纳定时任务的超时时间,就创建并配置好第二层时间轮。并尝试再次放入。如果还无法容纳,就创建第三层。知道找到合适的第N层时间轮。

kafka借用定时器来推进时间轮。也就是说,时间轮来执行插入和删除延时请求的操作,而定时队列也就是定时器负责时间推进的任务。

定时队列(DelayQueue)会将每个使用到的bucket(TimerTaskList)加入到定时队列中,然后根据每个bucket对应的超时时间进行排序,最短的放队头,kafka会有一个线程(过期操作收割机线程)来获取定时队列的延时请求列表,根据每个bucket的超时时间来推进时间轮,也就是获取到的bucket执行相应的操作,而bucket中的延时请求该执行过期操作就执行过期操作。



13.如何判断副本是否应该属于 ISR

Follower 副本的 LEO 落后 Leader LEO 的时间,是否超过了 Broker 端参数 replica.lag.time.max.ms 值。如果超过了,副本就会被从 ISR 中移除。


 

14.LEO,lSO , AR , ISR , HW 表示的含义

LEO:Log End Offset。日志末端位移值或末端偏移量,表示日志下一条待插入消息的位移值。举个例子,如果日志有 10 条消息,位移值从 0 开始,那么,第 10 条消息的位移值就是 9。此时,LEO = 10。

AR:Assigned Replicas。AR 是主题被创建后,分区创建时被分配的副本集合,副本个数由副本因子决定。

ISR:In-Sync Replicas。Kafka 中特别重要的概念,指代的是 AR 中那些与 Leader 保持同步的副本集合。在 AR 中的副本可能不在 ISR 中,但 Leader 副本天然就包含在 ISR 中。

HW:控制消费者可读取消息范围的手段。

LSO : 这是 Kafka 事务的概念。如果你没有使用到事务,那么这个值不存在(其实也不是不存在,只是设置成一个无意义的值)。该值控制了事务型消费者能够看到的消息范围。它经常与 Log Start Offset,即日志起始位移值相混淆,因为有些人将后者缩写成 LSO,这是不对的。在 Kafka 中,LSO 就是指代 Log Stable Offset。


 

15.kafka的日志结构

每个LOG对象对应的一个分区目录。LOG对象中封装了保存了日志段对象的容器segmentsConcurrentSkipListMap,跳表)。segments中保存了多个日志段对象(LogSegment

在LogSegment种保存的是消息集。

v0版本

Message Set(Record + Record + Record...)

Record->([offset,message size]消息头,[crc32,magic,attributes,key length, key , value length  ,  value ]消息体  )

v1版本

Message Set(Record + Record + Record...)

Record->([offset,message size]消息头,[crc32 , magic ,  timestamp ,  attributes , key length ,  key , value length  ,  value ]消息体  )

v2版本:

Record Batch( Record Batch Header  + Record)

Header -> (first offset , length,  partition leader epoch , magic , crc32, attributes  , last offset delta, first timetamp,  maxtimestamp, producer id , producer epoch , first sequence, records counts )

Record -> ( length, attributes, timestamp delta, ofset delta, key length, key , value length ,value headers count headers )

 

总结来说,v2版本不再使用之前的messageSet,而是RecordBatch, 并且增加了pid,sequence等字段。


 

16.kafka的页缓存技术

作用 :kafka索引文件,日志文件等

而目前几乎所有的操作系统都使用 LRU(Least Recently Used)或类似于 LRU 的机制来管理页缓存。

页缓存用于缓存文件的页数据,具体来说就是将磁盘中的数据缓存到内存中把对磁盘的访问变成对内存的访问。目的是为了加速IO,写数据的时候首先写入页缓存,然后由操作系统负责具体的刷盘任务,读取数据的时候会先去读取缓存,如果没命中,去磁盘读取,并且读到的数据也加入到缓存,当内存不够的时候 通过LRU淘汰算法来进行清理数据.这也意味着数据不需要重复拷贝到用户态空间,避免了很多不必要的时间空间消耗。

 好处:

避免了在java堆内存上的GC


 

17. kafka的零拷贝技术/和kafka的零拷贝使用场景

Kafka 中存在大量的网络数据持久化到磁盘(Producer 到 Broker)和磁盘文件通过网络发送(Broker 到 Consumer)的过程

linux的内核技术,数据由内核态->用户态->内核态的传输,零拷贝可以省略掉数据在用户态的传输,数据不需要重复拷贝到用户态空间,避免了很多不必要的时间空间消耗。

 

在 Kafka 中,体现 Zero Copy 使用场景的地方有两处:基于 mmap 的索引日志文件读写所用的 TransportLayer。先说第一个。索引都是基于 MappedByteBuffer 的也就是让用户态和内核态共享内核态的数据缓冲区,此时,数据不需要复制到用户态空间。不过,mmap 虽然避免了不必要的拷贝,但不一定就能保证很高的性能。在不同的操作系统下,mmap 的创建和销毁成本可能是不一样的。很高的创建和销毁开销会抵消 Zero Copy 带来的性能优势。由于这种不确定性,在 Kafka 中,只有索引应用了 mmap,最核心的日志并未使用 mmap 机制。再说第二个。TransportLayer 是 Kafka 传输层的接口。它的某个实现类使用了 FileChannel 的 transferTo 方法。该方法底层使用 sendfile 实现了 Zero Copy。对 Kafka 而言,如果 I/O 通道使用普通的 PLAINTEXT,那么,Kafka 就可以利用 Zero Copy 特性,直接将页缓存中的数据发送到网卡的 Buffer 中,避免中间的多次拷贝。相反,如果 I/O 通道启用了 SSL,那么,Kafka 便无法利用 Zero Copy 特性了。


 

18.kafka 索引文件及底层实现原理

每个日志段对象LogSegment都对应的有.log   .index   .timeindex  .txnindex等,其实还有关于.delete (kafka执行日志段文件删除) .snapshot(为幂等或事务型producer做的快照文件).cleaned (compaction时的产物) .swap(compaction时的产物)等等

kafka的索引文件以稀疏索引的方式来构建。

稀疏索引是通过MappedByteBuffer将索引文件映射到内存中。

内存映射文件,即 Java 中的 MappedByteBuffer。

.log

默认每写4KB的数据,.index和.timeindex文件就会分别增加一个偏移量索引项和时间索引项。

保存的日志消息

.index   <相对位移值,物理地址>

每个索引项占用8个字节。相对位移值占4个字节。物理地址占4个字节。

保存的是<相对位移值(消息的偏移量-当前日志段名字上的偏移量也就是该日志段的起始偏移量),消息对应在日志段文件中物理磁盘上的position位置> 这里如果使用绝对偏移量需要占用8个字节,如果是相对偏移量只需占用4个字节,节省了空间

.timeIndex:<时间戳,相对位移值>

每个索引项占用12个字节。 时间戳8字节, 相对位移值4字节

索引文件和日志段文件一样,都用到了页缓存零拷贝。kafka的索引文件是通过内存映射文件页缓存)来实现的,即java中的MappedByteBuffer,直接将文件内存映射到一段虚拟内存上。

二分法改进方案,对索引查询增加了不少性能


 

19.kafka的稀疏索引

默认日志写入大小达到4KB的时候,才会新增一个.index索引项和.time索引项。


 

20.kafka是如何查找消息的

查找指定时间戳的消息

 1.首先需要找到对应时间戳的索引文件

2.在时间戳索引文件中通过二分法找到时间戳对应的相对偏移量

3.然后通过二分法中查找获取index索引文件

3.从日志分段中通过二分法查找对应相对偏移量的对应的物理位置

4.然后根据物理位置去磁盘查找

 

查找指定偏移量的消息

1.根据偏移量,二分法查找到对应的偏移量index索引文件

2.在指定的index索引文件中获取到偏移量-(baseoffset)的相对偏移量,然后二分法查找到对应的物理位置

3.根据物理位置去磁盘查找


 

21.kafka日志段切分条件

日志分段文件达到一定的条件时需要进行切分,那么其对应的索引文件也需要进行切分

  1.  当前日志分段文件的大超过log.segment.bytes 默认1Gb.
  2. 偏移量索引文件或者时间戳索引文件达到log.index.size.max.bytes,默认10M.

 

22.kafka的ISR副本复制机制

每个partition都会有一个leader,每个follower都会同步leader的数据到follower,生产者端有个ack的设置,如果是-1,需要等待follower都同步成功才算成功,那么producer需要等待多少个ack的响应才算成功呢,答案是isr需要都返回响应。isr的条件就是副本始终与zk保持心跳,根据参数指定follower同步的进度。不满足则会提出isr集合中。

基于ISR的动态复制
如果Follower在replica.lag.time.max.ms时间内未向Leader发送Fetch请求(数据复制请求),则Leader将其从ISR中移除。

 

23.kafka的生产,消费,分区的负载均衡

 

生产者:

对于同一个topic得不同partition,kakfa会尽力将partition分不到不同得broker服务器上,均衡策略实际上都是基于zk节点监听实现的。均衡策略很大程度上依赖于分区器。DefaultPartitioner。它的分区策略是根据 Key 值进行分区分配的:

如果 key 为 null:消息将以轮询的方式,在所有可用分区中分别写入消息。

 

消费者:

通过rebalance进行消费者的再分配.

分区

对于kafka来说,针对分区多副本也有负载均衡策略。比如:优先副本策略。AR集合列表中的第一个副本也在isr中。被选为leader,尽量确保所有主题的优先副本在 Kafka 集群中均匀分布,这样就保证了所有分区的 leader 均衡分布。

 


 

24.kafka ISR的伸缩

kafka在启动的时候会启动两个与ISR相关的定时任务,“isr-expiration”和“isr-change-propagation”。前者任务会周期性的检测每个分区是否需要缩减ISR集合。与参数 replica.lag.time.max.ms参数有关。超时会被踢出集合,如果某个分区中的ISR集合发生变更,就会在zk上记录state节点,并且会将ISR集合变更后的记录更新到isrChangeSet内存中,另一个定时任务会周期性的(2500ms)检查isrChangeSet,如果发现有变更记录,就会在zk上创建一个/isr_change_...节点,Controller会通过zk的watcher监听该节点的变化,如果变更controller就会更新元数据并向管理的broker也发送更新元数据的请求。然后删除zk节点。

当失效副本追赶上leader上的HW,就会重新添加会isrChangeSet和更新zk的state节点,另一个线程得知isrChangeSet就会通知controller进行更新相关元数据的操作。


 

25.kafka的ACK机制

ack机制是从生产者端考虑的,指producer发送消息的确认机制。

  • ack=1         只要该分区的leader副本写入成功,则表示发送成功。
  • ack=0         producer发送一次就不管了。不管发送是否成功。
  • ack=-1/all   producer只有收到分区内所有副本的成功写入的通知才认为推送消息成功了。  

 

26.Leader和Follower副本之间是如何同步消息的(副本的备份同步机制)(Leader和Follower之间是怎么更新HW的和存在的问题)

是通过leaderfollower之间的HW来保证数据的一致性的。

leader所在的broker的副本管理机中会保存一套所有followerLEO,每个follower所在的broker的副本管理机种也会保存自己的LEO.

原理

1.followerh会不停的向leader发送请求(也就是被动向leader副本请求数据),当leader接收到follower的请求时,会先从leader自己的log中读取相应的数据,在返回给follower数据之前,先去更新leader自己保存的一套follower上的LEO。

2.之后follower一旦获取到消息后写入字节的日志中,其follower上的LEO也会自动的更新。。

3.一旦follower更新完自己的LEO之后,就会尝试更新自己的HW值(比较当前LEO值和leader响应给follower的LEO值取最小)。

4.正常情况下,leader更新HW会在每次producer向leader写入消息时每次在follower发送请求到leader的时候,leader就会更新比较HW(比较所有ISR中包括leader自己的所有HW,选出最小为HW)。所以,每次leader更新HW是在更新上一次请求过后的HW值也就是说leader的HW更新操作其实是在每次follower向leader发送请求的时候,leader才会去判断更新HW。这就是延迟一轮RPC请求更新HW值的设计。

例子

第一轮请求

  • 1.producer发送给leader一条消息。

1.leader会先写入消息到Log,

2.同时更新leader的LEO LEO=1

3.更新leader的HW。此时 leader.HW=0(比较leader副本管理机中保存的所有ISR的LEO值取最小)

  • 2.follower发送请求到leader.

1.leader接收到follower的请求后

2.leader读取底层Log数据,同时更新leader上的Follower的LEO leader.follower.LEO=0(因为follower发送的请求上会带有follower上的offset信息,leader判断出follower还没写入数据,故leader.follower.LEO=0)

3.更新leader端的HW值。leader.HW=0(所有满足条件的leader.follower上,LEO最小为0)

4.follower接收到leader的响应后

5.follower写入消息到本地Log中,同时更新follower.LEO=1

6.更新follower的HW,follower.HW=0(比较响应回来的leader.HW值 和本地follower.LEO值取最小)

此时第一轮请求结束。

情况如下:leader.LEO=1,     leader.follower.LEO=0,     follower.LEO=1,        follower.HW=0

第二轮请求

follower发送请求到leader

1.follower发送请求到leader

2.leader读取底层Log数据,同时更新leader上的Follower的LEO,leader.follower.LEO=1(因为follower的请求中携带了follower端的offset为1)

3.尝试更新Leader上的HW。leader.HW=1.(此时leader.LEO=1, leader.follower.LEO=1),

4.将数据(实际没有数据)和当前分区的HW值响应给follower.

5.follower接收到leader的响应后,follower写入消息(此时没有消息)到本地log中,同时更新follower.LEO=1.(没变化)

6.更新follower的HW。follower.HW=1(比较响应回来的Leader.HW值和本地follower.LEO值取最小)

由此看出,在第二轮的请求当中,leader的HW才会发生变化。

弊端:

当leader故障时,新选出leader之后,其他的副本会从高水位开始截掉,重新同步。保证数据一致。

1.丢失

LeaderA.LEO=1,LeaderA.HW=1

followerB.LEO=1,followerB.HW=0

这种情况下,followerB已经请求了第一次,将数据同步到了followerB,该到了followerB再次请求到leaderA的阶段了,followerB因为重启没有再次请求leader,此时leaderA宕机,followerB重启之后变为LeaderB,发现LeaderB.HW=0,于是将Leader.offset=1的消息删掉,同时更新LeaderB.LEO=1.,等A恢复之后(比较leaderB.HW和Leader.LEO),也会进行日志截断,然后设置FollowerA.HW=0.此时offset=1的消息则完全丢失

2.不一致

leaderA和FollowerB都成功写入一条日志,并且leadr写入第二条日志,此时followerB发送请求给A,follower携带followerB.LEO=1,leader收到请求后(leaderA.follower.LEO=1leaderA.HW=1)返回给FollowerB.

此时FollowerB宕机,没有收到响应,LeaderA也发生宕机,当B重新恢复当选Leader后(leaderB.LEO=1,leaderB.HW=0),此时producer发送数据到leaderB(leaderB.HW=1,leaderB.LEO=2).此时A恢复变为follower(followerA.HW=1,followerA.LEO=1),所以不做更新,看似一切正常,但LeaderBFollowerAoffset=1的消息是不一致



27.基于HW副本同步,为了保证数据一致性的Epoch原理

 

为了解决根据HW执行同步时的潜在问题导致数据不一致或丢失的情况,引入了leader epoch,leader端多开辟了一块内存专门保存leader的epoch信息。实际上是一对<epoch,offset>

epoch表示的是leader的版本号。每次选举leader,就+1

offset对应于该epoch版本的leader写入的第一条消息的位移

leader 的这个缓存会定期写入到一个checkpoint中。

Kafka Broker 会在内存中为每个分区都缓存 Leader Epoch 数据,同时它还会定期地将这些信息持久化到一个 checkpoint 文件中。当 Leader 副本写入消息到磁盘时,Broker 会尝试更新这部分缓存。如果该 Leader 是首次写入消息,那么 Broker 会向缓存中增加一个 Leader Epoch 条目,否则就不做更新。这样,每次有 Leader 变更时,新的 Leader 副本会查询这部分缓存,取出对应的 Leader Epoch 的起始位移,以避免数据丢失和不一致的情况。也就是说,当leader更新时,不再依靠HW来进行同步依据,而是通过epoch.


 

28.为什么kafka不支持读写分离?

其实在kafka2.4版本之后,kafka提供了follower能够对外提供读的服务。

  • 但是针对场景而言把,读写分离适合于读多写少的场景,kafka的使用场景更多应该是偏向于写
  • 其次就是数据不一致,leader和副本之间数据并不是全部同步的。是允许有滞后的。所以主写从读并不可靠
  • leader同步到follower势必也会有延时问题,所以,对于延时敏感的应用而言,主写从读的功能并不太适用。

 

29.Log对象什么时候会更新LEO

  1. log对象初始化的时候,需要创建一个新 的LEO对象,并对其进行初始化。
  2. 写入新消息的时候,当不断向Log对象插入消息时,LEO会向指针一样,不断的增加。
  3. Log对象发生日志切分的时候,当前日志段对象已满的时候,会关闭当前写入的日志段对象,创建一个全新的日志段对象,一旦日志发生切分,LEO中的起始位移值和段大小数据都要被更新,所以需要更新LEO对象
  4. 日志截断的时候,日志中的部分消息被删除了,自然可能导致LEO值发生变化,所以需要更新LEO对象。

 

30.Coordinator协作器

每个消费组都会选择一个broker作为自己的Coordinator,

至于如何选取的,首先会对消费组的groupid进行hash,接着对_consumer_offsets的分区数量取模,默认是50,找到groupId的offset要提交到_consumer_offsets的哪个分区,因为_consumer_offset的分区默认副本数是1,只有一个leader,也就是说找到的分区对应的leader所在的broker,就是该组的Coordinator

 

主要作用:

对位移主题_consumer_offsets的读写操作,就是由Coordinator进行的。

rebalance的操作


 

31.__consumer_offsets 是做什么用的?

_consumer_offsets保存了消费者组注册消息和消费者组已提交位移消息
默认50个分区,1个副本数。可以通过参数设置。

//消费_consumer_offsets的数据

kafka-console-consumer --topic __consumer_offsets --bootstrap-server 192.168.8.173:9092 --formatter "kafka.coordinator.group.GroupMetadataManager\$OffsetsMessageFormatter"
  • 它是一个内部主题,无需手动干预,由 Kafka 自行管理。当然,我们可以创建该主题。
  • 它的主要作用是负责注册消费者以及保存位移值。可能你对保存位移值的功能很熟悉,但其实该主题也是保存消费者元数据的地方。另外,这里的消费者泛指消费者组和独立消费者,而不仅仅是消费者组。
  • Kafka 的 GroupCoordinator 组件提供对该主题完整的管理功能,包括该主题的创建、写入、读取和 Leader 维护等。
  • Consumer的leader分配分区消费方案给Coordinator后,Coordinator会将注册消息写入到_consumer_offsets

 

32.Controller 发生网络分区(Network Partitioning)时,Kafka 会怎么样?

一旦发生 Controller 网络分区,那么,第一要务就是查看集群是否出现“脑裂”,即同时出现两个甚至是多个 Controller 组件。这可以根据 Broker 端监控指标 ActiveControllerCount 来判断。

由于 Controller 会给 Broker 发送 3 类请求,即 LeaderAndIsrRequestStopReplicaRequest UpdateMetadataRequest,因此,一旦出现网络分区,这些请求将不能顺利到达 Broker 端。这将影响主题的创建、修改、删除操作的信息同步,表现为集群仿佛僵住了一样,无法感知到后面的所有操作。因此,网络分区通常都是非常严重的问题,要赶快修复


 

33.Java Consumer 为什么采用单线程来获取消息?

Java Consumer 是双线程的设计。一个线程是用户主线程,负责获取消息;另一个线程是心跳线程,负责向 Kafka 汇报消费者存活情况。将心跳单独放入专属的线程,能够有效地规避因消息处理速度慢而被视为下线的“假死”情况。(如果收不到心跳会触发rebalance)单线程获取消息的设计能够避免阻塞式的消息获取方式单线程轮询方式容易实现异步非阻塞式,这样便于将消费者扩展成支持实时流处理的操作算子。因为很多实时流处理操作算子都不能是阻塞式的。另外一个可能的好处是,可以简化代码的开发。多线程交互的代码是非常容易出错的。


 

34.如何调优 Kafka?

常见的优化目标是吞吐量、延时、持久性和可用性

确定了目标之后,还要明确优化的维度。有些调优属于通用的优化思路,比如对操作系统、JVM 等的优化;有些则是有针对性的,比如要优化 Kafka 的 TPS。我们需要从 3 个方向去考虑。

  • Producer 端:增加 batch.size、linger.ms,启用压缩,关闭重试等。
  • Broker 端:增加 num.replica.fetchers,提升 Follower 同步 TPS,避免 Broker Full GC 等。
  • Consumer:增加 fetch.min.bytes 等

 

35.消费者的rebalance (负载均衡)

相当于重新对消费组里的消费组进行分配策略。

kafka2.6.0开始,支持了消费者自行控制rebalance.

触发时机

  1. 有新的consumer加入
  2. 旧的consumer挂了
  3. topic的partition新加
  4. coordinator挂了,集群选举出新的coordinator。
  5. consumer取消订阅的时候,unsubscribe()

rebalance方案是基于zkwatch来实现的。consumer启动时候,会在zk下维护一个临时节点(/consumers/[group_name]/ids),节点发生变化(是通过consumer心跳线程来判断consumer的存活),就会触发rebalance

当然,在rebalance期间,consumer是不能正常消费的。

 

rebalance的过程分为2步

join 加入组。所有消费者向coordinator发送请求,coordinator会选择一个consumer担任leader角色,leader负责消费分配方案的指定。

sync : 分配方案。leader分配哪个consumer负责消费那些topic的那些partition,一旦完成分配,leader就会把syncGroup请求发给Coordinator,其他非leader也会发送请求给Coordinator,只不过内容是空的,coordinator收到分配方案就把syncGroup中的response响应发给各个consumer,这样组内的所有成员就知道自己该消费那些分区了。

Rebalance的触发都是由Coordinator来执行的

 

如何尽量避免不必要的rebalance?

尽量避免不必要的Rebalance,比如设置consumer发送心跳的超时时间等session.timeout.ms 和 heartbeat.interval.ms例如:

  • 设置 session.timeout.ms = 6s。 用于检测消费者组成员存活性的
  • 设置 heartbeat.interval.ms = 2s。  

要保证 Consumer 实例在被判定为 “dead” 之前,能够发送至少 3 轮的心跳请求,即 session.timeout.ms >= 3 * heartbeat.interval.ms。

消费超时也会导致consumer被提出group.max.poll.interval.ms 比如让下游最大处理时间稍长一些。

 


 

36.kafka的可靠性分析,如何保证不丢失数据

就可靠性而言,基本没有任何东西可以保证做到百分之百的可靠。只能是通过一些策略来最大程度的保证可靠性。

1.增大副本数。副本数越多,越能够保证数据的可靠性。但是副本数越多也会引起磁盘,网络带宽的浪费,还有性能的下降。

所以,一般设置为3即可满足大多数的场景对可靠性的要求,部分银行之类的可能会设置成5.

2.生产端对于一些由于网络故障等造成发送失败的可重试异常,可以通过设置重试次数(retries)来增加可靠性。

3.生产者设置ack机制,为-1.但是也可能出现某个分区的数据的follower同步速度太慢都被提出了ISR集合,那么设置成-1和设置成1的效果其实一样了,因为这种情况只有Leader收到数据就可以,没有其他保证性了。可以通过min.issync.replicas参数来进行配置,表示ISR集合中的最小副本数,也就是说,必须保证ISR>1的时候,ack机制才会更可靠。默认是1.

4.正常情况下,通过sender线程写入到页缓存后,会有操作系统进行刷盘任务,当然kafka也可以配置同步刷盘的策略。

5.消费端尽量保证手动处理偏移量。保证数据能成功消费,不会造成数据丢失的情况。

6.kafka producer .send(message,Callback) 可以通过回调函数,来处理发送失败的数据。

7.设置 unclean.leader.election.enable = false。这是 Broker 端的参数,它控制的是哪些 Broker 有资格竞选分区的 Leader。如果一个 Broker 落后原先的 Leader 太多,那么它一旦成为新的 Leader,必然会造成消息的丢失。故一般都要将该参数设置成 false,即不允许这种情况的发生。


 

37.kafka CAP理论

分布式系统中,一致性、可用性、分区容错性不可兼得,最多只可同时满足两个

C:一致性。

A:可用性。

P:分区容错性。

Kafka满足的是CAP当中的CA (强一致性,可用性),分区问题始终都会存在。


38.如何选择合适的分区数?

  • 分区数越多,文件句柄的开销就越大。
  • 分区越多,在重新选取leader等操作上会比少量分区数的操作更耗时。,不可用性增加。
  • 客户端/服务器端需要使用的内存就越多

 通常需要根据服务器性能来合理安排partition的数量和副本数。


39.kafka的副本机状态机和分区状态机。

在每个broker进程启动的时候,都会创建ZKReplicaStateMachine(副本状态机)和ZkPartitionStateMachine (分区状态机)的实例。但是并不代表每个broker都会启动,事实上,只有Controller所在的broker上,才会启动它们。

副本状态机:broker上的副本更新信息,实则是通过Controllerbroker发送请求实现的。

大概的状态有:副本被创建,被删除,副本下线等等。都是通过Controller发送的3类请求实现的。

分区状态机: 管理分区状态,实则也是通过Controllerbroker发送请求实现的。

大概的状态有:分区被创建,被删除,正常提供服务等等。


40.kafkaController的作用?

集群 Broker 是不会与 ZooKeeper 直接交互去获取元数据的。相反地,它们总是与 Controller 进行通信,获取和更新最新的集群数据。而且社区已经打算把 ZooKeeper“干掉”了

controller会为每个broker创建一个请求阻塞队列,同时Controller也 会为集群中的每个 Broker 都创建一个对应的 RequestSendThread 线程。该线程每次从对应的阻塞队列中获取请求进行处理,每个请求都会阻塞,待完成response之后继续后续请求处理。这样的好处就是隔离broker的交互,代码实现容易,且没有额外线程安全的开销。

目前为止,Controller只会为broker发送3类请求,

1.更新元数据的请求

2.通知broker分区leader及ISO所在的broker。

3.指定broker停止上面的副本对象。

 

controllerContext 保存集群所有元数据。

controllerChannelManager:负责Controller向broker发送请求。

会向 Broker 发送请求,只有3类请求。(控制类请求)

 LeaderAndIsrRequest (告诉 Broker 相关主题各个分区的 Leader 副本位于哪台 Broker 上、ISR 中的副本都在哪些 Broker 上)

StopReplicaRequest(告知指定 Broker 停止它上面的副本对象,比如分区副本迁移和删除主题的场景下)

UpdateMetadataRequest (该请求会更新 Broker 上的元数据缓存,实则副本状态机和分区状态机的状态变更很多都是发送的该请求)

kafkaScheduler : 线程调度器,比如负责执行分区重平衡的leader选举。

eventManager  :事件管理器。

管理副本状态机和分区状态机  replicaStateMachine   partitionStateMachine

topicDeletionManager  主题删除管理器,负责删除主题及日志。

集群成员管理。通过brokerChange事件写入队列,然后进行处理。


41.kafkaController的选举流程和场景

1.集群从零启动的时候。

所有的broker都会启动ControllerEventThread线程,注册zk状态变更监听器,用于监听broker和zk的会话是否过期,然后会将事件添加到事件队列eventManager。开启线程去处理事件队列中的事件。

2.Broker侦测到/controller节点消失的时候

broker会通过zk的监听器检测到/controller节点消失,

3.Broker侦测到/controller节点数据发生变更的时候

broker会通过zk的监听器检测到/controller节点变更,如果broker之前是controller,那么会执行卸任(比如关闭之前的状态机,监听器的注册,清空集群元数据等等)操作。如果不是,然后去竞选Controller.

竞选则是每个broker去zk上创建/controller节点。 选举流程主要依靠zk的监听器机制和/controller临时节点


42.消费者分区分配策略

 

1.RangeAssignor  默认:范围分区  按照消费者总数和分区总数进行整除运算进行平均分配,最后多出来的会按字典靠前的消费者多分配。 
 2.RoundRobinAssignor  轮询分区  将每个分区和消费者排序,轮询分配给消费者,但是当某个消费者没有订阅某个topic时,该消费者是不会被分配到该topic的任何分区的。
3.StickyAssignor 粘黏分区  v2版本之后引入的。  除了尽可能保证分区分配的均匀,当分区需要重新分配的时候,可以尽量保证消费者有些分区与上次分配的相同
 


43.kafka二分法改进

Kafka 索引应用二分查找算法快速定位待查找索引项位置,

因为kafka写入索引文件的方式也是在文件尾部追加写入,而几乎所有的索引查询都集中在索引的尾部,这么看的话,通过LRU非常适合索引访问场景,但这里有个问题是,当 Kafka 在查询索引的时候,原版的二分查找算法并没有考虑到缓存的问题,因此很可能会导致一些不必要的缺页中断(Page Fault)。此时,Kafka 线程会被阻塞,等待对应的索引项从物理磁盘中读出并放入到页缓存中。(也就是说,有些相对不频繁访问的数据因为LRU或许已经不存在在页缓存中,那么就需要线程阻塞等待从磁盘读取并放入页缓存

所以,当随着LRU对索引文件的淘汰策略就会使得page页发生变化,那么二分查找的路径就会被改变,也就是会导致一些或许从来不在页缓存的冷数据必须加载到页缓存中,这种加载过程就会很耗时。

改进方案则是:将索引项分成了2部分。热区冷区。然后分别对两区域内执行二分算法。

该算法能保证经常需要访问的page组合是固定的。也就是说,查询最热那部分数据通过二分法所需要遍历的page永远是固定的。从而避免缺页。(具体实现大概是通过冷区热区分割线8192字节处,大部分查询集中在尾部,把尾部8192字节设置为热区,因为每个索引项是4KB写入一次,每个page大小最小为4096大小,热区大约也就占用2-3个页面吧,所以设置成8192基本是差不多正合适,这样避免了页中断。也保证了所有热数据都在缓存中)


44.kafka的选举

分为:

消费者的选举

leader的选举

controller的选举


45.kafka数据迁移

kafka Mirror Maker 用于kafka和kafka之间的数据复制。

kafka Connect用于其他数据存储系统与kafka之间的数据复制。可以将数据从kafka中导入或导出。RDBMS,hadoop,nosql


46.kafka的请求处理模块

 

kafka三层网络架构
1.acceptor线程用来接收客户端或者broker的请求  :创建对应的SocketChannel连接通道,通过轮询的方式将请求和通道交给processor线程。
2.processor线程 默认3.  num.network.threads ..     PLAINTEXT://localhost:9092,SSL://localhost:9093, 则默认就是6个线程
3. processor线程获取socketChannel通道注册JavaNIO的selector 然后进行接收请求事件比如连接事件,读写事件,这些通道会和对应的节点ID进行保存用来后续请求进行复用。建立网络连接使用的多路复用,第一次建立连接后,后续可以直接复用
4.processor会将通道上的请求转换成请求对象Request.放入到RequestChannel。其实requestChannel也是起到一个缓冲的作用。
5.requestChannel中封装了请求队列保存请求,也封装了响应队列,来封装请求完得到的响应。
6.具体处理请求的逻辑是在KafkaRequestHandlerPool线程池中的KafkaRequestHandler线程进行真正的处理请求逻辑。
默认kafkaRequestHandler线程个数为8个。num.io.threads= 8 . 用来处理processor的请求。 

 

 


47.producersend线程唤醒的时机

主线程发送数据到消息累加器recordaccumulator后, 会需要先获取元数据,send线程发送网络请求同步阻塞等待kafka返回响应,拉取到元数据返回后会唤醒send线程


48.kafka生产端的recordaccumulator(内存池的设计)

默认32M.其中包括内存池(开始初始化为0)和可用内存(初始化32M).

每次有数据需要保存到累加器recordaccumulator的时候,因为我们知道数据是以batch来进行存储的,每个batch默认大小16KB,则刚开始会从可用内存申请内存块到内存池bufferPool,每个申请的内存块默认16KB,用来存储数据,如果消息大于16KB小于1M,则以消息大小从可用内存中申请内存块到内存池然后存储数据。

每个batch的消息被发送出去之后,特殊申请的内存比如大于16KB的会归还给可用内存,16KB的内存因为可以复用,则归还给内存池,不用太频繁申请16KB内存块。如果内存池都是些复用的16KB的内存块,此时需要申请大于16KB的内存保存数据比如100KB,则一个一个内存池的可用内存块16KB进行一个一个释放,直到可用内存达到100KB,停止释放,然后申请100KB的内存块到内存池然后去保存数据

内部结构是CopyOnWriteMap。写时复制,适用于读多写少的场景。写时会加分段锁保证高并发。

49.kafka Producer send线程加载元数据

 

50.producer batch什么条件下可以发送出去?


1.等待时间到了,linger.ms  不管批次有没有写满都要发送。
2.如果批次写满,则发送。
3.消息累加器中bufferPool中有保存需要等待申请内存块的队列大于0,说明内存不够用,也需要发送,最后释放内存。
4.客户端关闭,消息也会发送出去。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

刘狗

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值