05 - Kafka消息队列 · 高频面试考题背诵指南


目录


一、Kafka 核心概念

Q1:Kafka 的整体架构?核心组件关系?

核心考点:Broker/Topic/Partition/Replica/Producer/Consumer/ZooKeeper/Controller 各自职责。

背诵话术

Kafka 的核心组件

  • Broker:Kafka 的服务节点,一个 Kafka 集群由多个 Broker 组成。每个 Broker 负责存储和提供一部分分区的数据。
  • Topic(主题):消息的逻辑分类,生产者向特定 Topic 发送消息,消费者从 Topic 订阅消息。
  • Partition(分区):Topic 的物理分片,每个 Topic 可以有多个 Partition,分布在不同的 Broker 上。Partition 内部消息有序,是 Kafka 并行消费的基础。Kafka 的最大并发度 = Partition 数量
  • Replica(副本):每个 Partition 有多个副本(一个 Leader + 多个 Follower)。Leader 负责所有读写,Follower 异步从 Leader 同步数据。Leader 所在的 Broker 宕机后,从 ISR 中选举新 Leader。
  • ISR(In-Sync Replicas):与 Leader 保持同步的副本列表。如果 Follower 长时间未同步(超过 replica.lag.time.max.ms,默认 10s),会被踢出 ISR。
  • Producer(生产者):向 Kafka 发送消息,可指定 Topic 和 Partition。
  • Consumer Group(消费者组):一组消费者的逻辑分组。同一个 Topic 的同一个 Partition,同一时刻只能被同一消费者组内的一个消费者消费,实现负载均衡;不同消费者组之间互相独立,实现广播效果。
  • ZooKeeper(Kafka 3.0 前):负责存储 Broker 元数据、Controller 选举、Topic 配置、Consumer Offset 等信息(新版本 Kafka 已用 KRaft 自管理替代 ZooKeeper)。

Q2:Kafka 为什么这么快?

核心考点:五大性能利器:顺序写磁盘、PageCache、零拷贝、批量发送、消息压缩。

背诵话术

Kafka 在大数据量下依然能保持极高吞吐的核心原因有以下几点:

  1. 顺序写磁盘(Sequential Write):Kafka 的消息追加写到日志文件末尾,是顺序 IO 操作。机械硬盘的顺序写速度可达 600MB/s,远超随机写的 100MB/s,SSD 顺序写更快。Kafka 日志文件的设计决定了它的磁盘写性能极高。

  2. PageCache(内存映射):Kafka 不直接将数据写到磁盘文件,而是先写到操作系统的 PageCache(内核缓冲区),由 OS 异步刷盘。消费者读取最新消息时,数据大概率还在 PageCache 中,直接内存读取,无需磁盘 IO。

  3. 零拷贝(Zero-Copy):传统的文件传输需要 4 次数据拷贝(磁盘→内核缓冲区→用户空间→Socket缓冲区→网卡)。Kafka 使用 Linux 的 sendfile() 系统调用,直接将 PageCache 中的数据发送到网卡,只需 2 次拷贝,避免了数据经过用户空间的来回拷贝,大幅降低 CPU 消耗和延迟。

  4. 批量发送(Batch):Producer 不是每条消息都立即发送,而是通过 batch.sizelinger.ms 配置,将多条消息攒成一批再一起发送,减少网络请求次数。

  5. 消息压缩(Compression):支持 GZIP、Snappy、LZ4、ZSTD 等压缩算法,减小消息体积,降低网络传输带宽消耗。压缩和解压都在 Producer/Consumer 端完成,Broker 只负责存储压缩后的数据。


Q3:Kafka 消息存储结构

核心考点:.log / .index / .timeindex 文件的关系,稀疏索引的查找过程。

背诵话术

