少女祈祷中...
NEKO
知识库

kafka 面试要点

请用你自己的话,介绍一下 Kafka 是什么?它与传统的 RabbitMQ 或 ActiveMQ 有什么核心区别?

核心回答:

Kafka 是一个分布式、分区、多副本的基于 ZooKeeper(注:新版本正在去 ZK,用 KRaft) 协调的消息队列系统。但它不只是一个 MQ,更是一个分布式流处理平台。

与传统 MQ 的核心区别(重点):

  1. 设计哲学不同:

    • 传统 MQ(RabbitMQ/ActiveMQ):是“万金油”,注重消息的可靠性、路由的灵活性(Exchange)和即时消费,消费完通常立即删除。

    • Kafka:是“日志聚合器”,天生为海量吞吐而生。它不删除消息,而是基于 Offset 位移持久化,允许消费者“重播”历史数据。

  2. 吞吐量:

    • Kafka 利用 PageCache 顺序读写 + 零拷贝(sendfile),单机吞吐可达百万级/秒;传统 MQ 通常为万级。

  3. 消费模型:

    • 传统 MQ 消费完消息即删除,是“临时性”的。

    • Kafka 消费者是“拉取(Pull)”模式,且消费进度(Offset)由客户端控制,这使得它天然支持批处理和流式计算(如 Flink)。

Kafka 是如何保证同一分区内的消息是有序的?如果某个消费者组里的消费者数量超过了分区数,会发生什么?

问题 1 回答:

Kafka 保证分区(Partition)内有序,主题(Topic)级别无序。

  • 机制:每个分区内部维护一个单调递增的 Offset。生产者发送时,若指定了 key,则同一 key 会哈希到同一分区;若不指定,则采用轮询或随机,但 Kafka 保证同一 Producer 在同一个分区内,发送顺序与追加顺序一致(通过 max.in.flight.requests.per.connection=1 可严格保序,否则重试可能导致乱序)。

max.in.flight.requests.per.connection 是 Producer(客户端)配置

  • 默认值:5(Kafka 0.11 之前是 1,0.11 起改为 5)

  • 作用域:单个 Producer 到单个 Broker 的连接上,允许同时未确认(unacked)的请求数上限

问题 2 回答(考察 Group 协调机制):

如果消费者组(Group)内的消费者实例数 大于 主题的分区数,多余的那几个消费者将处于“空闲(Idle)”状态,分不到任何分区。

  • 因为 Kafka 的分配策略(如 Range、RoundRobin、Sticky)规定:一个分区只能被同一 Group 内的一个消费者消费。

  • 引申意义:要提高并行度,只能增加分区数,而不是无限增加消费者。

在项目生产中,如果业务要求消息绝对不丢失,你在 Kafka 生产端和 Broker 端分别会怎么配置?acks 参数怎么设?

第一层:生产端(Producer)配置(必须组合拳):

  1. acks = all(或 -1):Leader 必须等待所有 ISR(In-Sync Replica)副本都写入成功才返回确认。

  2. enable.idempotence = true:开启幂等性,防止网络重试导致的消息重复(且要求 acks=all 且 max.in.flight.requests.per.connection <= 5)。

  3. retries = Integer.MAX_VALUE:网络抖动时无限重试,配合 delivery.timeout.ms 设置总超时。

第二层:Broker 端配置:

  • min.insync.replicas > 1(通常设为 2):至少要有 2 个副本写入成功,配合 acks=all,确保哪怕 Leader 宕机,数据也存在于 Follower 中。

  • replication.factor ≥ 3 每个分区的副本总数

  • unclean.leader.election.enable = false:禁止非 ISR 副本竞选 Leader,代价是 ISR 全挂时分区不可用(牺牲可用性),收益是不丢已确认数据(保一致性)。为了绝对不丢,必须设为 false。

关键陷阱提示:

面试官如果追问 “消息真的能 100% 不丢吗?”
回答:严格物理意义上做不到。但在业务层面,acks=all + min.insync=2 可保证 Broker 不丢。唯一极端风险是 Broker 所有副本同时掉电(概率极低)。更务实的做法是业务侧做落库补偿或死信队列重放。

ISR = In-Sync Replicas(同步副本集合),指与 Leader 副本保持"足够同步"的所有副本(含 Leader 自身)。它是 Kafka 在"一致性"和"可用性"之间做权衡的核心机制。

