今天我们就来安排一期关于 Kafka 的核心面试题连环炮, 从「基础知识」、「进阶提升」、「架构调优」 三个方向梳理面试题,希望在金三银四的关键节点可以帮助到大家。
这篇文章干货很多,希望你可以耐心读完。
02 kafka 进阶提升10问
谈谈你对 kafka 的集群架构是如何理解的?
01
Kafka 整体架构图
一个典型的 Kafka 集群中包含若干 Producer,若干 Broker「Kafka支持水平扩展,一般 Broker 数量越多,集群吞吐率越高」,若干 Consumer Group,以及一个 Zookeeper集群。
Kafka 通过 Zookeeper 管理集群配置,选举 Leader,以及在 Consumer Group 发生变化时进行 Rebalance。Producer 使用 push 模式将消息发布到 Broker,Consumer使用 pull 模式从 Broker 订阅并消费消息。
02
Kafka 存储机制
Producer 端生产的消息会不断追加到 log 文件末尾,这样文件就会越来越大, 为了防止 log 文件过大导致数据定位效率低下,Kafka 采取了分片和索引机制。
它将每个 Partition 分为多个 Segment每个 Segment 对应3个文件:
1).index 索引文件
2).log 数据文件
3).timeindex 时间索引文件
这些文件都位于同一文件夹下面,该文件夹的命名规则为:topic 名称-分区号。
03
Kafka 副本机制
Kafka中的 Partition 为了保证数据安全,每个 Partition 可以设置多个副本。此时我们对分区0,1,2分别设置3个副本。而且每个副本都是有「角色」之分的,它们会选取一个副本作为 Leader 副本,而其他的作为 Follower 副本,我们的 Producer 端在发送数据的时候,只能发送到Leader Partition 里面 ,然后 Follower Partition 会去 Leader Partition 自行同步数据, Consumer 消费数据的时候,也只能从 Leader 副本那去消费数据的。
04
Kafka 网络模型
Kafka 采用多路复用方案,Reactor 设计模式,并引用 Java NIO 的方式更好的解决网络超高并发请求问题。
谈谈Kafka客户端如何巧妙解决JVM GC问题?
01
Kafka 客户端缓冲机制
首先,大家知道的就是在客户端发送消息给 Kafka 服务端的时候,存在一个「内存缓冲池机制」 的。即消息会先写入一个内存缓冲池中,然后直到多条消息组成了一个 Batch,达到一定条件才会一次网络通信把 Batch 发送过去。
整个发送过程图如下所示:
Kafka Producer 发送消息流程如下:
1)进行 Producer 初始化,加载配置参数,开启网络线程。
2)执行拦截器逻辑,预处理消息, 封装 Producer Record。
3)调用 Serializer.serialize() 方法进行消息的 key/value 序列化。
4)调用 partition() 选择合适的分区策略,给消息体 Producer Record 分配要发送的 Topic 分区号。
5)从 Kafka Broker 集群获取集群元数据 metadata。
6)将消息缓存到 RecordAccumulator 收集器中, 最后判断是否要发送。这个加入消息收集器,首先得从 Deque<RecordBatch> 里找到自己的目标分区,如果没有就新建一个 Batch 消息 Deque 加进入。
7)当达到发送阈值,唤醒 Sender 线程,实例化 NetWorkClient 将 batch record 转换成 request client 的发送消息体, 并将待发送的数据按 【Broker Id <=> List】的数据进行归类。
8)与服务端不同的 Broker 建立网络连接,将对应 Broker 待发送的消息 List 发送出去。
9)批次发送的条件为: 缓冲区数据大小达到 batch.size 或者 linger.ms 达到上限,哪个先达到就算哪个。
02
内存缓冲造成的频繁GC问题
内存缓冲机制说白了,其实就是把多条消息组成一个Batch,一次网络请求就是一个Batch 或者多个 Batch。这样避免了一条消息一次网络请求,从而提升了吞吐量。
那么问题来了,试想一下一个 Batch 中的数据取出来封装到网络包里,通过网络发送到达 Kafka 服务端。此时这个 Batch 里的数据都发送过去了,里面的数据该怎么处理?这些 Batch 里的数据还存在客户端的 JVM 的内存里!那么一定要避免任何变量去引用 Batch 对应的数据,然后尝试触发 JVM 自动回收掉这些内存垃圾。这样不断的让 JVM 进行垃圾回收,就可以不断的腾出来新的内存空间让后面新的数据来使用。
想法是挺好,但实际生产运行的时候最大的问题,就是 JVM Full GC 问题。JVM GC 在回收内存垃圾的时候,会有一个「Stop the World」的过程,即垃圾回收线程运行的时候,会导致其他工作线程短暂的停顿,这样可以踏踏实实的回收内存垃圾。
试想一下,在回收内存垃圾的时候,工作线程还在不断的往内存里写数据,那如何让JVM 回收垃圾呢?我们看看下面这张图就更加清楚了:
虽然现在 JVM GC 演进越来越先进,从 CMS 垃圾回收器到 G1 垃圾回收器,核心的目标之一就是不断的缩减垃圾回收的时候,导致其他工作线程停顿的时间。但是再先进的垃圾回收器这个停顿的时间还是存在的。
因此,如何尽可能在设计上避免 JVM 频繁的 Full GC 就是一个非常考验其设计水平了。
03
Kafka 实现的缓冲机制
在 Kafka 客户端内部,针对这个问题实现了一个非常优秀的机制,就是「缓冲池机制」。即每个 Batch 底层都对应一块内存空间,这个内存空间就是专门用来存放写进去的消息。
当一个 Batch 数据被发送到了 kafka 服务端,这个 Batch 的内存空间不再使用了。此时这个 Batch 底层的内存空间先不交给 JVM 去垃圾回收,而是把这块内存空间给放入一个缓冲池里。
这个缓冲池里存放了很多块内存空间,下次如果有一个新的 Batch 数据了,那么直接从缓冲池获取一块内存空间是不是就可以了?然后如果一个 Batch 数据发送出去了之后,再把内存空间还回来是不是就可以了?以此类推,循环往复。
我们看看下面这张图就更加清楚了:
一旦使用了这个缓冲池机制之后,就不涉及到频繁的大量内存的 GC 问题了。
初始化分配固定的内存,即32MB。然后把 32MB 划分为 N 多个内存块,一个内存块默认是16KB,这样缓冲池里就会有很多的内存块。然后如果需要创建一个新的 Batch,就从缓冲池里取一个 16KB 的内存块就可以了。
接着如果 Batch 数据被发送到 Kafka 服务端了,此时 Batch 底层的内存块就直接还回缓冲池就可以了。这样循环往复就可以利用有限的内存,那么就不涉及到垃圾回收了。没有频繁的垃圾回收,自然就避免了频繁导致的工作线程的停顿了,JVM Full GC 问题是不是就得到了大幅度的优化?
没错,正是这个设计思想让 Kafka 客户端的性能和吞吐量都非常的高,这里蕴含了大量的优秀的机制。
谈谈你对 Kafka 消息语义是如何理解的?
对于 Kafka 来说,当消息从 Producer 到 Consumer,有许多因素来影响消息的消费,因此「消息传递语义」就是 Kafka 提供的 Producer 和 Consumer 之间的消息传递过程中消息传递的保证性。主要分为三种, 如下图所示:
对于这三种语义,我们来看一下可能出现的场景:
01
Producer端
生产者发送语义:首先当 Producer 向 Broker 发送数据后,会进行消息提交,如果成功消息不会丢失。因此发送一条消息后,可能会有几种情况发生:
1)遇到网络问题造成通信中断, 导致 Producer 端无法收到 ack,Producer 无法准确判断该消息是否已经被提交, 又重新发送消息,这就可能造成 「at least once」语义。
2)在 Kafka 0.11之前的版本,会导致消息在 Broker 上重复写入(保证至少一次语义),但在0.11版本开始,通过引入「PID及Sequence Number」支持幂等性,