无论是作为面试官,还是应聘者,我都接触过很多 Kafka 面试题。而在最近面试了很多候选人,发现写了熟悉 Kafka,但是对于 Kafka 相关的知识却是只知道大概用处,简单搭建和使用。我想说,虽然我们是 SRE (可靠性工程师),但不论你是业务层的 SRE 还是基础设施层的 SRE,我们都需要对业务方的使用场景有足够理解,或者对我们要提供的服务有足够的了解才行,这样你才能整体的保证你的业务连续性以及业务可靠性。因此,专门总结了如下经典的 kafka 面试详解。
以下面试题,参考胡大的 Kafka 核心源码解读
,对相关的知识进行了补充和思考。
能问这道题,主要是想看候选人对于 kafka 的使用场景以及定位认知理解有多深,同时候可以知道候选人对于这项技术的关注度。
我们都知道,在开源软件中,大部分软件随着用户量的增加,整个软件的功能和定位也有了新的变化,而 Apache Kafka 一路发展到现在,已经由最初的分布式提交日志系统逐渐演变成了实时流处理框架。
因此,这道题你最好这么回答:Apach Kafka 是一款分布式流处理平台,用于实时构建流处理应用。它有一个核心的功能广为人知,即作为企业级的消息引擎被广泛使用 (通常也会称之为消息总线 message bus)。
关于分布式流处理平台
,其实从它官方的 logo 以及 slogan 我们就很容易看出来。
消费者组是 Kafka 独有的概念,如果面试官问这个,就说明他对此是有一定了解的。
胡大给的标准答案是: 官网上的介绍言简意赅,即消费者组是 Kafka 提供的可扩展且具有容错性的消费者机制。
但实际上,消费者组 (Consumer Group) 其实包含两个概念,作为队列
,消费者组允许你分割数据处理到一组进程集合上 (即一个消费者组中可以包含多个消费者进程,他们共同消费该 topic 的数据),这有助于你的消费能力的动态调整;作为发布-订阅模型(publish-subscribe)
,kafka 允许你将同一份消息广播到多个消费者组里,以此来丰富多种数据使用场景。
需要注意的是: 在消费者组中,多个实例共同订阅若干个主题,实现共同消费。同一个组下的每个实例都配置有相同的组 ID,被分配不同的订阅分区。当某个实例挂掉的时候,其他实例会自动地承担起它负责消费的分区。 因此,消费者组在一定程度上也保证了消费者程序的高可用性。
kafka-consumer-group注意:
消费者组的题目,能够帮你在某种程度上掌控下面的面试方向。
位移值原理(Offset)
,就不妨再提一下消费者组的位移提交机制;Kafka Broker
,可以提一下消费者组与 Broker 之间的交互;Producer
,那么就可以这么说:“消费者组要消费的数据完全来自于 Producer 端生产的消息,我对 Producer 还是比较熟悉的。”总之,你总得对 consumer group
相关的方向有一定理解,然后才能像面试官表名你对某一块很理解。
这道题,也是我经常会问候选人的题,因为任何分布式系统中虽然都通过一些列的算法去除了传统的关系型数据存储,但是毕竟还是有些数据要存储的,同时分布式系统的特性往往是需要有一些中间人角色来统筹集群。比如我们在整个微服务框架中的 Dubbo
,它也是需要依赖一些注册中心或配置中心类的中间件的,以及云原生的 Kubernetes 使用 Etcd
作为整个集群的枢纽。
标准答案: 目前,Kafka 使用 ZooKeeper 存放集群元数据、成员管理、Controller 选举,以及其他一些管理类任务。之后,等 KIP-500 提案完成后,Kafka 将完全不再依赖于 ZooKeeper。
KIP-500 思想,是使用社区自研的基于 Raft 的共识算法,替代 ZooKeeper,实现 Controller 自选举。
标准答案: 在 Kafka 中,每个主题分区下的每条消息都被赋予了一个唯一的 ID 数值,用于标识它在分区中的位置。这个 ID 数值,就被称为位移,或者叫偏移量。一旦消息被写入到分区日志,它的位移值将不能被修改。
答完这些之后,你还可以把整个面试方向转移到你希望的地方:
位移值
和消费者位移值
之间的区别推荐的答案: Kafka 副本当前分为领导者副本和追随者副本。只有 Leader 副本才能对外提供读写服务,响应 Clients 端的请求。Follower 副本只是采用拉(PULL)的方式,被动地同步 Leader 副本中的数据,并且在 Leader 副本所在的 Broker 宕机后,随时准备应聘 Leader 副本。
加分点:
注意:
之前确保一致性的主要手段是高水位机制 (HW),但高水位值无法保证 Leader 连续变更场景下的数据一致性,因此,社区引入了 Leader Epoch
机制,来修复高水位值的弊端。
关于高水位和 Leader Epoch 的讨论
对于 SRE 来讲,该题简直是送分题啊,但是,最大消息的设置通常情况下有生产者端,消费者端,broker 端和 topic 级别的参数,我们需要正确设置,以保证可以正常的生产和消费。
message.max.bytes
,max.message.bytes(topic级别)
,replica.fetch.max.bytes(否则follow会同步失败)
fetch.message.max.bytes
对于 SRE 来讲,依然是送分题。但基础的我们要知道,kafka 本身是提供了 jmx(Java Management Extensions)
的,我们可以通过它来获取到 kafka 内部的一些基本数据。
kafka-run-class.sh kafka.tools.JmxTool
来查看具体的用法。其实对于 SRE 还是送分题,因为目前来讲大部分公司的业务系统都是使用 Java 开发,因此 SRE 对于基本的 JVM 相关的参数应该至少都是非常了解的,核心就在于 JVM 的配置以及 GC 相关的知识。
标准答案: 任何 Java 进程 JVM 堆大小的设置都需要仔细地进行考量和测试。一个常见的做法是,以默认的初始 JVM 堆大小运行程序,当系统达到稳定状态后,手动触发一次 Full GC,然后通过 JVM 工具查看 GC 后的存活对象大小。之后,将堆大小设置成存活对象总大小的 1.5~2 倍。对于 Kafka 而言,这个方法也是适用的。不过,业界有个最佳实践,那就是将 Broker 的 Heap Size 固定为 6GB。经过很多公司的验证,这个大小是足够且良好的。
该题也算是 SRE 的送分题吧,对于 SRE 来讲,任何生产的系统第一步需要做的就是容量预估以及集群的架构规划,实际上也就是机器数量和所用资源之间的关联关系,资源通常来讲就是 cpu,内存,磁盘容量,带宽。但需要注意的是,kafka 因为独有的设计,对于磁盘的要求并不是特别高,普通机械硬盘足够,而通常的瓶颈会出现在带宽上。
在预估磁盘的占用时,你一定不要忘记计算副本同步的开销。如果一条消息占用 1KB 的磁盘空间,那么,在有 3 个副本的主题中,你就需要 3KB 的总空间来保存这条消息。同时,需要考虑到整个业务 Topic 数据保存的最大时间,以上几个因素,基本可以预估出来磁盘的容量需求。
需要注意的是:对于磁盘来讲,一定要提前和业务沟通好场景,而不是等待真正有磁盘容量瓶颈了才去扩容磁盘或者找业务方沟通方案。
对于带宽来说,常见的带宽有 1Gbps 和 10Gbps,通常我们需要知道,当带宽占用接近总带宽的 90% 时,丢包情形就会发生。
对于有经验的 SRE 来讲,早期的 kafka 版本应该多多少少都遇到过该种情况,通常情况下就是 controller
不工作了,导致无法分配 leader,那既然知道问题后,解决方案也就很简单了。重启 controller 节点上的 kafka 进程,让其他节点重新注册 controller 角色,但是如上面 zookeeper
的作用,你要知道为什么 controller
可以自动注册。
当然了,当你知道 controller
的注册机制后,你也可以说: 删除 ZooKeeper 节点 /controller,触发 Controller 重选举。Controller 重选举能够为所有主题分区重刷分区状态,可以有效解决因不一致导致的 Leader 不可用问题。但是,需要注意的是,直接操作 zookeeper
是一件风险很大的操作,就好比在 Linux 中执行了 rm -rf /xxx 一样,如果在 /
和 xxx
之间不小心多了几个空格,那 "恭喜你",今年白干了。
讲真,我不认为这是炫技的题目,特别是作为 SRE 来讲,对于一个开源软件的原理以及概念的理解,是非常重要的。
需要注意的是,通常在 ISR
中,可能会有人问到为什么有时候副本不在 ISR 中,这其实也就是上面说的 Leader 和 Follower 不同步的情况,为什么我们前面说,短暂的不同步我们可以关注,但是长时间的不同步,我们需要介入排查了,因为 ISR 里的副本后面都是通过 replica.lag.time.max.ms
,即 Follower 副本的 LEO 落后 Leader LEO 的时间是否超过阈值来决定副本是否在 ISR 内部的。
Kafka 不需要用户手动删除消息。它本身提供了留存策略,能够自动删除过期消息。当然,它是支持手动删除消息的。
cleanup.policy=compact
的主题而言,我们可以构造一条 的消息发送给 Broker,依靠 Log Cleaner 组件提供的功能删除掉该 Key 的消息。这是一个内部主题,主要用于存储消费者的偏移量,以及消费者的元数据信息 (消费者实例,消费者id
等等)
需要注意的是: Kafka 的 GroupCoordinator
组件提供对该主题完整的管理功能,包括该主题的创建、写入、读取和 Leader 维护等。
分区的 Leader 副本选举对用户是完全透明的,它是由 Controller
独立完成的。你需要回答的是,在哪些场景下,需要执行分区 Leader 选举。每一种场景对应于一种选举策略。
这 4 类选举策略的大致思想是类似的,即从 AR 中挑选首个在 ISR 中的副本,作为新 Leader。
其实这道题对于 SRE 来讲,有点超纲了,不过既然 Zero Copy
是 kafka 高性能的保证,我们需要了解它。
Zero Copy 是特别容易被问到的高阶题目。在 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 特性了。
这其实是分布式场景下的通用问题,因为我们知道 CAP 理论下,我们只能保证 C (可用性) 和 A (一致性) 取其一,如果支持读写分离,那其实对于一致性的要求可能就会有一定折扣,因为通常的场景下,副本之间都是通过同步来实现副本数据一致的,那同步过程中肯定会有时间的消耗,如果支持了读写分离,就意味着可能的数据不一致,或数据滞后。
Leader/Follower 模型并没有规定 Follower 副本不可以对外提供读服务。很多框架都是允许这么做的,只是 Kafka 最初为了避免不一致性的问题,而采用了让 Leader 统一提供服务的方式。
不过,自 Kafka 2.4 之后,Kafka 提供了有限度的读写分离,也就是说,Follower 副本能够对外提供读服务。
作为 SRE 来讲,任何生产环境的调优,首先需要识别问题和瓶颈点,而不是随意的进行臆想调优。随后,需要确定优化目标,并且定量给出目标
对于 kafka 来讲,常见的调优方向基本为:吞吐量、延时、持久性和可用性,每种目标之前都是由冲突点,这也就要求了,我们在对业务接入使用时,要进行业务场景的了解,以对业务进行相对的集群隔离,因为每一个方向的优化思路都是不同的,甚至是相反的。
确定了目标之后,还要明确优化的维度。有些调优属于通用的优化思路,比如对操作系统、JVM 等的优化;有些则是有针对性的,比如要优化 Kafka 的 TPS。我们需要从 3 个方向去考虑:
batch.size
和 linger.ms
,启用压缩,关闭重试num.replica.fetchers
提升 Follower 同步 TPS,避免 Broker Full GC 等。fetch.min.bytes
这道题目能够诱发我们对分布式系统设计、CAP 理论、一致性等多方面的思考。
一旦发生 Controller 网络分区,那么,第一要务就是查看集群是否出现 “脑裂”,即同时出现两个甚至是多个 Controller 组件。这可以根据 Broker 端监控指标 ActiveControllerCount 来判断。
不过,通常而言,我们在设计整个部署架构时,为了避免这种网络分区的发生,一般会将 broker 节点尽可能的防止在一个机房或者可用区。
由于 Controller 会给 Broker 发送 3 类请求,LeaderAndIsrRequest
,StopReplicaRequest
,UpdateMetadataRequest
,因此,一旦出现网络分区,这些请求将不能顺利到达 Broker 端。
这将影响主题的创建、修改、删除操作的信息同步,表现为集群仿佛僵住了一样,无法感知到后面的所有操作。因此,网络分区通常都是非常严重的问题,要赶快修复。
在回答之前,如果先把这句话说出来,一定会加分:Java Consumer 是双线程的设计。一个线程是用户主线程,负责获取消息;另一个线程是心跳线程,负责向 Kafka 汇报消费者存活情况。将心跳单独放入专属的线程,能够有效地规避因消息处理速度慢而被视为下线的 “假死” 情况。
单线程获取消息的设计能够避免阻塞式的消息获取方式。单线程轮询方式容易实现异步非阻塞式,这样便于将消费者扩展成支持实时流处理的操作算子。因为很多实时流处理操作算子都不能是阻塞式的。另外一个可能的好处是,可以简化代码的开发。多线程交互的代码是非常容易出错的。
首先,Follower 发送 FETCH 请求给 Leader。
接着,Leader 会读取底层日志文件中的消息数据,再更新它内存中的 Follower 副本的 LEO 值,更新为 FETCH 请求中的 fetchOffset 值。
最后,尝试更新分区高水位值。Follower 接收到 FETCH 响应之后,会把消息写入到底层日志,接着更新 LEO 和 HW 值。
Leader 和 Follower 的 HW 值更新时机是不同的,Follower 的 HW 更新永远落后于 Leader 的 HW。这种时间上的错配是造成各种不一致的原因。
因此,对于消费者而言,消费到的消息永远是所有副本中最小的那个 HW。
胡老师的 kafka 源码课程