min.insync.replicas > 1(通常设为 2)

含义:一个分区至少要有多少个 ISR(In-Sync Replica,同步副本)副本确认写入成功,Leader 才认为这次写入是成功的。

  • 它是配合 acks=all 生效的。acks=all 说的是“Leader 要等所有 ISR 都写入”,但“所有 ISR”到底有几个,取决于当前 ISR 集合大小;min.insync.replicas 则是给这个集合设了一个下限。

  • 设为 2 的含义:当 ISR 里只剩 1 个副本时(比如另外两个 Follower 落后或宕机),Broker 会拒绝写入,producer 收到 NotEnoughReplicasException。

  • 为什么这是“不丢”的关键:如果 min.insync.replicas=1,那么 ISR 只剩 Leader 自己时,acks=all 退化成 acks=1——Leader 写成功就返回,但此时数据只有一份,Leader 一宕机数据就丢了。设成 2 就堵住了这个漏洞。

replication.factor ≥ 3

含义:每个分区的副本总数(包括 Leader 和所有 Follower)至少为 3。

  • 这是副本冗余的“总量”指标,决定数据在物理上存了几份。

  • 和 min.insync.replicas 的关系:replication.factor 是总副本数,min.insync.replicas 是允许写入所需的最小同步副本数。二者必须满足:

1 ≤ min.insync.replicas ≤ replication.factor

典型生产组合是 replication.factor=3、min.insync.replicas=2。含义是:总共 3 份,至少 2 份同步确认才算写成功。这样允许 1 个副本宕机/落后而不影响写入,同时最多容忍 1 个副本丢失而不丢数据。

  • 如果 replication.factor=3 但 min.insync.replicas=3:任一 Follower 掉线都会导致无法写入,可用性太低,一般不这么配。

  • 如果 replication.factor=2、min.insync.replicas=2:没有任何容错余量,一个副本挂了就写不进去。

max.in.flight.requests.per.connection <= 5

Kafka 的幂等 Producer 在 Broker 端为每个 (PID, 分区) 维护一个最多能容忍的乱序序列号数量,这个容量是 5。也就是说:

  • Broker 最多能正确处理“序列号领先当前期望值不超过 5”的乱序批次;

  • 如果同时 in-flight 的请求超过 5 个,就可能出现某个批次的序列号领先期望值超过 5,超出 Broker 的乱序缓存窗口,导致:

    • Broker 无法正确排序/去重,可能抛出 OutOfOrderSequenceException;

    • 严重时 Producer 会被迫重置 PID 或报错,幂等性保证被破坏。

因此 Kafka 官方规定:开启幂等性时,max.in.flight.requests.per.connection 必须 ≤ 5。这不是性能调优建议,而是幂等性正确性的硬性要求。

  • =1:最保守,绝对有序,但吞吐低(每次都要等 RTT)。

  • 2~5:允许一定并发,兼顾吞吐,且仍在 Broker 乱序窗口内,幂等性有效。

  • >5:可能破坏幂等性保证,Kafka 客户端在 enable.idempotence=true 时如果检测到该值 >5,会直接报配置错误(较新版本)或自动降级/告警。

既然你用了 enable.idempotence 防止了生产重复,那消费端的重复消费问题你怎么解决?如果让你设计,你会在代码里怎么处理?

核心认知:

Kafka 只保证至少一次(At Least Once),不保证恰好一次(Exactly Once),除非我们实现了事务性消费。
消费重复产生的根本原因:消费处理成功,但提交 Offset 失败(如处理完重启,位移未提交导致重新拉取)。

我的解决方案(分层设计):

  1. 业务幂等性(最推荐):

    • 利用消息中的唯一 UUID/Business ID,在数据库中使用 INSERT IGNORE 或 ON DUPLICATE KEY UPDATE(MySQL),或在 Redis 中 SETNX 做防重表(去重表),以业务主键作为唯一约束。

  2. 原子性操作(精确一次):

    • 将 消费处理逻辑 与 Offset 提交 放在同一个事务里。例如:使用 Kafka 事务 API(producer.sendOffsetsToTransaction),或将消息处理后存入 DB,同时将 Offset 存入同一张表,利用 DB 本地事务保证要么都成功,要么都回滚。

  3. 下游兜底(最终一致性):

    • 如果下游是第三方接口(不支持幂等),使用状态机或版本号机制,忽略旧版本的处理结果。