每个 Partition 对应一个目录,目录下按照”日志段(LogSegment)”分割成多组文件:

  • .log 文件:存储实际的消息数据,文件名是该 Segment 中第一条消息的 offset(如 00000000000000000000.log)。消息以追加方式写入。
  • .index 文件(偏移量索引):记录 offset → 文件物理位置 的映射关系,是稀疏索引(不是每条消息都建索引,而是每隔一定字节建一个索引项)。文件名与 .log 相同。
  • .timeindex 文件(时间戳索引):记录 timestamp → offset 的映射,支持按时间戳查找消息。

消息查找过程(如查找 offset = 368772 的消息):

  1. 对所有 LogSegment 的起始 offset 进行二分查找,定位到消息所在的 LogSegment(如起始 offset 为 368769 的 Segment)。
  2. 在该 Segment 的 .index 文件中二分查找,找到不超过目标 offset 的最大索引项,获取对应的物理位置(如 position = 256)。
  3. .log 文件的 position 256 开始顺序读取,直到找到 offset = 368772 的消息。

Q4:ISR、HW、LEO 分别是什么?

核心考点:ISR 的动态维护机制,HW 保证消费者只能读到已提交的消息。

背诵话术

  • LEO(Log End Offset):每个副本(Leader 或 Follower)当前最新消息的下一个偏移量,代表该副本日志的末端位置。Leader 的 LEO 最大,Follower 会不断追赶 Leader 的 LEO。

  • HW(High Watermark,高水位):HW = ISR 中所有副本 LEO 的最小值。消费者只能消费到 HW 之前的消息,HW 之后的消息即使已写入 Leader 也不可见(未提交)。这样保证了即使 Leader 宕机,从 ISR 中选出的新 Leader 上也一定有 HW 之前的所有消息,不会丢失已提交的数据。

  • ISR(In-Sync Replicas):与 Leader 保持同步的副本集合,由 Leader 动态维护。如果某个 Follower 超过 replica.lag.time.max.ms(默认 10s)未向 Leader 发送心跳或同步请求,Leader 会将其从 ISR 中踢出(降为 OSR)。Follower 追上后重新加入 ISR。min.insync.replicas 配置了保证消息不丢失所需的最小 ISR 数量。


二、生产者

Q5:Kafka 生产者发送消息的完整流程

核心考点:客户端 Sender 线程、RecordAccumulator 批次累积、网络发送。

背诵话术

生产者调用 send() 方法后,消息不会立即发送,而是经过以下流程:

  1. 拦截器链(Interceptors):对消息进行预处理(如添加头信息、过滤、监控统计)。
  2. 序列化器(Serializer):将 Key 和 Value 序列化为字节数组。
  3. 分区器(Partitioner):决定消息发送到哪个 Partition(默认按 Key 哈希,无 Key 时轮询或粘性分配)。
  4. RecordAccumulator(消息累加器):消息被放入对应 Broker 的 RecordBatch 中,当 Batch 大小达到 batch.size(默认 16KB)或等待时间超过 linger.ms(默认 0ms,即不等待),Batch 被标记为就绪,等待发送。
  5. Sender 线程:从 RecordAccumulator 中取出就绪的 Batch,通过 NetworkClient 建立 TCP 连接,将 Batch 发送给对应的 Leader Broker。
  6. ACK 确认:根据 acks 配置等待 Broker 的确认响应,失败时按 retries 配置进行重试。

Q6:如何保障 Kafka 消息零丢失?

核心考点:三端闭环:Producer(acks=all + 幂等)+ Broker(min.insync.replicas≥2)+ Consumer(手动提交)。

背诵话术

在招行调度中台和金融结算场景中,消息零丢失是底线,需要从生产者、Broker、消费者三端同时配置:

Producer 端

  • acks = all(或 -1):消息必须被 Leader + ISR 中所有 Follower 都确认写入,才算发送成功,避免 Leader 宕机时 Follower 没有数据的情况。
  • retries = Integer.MAX_VALUE:网络抖动等临时故障自动重试。
  • enable.idempotence = true:开启幂等性,防止重试导致消息重复写入(详见 Q8)。

