# 轻松过关kafka面试
首先来一张kafka架构图,咱们围绕它展开。
从图片看出,kafka
有 producer
、broker
、consumer
概念。
producer
可以组成producer group
,consumer
可以组成 consumer group
,为啥需要组?因为“人”多力量大嘛,一组充分发挥了消息生产的优势、消息消费的优势。夹在中间的就是Kafka cluster
,节点称之为broker
,broker内部有各种消息主题,称之为topic
。
topic存放在partition
上,为了保证系统可靠性,对每个partition进行分组,每组partition在broker上唯一,为了防止broker故障,需要做灾备,这时候就有了partition多副本
,副本间当然要分开放咯,哪台机器不行了就轮到另外机器的副本上,谁上位谁就是leader
,没上位的就是follower
,这种上位机制需要zookeeper
协助。
producer group的消息也是发送到leader上,follower同步leader的消息,保证挂一个另外一个能成为替补,这时候又会引入ISR
和OSR
概念,它两一起组成了AR
。这个概念意思是,跟得上同步leader数据的follower随leader进入ISR,没跟得上的踢进OSR,直到它同步好了,再恢复到ISR中。搞出个这个玩意就是为了防止信息同步不完整的partition的follower在leader被踢出ISR时成为了新leader,所以当它还没成为leader之前就把它踢出去。follower 副本是否与leader同步的判断标准取决于 Broker 端参数 replica.lag.time.max.ms
(默认为10秒),follower 默认每隔 500ms 向 leader fetch 一次数据,只要一个 Follower 副本落后 Leader 副本的时间不连续超过10秒,那么 Kafka 就认为该 Follower 副本与 leader 是同步的。在正常情况下,所有的 follower 副本都应该与 leader 副本保持一定程度的同步,即 AR=ISR,OSR 集合为空。
多副本机制解决单机故障问题时,也带来了多副本间数据同步一致性问题。Kafka 通过HW高水位更新机制、副本同步机制、 Leader Epoch 等多种措施解决了多副本间数据同步一致性问题,下面我们来依次看下这几大措施:
producer怎么认为消息发送出去了呢,有个消息确认机制ACK
。这个ACK有三种值-1、0、1,分别含义:
Request.required.acks = 0
:请求发送即认为成功,不关心有没有写成功,常用于日志进行分析场景。Request.required.acks = 1
:当 leader partition 写入成功以后,才算写入成功,有丢数据的可能。Request.required.acks= -1
:ISR 列表里面的所有副本都写完以后,这条消息才算写入成功,强可靠性保证。
为了实现强可靠的 kafka 系统,我们需要设置Request.required.acks= -1,同时还会设置集群中处于正常同步状态的副本 follower 数量min.insync.replicas>2,另外,设置 unclean.leader.election.enable=false 使得集群中 ISR 的 follower 才可变成新的 leader,避免特殊情况下消息截断的出现。
三种值对应三种不同的场景,有各自的用途,选择合适的配置对应在业务场景中就行。
当然为了防止partition上的数据记录无限增长导致文件巨大,对partition上的数据文件(.log
结尾的文件)进行切割成segment
,同时每条记录在segment上做了有序递增的offset
,这个可以减少二分查找耗时。当然offset只能保证单个partition内有序递增,不能保证partition组集体的有序。
这个offset在哪些地方用得到呢?
- follower同步leader数据
- 记录consumer消费topic数据在partition的segment中.log数据文件上的消费点位。
根据上面对Kafka架构拆解分析,现在开始手撕面试概念题了:
- producer group 往Kafka cluster 写数据的分区策略有哪些?
- 轮询策略:也称Round-robin策略,即顺序分配。
- 随机策略:也称Randomness策略。所谓随机就是我们随意地将消息放置到任意一个分区上。
- 消息键保序策略:按照消息key规则,让消息进入指定的分区。
- 地理位置分区策略:特别是跨城市、跨国家甚至是跨大洲的集群。。
轮询策略的解释,比如一个 Topic 下有 3个分区,那么第一条消息被发送到分区0,第二条被发送到分区1,第三条被发送到分区2,以此类推。轮询策略有非常优秀的负载均衡表现,它总是能保证消息最大限度地被平均分配到所有分区上,故默认情况下它是最合理的分区策略,也是我们最常用的分区策略之一。
消息键分配策略的解释,一旦消息被定义了 Key,那么你就可以保证同一个 Key 的所有消息都进入到相同的分区里面,比如订单 ID,那么绑定同一个 订单 ID 的消息都会发布到同一个分区,由于每个分区下的消息处理都是有顺序的,故这个策略被称为按消息键保序策略。
- 如何保证数据不丢失?
数据丢失出现的情况有三种:
a. producer - kafka
b. kafka broker crash自身宕机 / Kafka restart集群重启
c. consumer - kafka
首先我们对a情况展开,
当ACK=0时,本身就不关心数据丢失情况,这种适合高并发产生的日志,
当ACK!=0时,生产者写数据到Kafka,分同步(sync)和异步(async)
异步策略时见下图
异步写入场景时,写 kafka 的错误信息,我们暂时仅能够从这个错误日志来得知具体发生了什么错,并且也不支持我们自建函数进行兜底处理。
同步策略时见下图
同步发送在一定程度上确保了我们在跨网络向 Broker 传输消息时,消息一定可以可靠地传输到 Broker。因为在同步发送场景我们可以明确感知消息是否发送至 Broker,若因网络抖动、机器宕机等故障导致消息发送失败或结果不明,可通过重试等手段确保消息至少一次(at least once) 发送到 Broker。另外,Kafka(0.11.0.0版本后)还为 Producer 提供两种机制来实现精确一次(exactly once) 消息发送:幂等性(Idempotence)和事务(Transaction)。
接着我们聊聊Kafka broker端丢失情况,为了确保 Producer 收到 Broker 的成功 ack 后,消息一定不在 Broker 环节丢失,我们核心要关注以下几点:
- Broker 返回 Producer 成功 ack 时,消息是否已经落盘;
- Broker 宕机是否会导致数据丢失,容灾机制是什么;
- Replica 副本机制带来的多副本间数据同步一致性问题如何解决;
kafka 为了获得更高吞吐,Broker 接收到消息后只是将数据写入 PageCache 后便认为消息已写入成功,而 PageCache 中的数据通过 linux 的 flusher 程序进行异步刷盘(刷盘触发条:主动调用 sync 或 fsync 函数、可用内存低于阀值、dirty data 时间达到阀值),将数据顺序写到磁盘。
由于消息是写入到 PageCache ,单机场景,如果还没刷盘 Broker 就宕机了,那么 Producer 产生的这部分数据就可能丢失。为了解决单机故障可能带来的数据丢失问题,Kafka 为分区引入了副本机制。
通过副本机制并结合 ACK 策略可以大概率规避单机宕机带来的数据丢失问题,并通过 HW、副本同步机制、 Leader Epoch 等多种措施解决了多副本间数据同步一致性问题,最终实现了 Broker 数据的可靠持久化。
最后我们讨论consumer,Consumer 在消费消息的过程中需要向 Kafka 汇报自己的位移数据,只有当 Consumer 向 Kafka 汇报了消息位移,该条消息才会被 Broker 认为已经被消费。因此,Consumer 端消息的可靠性主要和 offset 提交方式有关,Kafka 消费端提供了两种消息提交方式:
提交方式 | 参数设置 | 功能特点 | 注意事项 |
---|---|---|---|
手动提交 | enable.auto.commit = false | 开发人员根据程序的逻辑在合适的地方进行位移提交,可以实现消息消费的精确控制。 | 如果某条消息处理完成后还没来得及提交位移,系统发生重启等故障,则该条未提交的消息可能会被重复消费。 |
自动提交 | enable.auto.commit = true , auto.commit.interval.ms(提交间隔,默认5s) | 不是每消费一条消息就提交一次,而是定期提交。自动位移提交的动作是在 poll()方法的逻辑里完成的,在每次真正向broker发起拉取请求之前会检查是否可以进行位移提交,如果可以,那么就会提交上一次轮询的位移。 | 假设拉取一批消息进行消费,在下一次自动提交消费位移之前,消费者崩溃了,那么又得从上一次位移贺交的地方重新开始消费,则多条未被提交的消息可能会被重复消费 。 |
- 如何保证消息有序?
Kafka单分区内有序,如需全局有序,那么只能将topic分区数设置为1,这样做满足了要求,丢失了性能。
既要保证性能,又要保证全局有序只能在下游处理了,比如通过hive的row_number。
- Kafka基于磁盘存储数据,为何速度这么快?
- Reactor I/O 网络模型:通过 I/O 多路复用机制,Kafka 能够同时处理大量的网络连接请求,而不需要为每个连接创建一个线程,从而节省了系统资源。
- 顺序写入:Kafka 使用顺序写入的方式将消息追加到日志文件的末尾,避免了文件位置的频繁变动,从而减少了锁的使用。
- MMAP 内存映射文件:Kafka 使用内存映射文件(Memory Mapped File)来访问日志数据和索引文件。这种方式使得文件数据可以直接映射到进程的虚拟地址空间中,从而减少了系统调用的开销,提高了数据访问的效率。
- 零拷贝:Kafka 使用零拷贝(Zero Copy)技术,将数据从磁盘直接传输到网络,绕过了用户态的复制过程,大大提高了数据传输的效率。
- 数据压缩和批量处理:数据压缩在 Kafka 中有助于减少磁盘空间的使用和网络带宽的消耗,从而提升整体性能。;Kafka 支持批量处理消息,在一个批次中同时处理多个消息,减少了网络和 I/O 的开销。
- 如何调优Kafka集群?
生产者调优
服务端broker调优
消费者调优
系统调优
# 文件句柄数
ulimit -n 1000000
# 网络缓冲区
sysctl -w net.core.rmem_max=2500000
sysctl -w net.core.wmem_max=2500000
# TCP 优化
sysctl -w net.ipv4.tcp_window_scaling=1
- 实际开发以及使用Kafka过程中遇到了哪些问题?
- 分区数据量不均衡:在使用Kafka时,可能会遇到分区数据量不均衡的问题,即某些分区负载过重而其他分区空闲。这通常是由于生产者发送数据时没有指定key,导致数据不能均匀分布到各个分区。解决方法是在生产者发送数据时指定key,让Kafka根据key的值通过哈希算法将数据均匀分布到不同的分区上。
- 资源消耗过大:在使用spring-integration-kafka时,可能会出现CPU和内存占用过高的问题,这是因为spring-- integration-kafka会将Topic中的数据全部拉取到本地缓存。解决方法是通过配置标签中的capacity属性来限制本地缓存的数据量。
- 数据丢失问题:在Kafka中,数据丢失可能发生在生产者发送数据、Kafka broker接收数据或消费者读取数据的过程中。解决方法包括确保生产者的幂等性、增加数据备份和使用事务来保证数据的完整性和可靠性。
- 数据重复问题:消费者在读取数据时可能会因为异常或网络延迟等原因导致数据被重复消费。解决方法是确保消费者的幂等性、在每个消息中增加唯一标识以及使用事务来保证数据的完整性和可靠性。
- zookeeper在Kafka中的作用?
低于0.11版本
- Broker 注册:每个 Kafka broker 启动时,会在 ZooKeeper 中注册自己。ZooKeeper 维护一个包含所有活跃 brokers 列表的注册表。当新 broker 加入或现有 broker 离开时,ZooKeeper 会更新这个列表。
- Leader 选举:Kafka 使用 ZooKeeper 来进行分区副本的 Leader 选举。每个分区都有一个 Leader 和若干个 Follower 副本,Leader 负责处理所有读写请求。当 Leader 失败时,ZooKeeper 协调选举一个新的 Leader。
- 元数据管理:Kafka 的所有元数据(例如 topic、分区、副本)都存储在 ZooKeeper 中。ZooKeeper 充当一个集中管理点,使得 Kafka 集群中的各个节点可以获取最新的元数据信息。
- 消费者组offset维护
0.11版本 - 3.0版本之前
- 取消了消费者组offset维护,内部topic
__consumer_offset
管理。
3.0版本及其以后
在Kafka 3.0版本中,Zookeeper不再是必需的组件。Kafka引入了 Kraft机制 ,这是一种内置的控制器选举和元数据存储机制,用于替代Zookeeper的功能。具体来说:
- 控制器选举:在Kafka 3.0中,使用Kraft机制实现controller主控制器的选举,不再依赖Zookeeper。通过
Kraft
机制,可以从多个controller中选举出一个主控制器,其他controller作为备用控制器。 - 元数据存储:相关的集群元数据信息以Kafka日志的形式存在,而不是存储在Zookeeper中。这意味着元数据不再依赖于Zookeeper,而是存储在Kafka的日志中。
文章参考
Kafka常用调优方式总结
Kafka 如何调优
深入解析kafka高性能背后的实现原理