enable.idempotence=true 让 Producer 具备"幂等性"——同一 Producer 因重试而重复发送的消息,Broker 只会写入一条。

它解决的是 "至少一次(at-least-once)"语义下,重试导致的重复消息问题。

没有幂等时的问题

Producer 发消息 → Broker 写入成功 → ACK 丢失(网络抖动)→ Producer 认为失败,重试 → Broker 又写一遍 → 消息重复。

Producer ──[msg1]──▶ Broker
         ◀──[ACK 丢失]── Broker      ← 网络问题
         ──[msg1 重试]──▶ Broker
                                    ← Broker 收到两次 msg1
                                    结果:msg1 被写入两次

场景:

  • 网络抖动、超时

  • Broker 重启

  • retries > 0 且发生重试

  • max.in.flight.requests.per.connection > 1 时更明显

开启幂等后

Broker 侧维护 (PID, Partition, SeqNum) 去重表,同一 SeqNum 只接受一次。

Producer ──[msg1, PID=1, Seq=0]──▶ Broker
         ◀──[ACK 丢失]── Broker
         ──[msg1, PID=1, Seq=0]──▶ Broker
                                    ← Broker 发现 Seq=0 已存在
                                    结果:丢弃,只写一次

底层机制:PID + Sequence Number

1. Producer ID(PID)

  • Producer 启动时向 Broker 申请唯一 PID

  • 每个 Producer 实例一个 PID

  • 重启后 PID 会变(除非用事务)

2. Sequence Number(序列号)

  • 每个 (PID, Topic-Partition) 维护一个单调递增序列号

  • 从 0 开始,每条消息 +1

  • Broker 按序列号判断:

    • SeqNum = 期望值 → 正常写入

    • SeqNum < 期望值 → 重复,丢弃

    • SeqNum > 期望值 → 乱序,抛 OutOfOrderSequenceException

3. Broker 侧去重

Broker 为每个 (PID, Partition) 维护最近 5 个 batch 的序列号,用于去重。

开启后的约束(关键)

enable.idempotence=true 不是孤立的开关,它会强制/要求以下配置:

配置

要求

说明

acks

必须 all

否则抛 ConfigException

retries

必须 > 0

否则抛 ConfigException

max.in.flight.requests.per.connection

必须 ≤ 5

否则抛 ConfigException

Kafka 0.11+ 的默认值:

enable.idempotence=true        (3.0+ 默认;2.x 默认 false)
acks=all
retries=2147483647
max.in.flight.requests.per.connection=5

所以 Kafka 3.0+ 默认就开启幂等,无需手动配。

我们系统遇到了消息积压,有几百万条消息没消费。你作为负责人,如何在不重启服务、不丢失数据的前提下,快速让消费者恢复正常并消化积压?

这个题考察应急处理能力和对 Kafka Rebalance 机制的深刻理解。

(绝不能只回答“增加消费者”,因为分区数固定,加消费者会闲置)

我的分步策略(三步走):

  1. 第一步:扩容分区(临时):

    • 动态增加该 Topic 的分区数(bin/kafka-topics.sh --alter)。

    • 注意:仅增加分区不会影响历史数据分布,但新消息会进入新分区。

  2. 第二步:横向扩容消费者(关键):

    • 增加新的消费者实例(新机器),Group 重新 Rebalance,新消费者会接管新增的分区。

    • 但是:如果原分区积压严重,原有消费者依然卡在旧分区的大量数据上。

  3. 第三步:临时创建“快速转发”队列(经典临时方案):

    • 操作:创建一个新的临时 Topic(如 topic_emergency),并增加大量分区(例如 30 个分区)。

    • 启动一个临时消费者(应急程序),从原积压 Topic 快速拉取数据,不进行复杂业务逻辑,只做简单清洗,然后重新生产到 topic_emergency 的新分区中。

    • 启动对应 30 个消费者的应急服务群,并行消费 topic_emergency,进行真正的业务处理。

    • 优点:隔离了积压影响,原服务不受影响,处理完后切回原 Topic。

评论