Broker 端

  • min.insync.replicas >= 2:要求 ISR 中至少有 2 个副本(含 Leader)才能确认消息,结合 acks=all 使用,确保至少有 1 个 Follower 也写入了数据,即使 Leader 宕机也不丢失。
  • 设置 unclean.leader.election.enable = false:禁止落后的 Follower(不在 ISR 中)参与 Leader 选举,防止选举出的新 Leader 数据不全。

Consumer 端

  • enable.auto.commit = false:关闭自动提交 offset。
  • 必须在业务逻辑成功处理完消息并完成数据库持久化后,才手动提交 offset(调用 consumer.commitSync()commitAsync())。如果消费过程中抛出异常,不提交 offset,消息会被重新消费,配合幂等消费防止业务重复处理。

Q7:Kafka 生产者幂等性原理?如何开启?

核心考点:ProducerID + Sequence Number 在 Broker 端去重,防止 retry 导致重复消息。

背诵话术

问题背景acks=all + retries 配置下,如果 Broker 已成功写入消息但 ACK 响应在网络中丢失,Producer 会认为发送失败而重试,导致 Broker 收到重复的消息。

幂等性原理:开启幂等性后(enable.idempotence = true,会同时强制设置 acks=allretries=MAX),Kafka 给每个 Producer 分配一个全局唯一的 PID(Producer ID),并为每条发往同一 Partition 的消息分配一个单调递增的 Sequence Number(序列号)

Broker 端维护了每个 (PID, Partition) 组合的最大已接收 Sequence Number。收到消息时:

  • 如果收到的 Sequence Number = 已记录的最大值 + 1:正常写入。
  • 如果已接收过(Sequence Number ≤ 最大值):识别为重复消息,丢弃,返回成功 ACK。这样 Producer 重试时,Broker 会自动去重。

局限性:幂等性只保证单个 Producer 会话内、单个 Partition 内的消息不重复,跨 Partition 或 Producer 重启后(PID 会改变),幂等性不再有效。跨 Partition 的幂等需要 Kafka 事务来保证。


三、消费者

Q8:Kafka 消费者组的工作机制

核心考点:分区与消费者的对应关系,并行度上限等于分区数。

背诵话术

Kafka 的消费者组(Consumer Group)遵循以下核心规则:

  • 同一 Partition,同一时刻只能被同一 Consumer Group 中的一个 Consumer 消费。这保证了组内消费者对该分区的有序消费,避免了重复处理。
  • 不同 Consumer Group 之间完全独立,同一 Topic 的消息会分别广播给每个订阅的消费者组,实现”广播消费”效果(如日志数据同时被监控系统和数据分析系统各自独立消费)。

并行度:消费者组内的并行消费能力取决于 Partition 数量,增加消费者数量不能超过 Partition 数(超出的消费者会空闲)。因此,分区数是 Kafka 消费并行度的天花板,如果发现消费积压,在增加消费者之前,必须先确认 Partition 数量是否足够。


Q9:分区分配策略(Range / RoundRobin / Sticky)

核心考点:三种策略的分配逻辑,Sticky 减少 Rebalance 时的数据移动。

背诵话术

Range(默认):按 Topic 维度,将该 Topic 的 Partition 连续分段分配给消费者组中的消费者。如果 Partition 数不整除消费者数,靠前的消费者会多分配一个 Partition。多个 Topic 时,每个 Topic 独立分配,可能导致靠前的消费者总是多分一个,产生负载不均

RoundRobin:将所有 Topic 的所有 Partition 按字典序排好,然后轮询分配给每个消费者。负载更均匀,但 Rebalance 时所有分配关系都要重算,迁移量大。

Sticky(推荐):初始分配类似 RoundRobin,但在 Rebalance 时尽量保持现有的分配关系不变,只将离开的消费者所持有的 Partition 重新分配给其他消费者。减少了不必要的 Partition 迁移,降低了 Rebalance 对消费的影响。生产中推荐使用 Sticky,特别是消费者频繁上下线时。


Q10:Rebalance 再平衡的触发条件和如何避免

核心考点:Rebalance 期间消费者暂停消费,触发条件,避免频繁触发的配置手段。

背诵话术

Rebalance 的触发条件

  1. 消费者组内有消费者加入或离开(扩容/缩容/消费者崩溃)。
  2. 消费者组订阅的 Topic 分区数变更(手动增加 Partition)。
  3. 消费者心跳超时(消费者未在 session.timeout.ms 内发送心跳,被 GroupCoordinator 认为已死亡)。

Rebalance 的代价:Rebalance 期间,所有消费者必须停止消费,等待新的分配方案执行完毕,可能造成几秒到几十秒的消息消费中断,影响消费延迟。

避免频繁 Rebalance 的手段

  • 调大 session.timeout.ms(默认 45s)和 heartbeat.interval.ms(默认 3s),防止因网络抖动导致的误判心跳超时。
  • 调大 max.poll.interval.ms(默认 5 分钟),避免消费者处理单批消息耗时过长导致被踢出组(核心!)。如果业务逻辑处理时间可能超过 5 分钟,必须调大这个参数。
  • 每次 poll() 拉取的消息数量(max.poll.records)不要设太大,避免处理时间超时。
  • 使用 Sticky 分区策略减少 Rebalance 的迁移成本。

Q11:如何保证 Kafka 消息的顺序消费?

核心考点:同一 Key → 同一 Partition → 单线程消费,三层保证。

背诵话术

Kafka 保证同一 Partition 内消息的有序性,但不保证跨 Partition 的全局有序。因此要保证消息顺序消费,需要三层配合:

  1. Producer 侧(相同业务数据发同一 Partition):为相关联的消息设置相同的 Key(如订单 ID、用户 ID),Kafka 的分区器会通过 hash(key) % partitionNum 确保相同 Key 的消息始终路由到同一个 Partition,从而保证这些消息的发送顺序。

  2. Partition 侧:Partition 内消息严格有序,只有一个消费者负责消费(Consumer Group 机制保证)。

  3. Consumer 侧(单线程消费该 Partition):消费者对拿到的消息必须单线程按序处理,不能将消息分发到多线程并行处理(否则处理完成顺序无法保证)。如果确实需要提高消费速度,可以按 Key 进一步分组后再交给不同的线程处理(相同 Key 的消息始终在同一线程内串行),保证单个业务实体的消息有序。

注意:开启幂等性后(enable.idempotence = true),生产者重试消息的顺序也有保障(Kafka 通过 max.in.flight.requests.per.connection 控制)。如需严格有序且不重复,需结合幂等性 + 顺序 Key + 单线程消费。


Q12:消费者如何实现幂等性消费?

核心考点:消息至少消费一次(At Least Once),业务侧的幂等性防重设计。

背诵话术

Kafka 默认语义是 At Least Once(至少一次):Consumer 消费消息后,在提交 offset 之前服务重启,则该批消息会被重新消费。因此,消费者端的幂等性设计至关重要。

常见幂等性方案

  1. 数据库唯一索引:消费消息后,在数据库插入记录时,以消息中的唯一业务 ID(如订单号、流水号)作为唯一索引,重复消费时数据库会抛出唯一键冲突异常,捕获该异常并忽略,保证数据不重复插入。这是最简单可靠的方案,在招行结算和 SRM 采购中均使用。

  2. Redis SETNX 防重:消费消息前,以消息的唯一 ID 为 Key,执行 SET msg_id 1 NX EX 7d(7 天过期),若返回成功则继续处理;若返回失败说明已处理过,直接跳过。适合对数据库写入压力敏感的场景。

  3. 状态机检查:根据业务状态判断,如订单只能从”待支付”流转到”已支付”,重复消费”已支付”消息时,发现订单已是”已支付”状态,直接幂等返回成功,不做重复处理。

  4. Token 令牌机制:消息中携带一个唯一 token,处理前先从 Redis 中删除该 token(DEL token_key),删除成功则处理,失败则说明已被处理(token 已不存在),幂等返回。


Q13:消息积压如何紧急处理?(结合 SRM 项目)

核心考点:先定位瓶颈,再扩容消费者或扩分区,临时中转是最后手段。

背诵话术

在 SRM 采购平台,供应商活动期间可能出现询价消息大量积压的情况。我的紧急处理流程分三步:

第一步:定位积压根因:通过 Kafka 监控(Lag 指标)确认积压的 Topic 和 Consumer Group,排查是消费异常(抛错导致重试卡死)、下游处理太慢(比价引擎计算耗时),还是数据库 IO 瓶颈导致消费速度跟不上生产速度。

第二步:扩容消费者(最快的手段)

  • Partition 数量充足(如有 10 个分区):立即对消费者服务进行水平扩容,将实例数增加到与分区数持平(最多 10 个消费者),让每个分区都有消费者并行处理。
  • Partition 数量不足(如只有 2 个分区):增加消费者实例无用(多出来的实例会空闲)。需要先扩 Partition,但在线扩 Partition 可能影响已有的 Key 路由(hash 值会变化)。此时最稳妥的方案是:启动一个临时中转服务,专门负责快速消费旧 Topic 的消息并立即 ACK,将消息转投到一个新建的、有足够 Partition 数(如 20 个)的临时 Topic,再启动 20 个消费者实例消费临时 Topic。

第三步:临时降级处理:关闭消费端一些非关键的业务逻辑(如积分回调、日志埋点),只保留核心的询价处理,释放 CPU 和 IO 资源,加快消费速度,待积压消费完后再恢复。


四、Kafka 高级特性

Q14:Kafka 事务消息原理?Exactly Once 如何保证?

核心考点:TransactionCoordinator + 两阶段提交,幂等性 + 事务 = Exactly Once。

背诵话术

Kafka 的 Exactly Once(精确一次)语义 = 幂等性(单 Partition 不重复)+ 事务(跨 Partition/跨 Topic 的原子性)

Kafka 事务的两阶段提交

  1. Prepare 阶段:Producer 向特定 Broker(TransactionCoordinator)申请一个事务 ID(transactional.id),并开始一个事务(beginTransaction())。随后向一个或多个 Partition 发送消息,这些消息写入 Broker 日志,但对消费者不可见(被标记为未提交状态)。
  2. Commit / Abort 阶段:Producer 调用 commitTransaction() 后,TransactionCoordinator 向所有参与的 Partition 写入一个事务提交标记(Transaction Marker),只有当消费者读到这个标记时,才能消费该事务中的消息。如果调用 abortTransaction(),写入回滚标记,消费者忽略这批消息。

读已提交消费:消费者需设置 isolation.level = read_committed,只读取已提交的事务消息,未提交的消息对该消费者不可见,从而保证消费端不会读到”中间态”数据。


Q15:死信队列(DLQ)在项目中的应用

核心考点:消费失败消息的最终归宿,避免无限重试阻塞正常消费。

背诵话术

死信队列(Dead Letter Queue, DLQ) 用于处理无法被正常消费的消息,防止它们因无限重试而阻塞整个消费进程。

标准处理流程

  1. Consumer 消费某条消息失败(如下游服务不可用、数据格式错误)。
  2. 进行有限次数的本地重试(如 3 次)。
  3. 如果重试依然失败,将该消息连同错误原因、重试次数等元信息,转投到一个专门的死信 Topic(如 original_topic_name.DLQ)。
  4. 消费者正常提交 offset,继续消费后续消息,不会被单条失败消息阻塞。
  5. 死信 Topic 由专门的死信处理服务监听,进行告警通知(钉钉/邮件)和人工排查。
  6. 问题修复后,可以从死信 Topic 重放消息进行补偿处理。

在招行调度中台的应用:核心金融流程调度消息如果消费失败,会经过 3 次重试后进入死信 Topic,触发高优先级告警,运维人员在 10 分钟内介入处理,确保金融流程不会因消息处理失败而长时间卡顿。