Chapter 06
自测题库与面试检验
前五章把 Kafka 从"一条可重放的分区日志"的心智模型,逐层铺到存储、副本、再平衡、投递语义的机制,再到生产陷阱与综合选型。读懂和能在面试里讲清楚之间,隔着一道主动提取——读过一遍只是认得,能闭卷复述并迁移到新场景才是真懂。这一章就是那道强制提取关。
怎么用这一章
- 共 20 道题,分三层梯度:概念层(回忆)6 道、原理层(理解)8 道、应用判别层(迁移)6 道。
- 每题带一条 提示链接,指回对应章节的锚点——卡住时先回去重读,别直接翻答案。
- 答案统一放在文末一个折叠块。先把答案写在纸上或编辑器里,全部做完再展开对照——直接点开等于把题库当又读了一遍,提取效果归零。
- 三层之后有一节面试加餐:挑出最易露馅的 6 道题,列出"普通答案 vs 资深答案必须点到的组合"。
§6.1概念层(对应 01 章 · 回忆)
每题单一考点,能一两句话答出来即可。答不出就回提示链接重读,别凭印象凑。
- offset 由谁维护——broker 还是消费者?这个归属决定了 Kafka 哪一条核心性质? 提示:参考 01 章 §1.1
- 消息被消费之后为什么不从日志里删除?它什么时候才真正消失? 提示:参考 01 章 §1.2
- Kafka 的顺序保证在什么范围内成立?为什么不是整个 topic 全局有序? 提示:参考 01 章 §1.3
- 一个 consumer group 的并行度上限是多少?组里消费者数量超过它会发生什么? 提示:参考 01 章 §1.4
- producer 带 key 发送时,记录落进哪个 partition 是怎么决定的?key 传
null又会怎样? 提示:参考 01 章 §1.4 - 两个不同的 consumer group 读同一个 topic,组 A 读到 offset 100、组 B 才到 10——组 B 会漏掉中间的消息吗?为什么? 提示:参考 01 章 §1.2
§6.2原理层(对应 02 章 · 理解)
这一层考机制和取舍,光报配置名不够,要能讲出"怎么运作、代价是什么"。
- LEO 和 HW(高水位)分别是什么?为什么消费者只能读到 ≤ HW 的记录? 提示:参考 02 章 §2.3
acks=all是否等于"所有副本都确认"?如果不是,它到底等于什么? 提示:参考 02 章 §2.3- ISR 在什么条件下会收缩(把一个 follower 踢出去)?由哪个配置控制? 提示:参考 02 章 §2.3
- eager(急切式)再平衡和 cooperative(协作增量式)再平衡的核心区别是什么?后者解决了前者的什么痛点? 提示:参考 02 章 §2.4
- 幂等 producer 靠什么机制去重?它为什么顺带解决了"重试导致乱序"? 提示:参考 02 章 §2.5
- 事务(transaction)怎么实现 EOS?
read_committed消费者在其中起什么作用? 提示:参考 02 章 §2.5 - 消费者的心跳在后台线程里照常发送,为什么它仍可能被判死并触发再平衡?"存活"到底由什么证明? 提示:参考 02 章 §2.4
- KRaft 为什么比 ZooKeeper 模式恢复(controller 故障切换)更快、能支撑更多分区? 提示:参考 02 章 §2.6
§6.3应用判别层(综合全书 · 迁移)
面试主战场。这些题没有"背一个名词"就能过的答案——要在场景里权衡,讲出选 A 不选 B 的理由和代价。
- 一条 100 万消息/秒、需要可重放的事件流 vs 一个低吞吐、需要复杂逐条路由 + 重试 + 死信队列(DLQ)的链路——分别该选 Kafka 还是 RabbitMQ?为什么? 提示:综合 01 章日志模型 + 04 章适用边界
- 业务方要求"全局严格顺序"(整个 topic 所有消息按一条总序)——怎么实现,代价是什么? 提示:综合 01 章 §1.3 + 02 章 §2.2
- 什么时候不该选 Kafka?至少举三类场景。 提示:参考 04 章"什么时候不该用"
- 一个消费组吞吐上不去,于是不断往组里加消费者。结果吞吐确实涨了,但消费 lag 也跟着涨——为什么?怎么修? 提示:综合 01 章 §1.4 + 02 章 §2.4 + 04 章再平衡风暴
- 给一个新负载定 partition 数:要考虑哪几条相互拉扯的约束?分区定多了、定少了各有什么代价?为什么之后想改很难? 提示:综合 01 章 §1.4 + 02 章 §2.2 + 04 章分区数
- 一个按
userId分区的 topic,某个大客户占了 90% 流量,导致一个消费者满载、其余空闲。这是什么问题?换成orderId当 key 能不能解决?Kafka 会自动均衡吗? 提示:综合 02 章 §2.2 热分区 + 04 章 key 倾斜
合上教程,凭记忆画出一条记录从 producer → broker → consumer 的完整路径:标上 leader 与 follower 副本、ISR、HW、以及消费者提交 offset 的那个点。画完翻回 02 章 §2.3 对照——你把 HW 标在哪里了?它是不是滞后于 leader 的 LEO 整整一个 fetch 轮?已 ack 给 producer 的那条记录,在你的图里对消费者可见了吗(它要等 HW 推进才可见)?这一个滞后正是"已确认 ≠ 立即可读"的来源,最容易在画图时被画错。
§6.4面试加餐:强答案必须包含什么
下面 6 道是 Kafka 面试里最容易露馅的题。露馅不在于答错,而在于答得"对但浅"——只报一个关键词,听上去像背过、没用过。面试官称量的是对取舍的推理和把多个机制组合起来的能力:持久性、EOS、顺序,正确答案几乎都是 "A 且 B 且 C" 的组合命题,缺一个组件就是一个破绽。每道列出"普通答案"和"资深答案必须额外点到的组合"。
把 Kafka 说成"更快的消息队列"是红旗信号;资深信号是把它框成分布式、可重放、按分区切分的提交日志 + 独立消费组。听的是组合不是关键词——只说 acks=all 而不讲它和 ISR、unclean.leader.election 的互动,就是背题。再追一句"broker 挂了 / 消费者慢了 / ISR 缩到 1 时各发生什么",能讲出失败模式的故事的人立刻和背配置的人分开。
| 题 | 普通答案(对但浅 / 破绽) | 资深答案必须点到的组合 |
|---|---|---|
| ① 端到端怎么保证不丢消息? | "设 acks=all。"——只说这一个就是破绽,ISR 缩到 1 时它会静默退化。 |
acks=all 且 min.insync.replicas≥2 且 replication.factor≥3 且 unclean.leader.election=false,再加消费侧"处理完再提交 offset"。五件套缺一个就有丢失窗口;要能说出每一件各堵哪个窗口。 |
② replication.factor 和 min.insync.replicas 什么关系? |
"RF 是副本数,min.insync 是最少同步数。"——只复述定义,没讲怎么配。 | 设 min.insync.replicas = RF − 1:这样挂一台仍能写、挂两台才停写。设成 = RF 则任意一台故障就停写(牺牲可用性);设成 1 则等于没有下限保护。要点出这是持久性与可用性之间的旋钮,持久性的真正来源是 min.insync.replicas 而非 acks。 |
| ③ Kafka 能做exactly-once 吗?边界在哪? | "开 enable.idempotence=true 就精确一次了。"——过度宣称,这是最常见的误解。 |
幂等 producer 只在同一会话、同一 (PID, partition) 内去重,跨进程重启换新 PID 即失效;真正的 EOS 需要幂等 producer + 事务(transactional.id + epoch)+ 消费侧幂等三者。且"精确一次"只在 Kafka 的"消费-转换-生产"闭环内成立,不覆盖外部副作用(DB 写、REST 调用、发邮件那些重试仍会重复触发)。 |
| ④ Kafka 怎么保证顺序? | "Kafka 保证顺序。"——太笼统,漏掉范围和失效条件。 | 只在单个 partition 内保证;要顺序的记录必须用同一个 key 哈希进同一分区。还要点出反例:max.in.flight.requests.per.connection > 1 且未开幂等时,某条消息重试会插到后发消息之后,造成乱序——开 enable.idempotence=true 才靠序列号保序。 |
| ⑤ 什么触发再平衡?怎么避免再平衡风暴? | "消费者增减会触发再平衡。"——只说触发条件,没讲协议代价。 | 要分清 eager(stop-the-world,全员放弃全部分区)vs cooperative-sticky(只移动需变动的分区,未动的继续消费)。再讲风暴成因:处理太慢超过 max.poll.interval.ms(或 session.timeout.ms 太紧)→ 成员被驱逐 → 重入 → 循环。缓解:调小 max.poll.records、用协作式协议 + static membership(group.instance.id)。 |
⑥ acks=1 有什么问题? |
"acks=1 比 acks=all 弱一点。"——没说清丢在哪个窗口。 |
leader 写进自己的日志就 ack、不等 follower 复制;在"已 ack、未复制"这个窗口内若 leader 崩溃,新 leader 从某个 follower 选出,那段没复制出去的记录随旧 leader 磁盘一起永久丢失,且不抛任何异常。这是用持久性换延迟,要能描述这个具体的丢失时序。 |
把"不丢"和"不重"同时讲成一个完整故事
面试官常把表 6.1 的 ①(不丢)和 ③(不重)合并追问:"一条订单事件,从 producer 发出到 consumer 处理入库,既不丢也不重复入库,整条链路你怎么设计?" 试着不看答案,把生产端持久性配置、broker 端副本/ISR、消费端 offset 提交时机、以及"外部 DB 写"这一步的去重,串成一段 2 分钟能讲完的话。
提示(卡住再展开)
关键是认清两段不同性质的边界:① Kafka 内部(producer→broker→consumer 读取)可以靠五件套 + 幂等/事务做到不丢、Kafka offset 不重复提交;② "写外部 DB"这一步在 Kafka 的 EOS 闭环之外——重投时这一步会再执行一次。所以"不重复入库"不能指望 Kafka 给你,要靠消费侧幂等:用 orderId 做数据库唯一键 / upsert,或先查后写。把"Kafka 保证什么"和"必须自己保证什么"切开讲,是这道题的资深信号。
答案(三层全部做完、画完图,再展开)
概念层(§6.1)
- offset 由消费者维护,不是 broker。broker 只负责顺序追加日志、按时间/大小保留;"读到哪"是消费者自己提交到
__consumer_offsets的一个数字。这个归属是后面一切的总开关——正因为进度在消费者侧、消息不因被读而消失,才有了可重放和多消费组各读各的。 - 因为 Kafka 是日志不是队列:消费是移动游标(seek),不是出队(dequeue),游标前移不影响日志本身。记录何时消失只取决于保留策略(
retention.ms/retention.bytes到期删整段,或 compaction 对每 key 只留最新),与"是否被读过"完全解耦。所以消费滞后超过保留期会丢数据。 - 顺序只在单个 partition 内保证,不保证 topic 全局有序。原因是物理的:全局总序需要一个单一写入点把所有记录串成一条序列,那就退回单分区、放弃了水平扩展。一条记录的完整坐标是
(topic, partition, offset)三元组,跨分区的 offset 之间没有可比性。 - 并行度上限 = partition 数。一个 partition 同一时刻只能被同组内一个 consumer 持有,所以消费者数超过分区数,多出来的就空闲拿不到分区;少于分区数则有消费者要扛多个分区。
- 带 key 时
partition = murmur2(key) % 分区数——相同 key 永远算出同一分区,因此同 key 记录在该分区内有序。key 传null时用 sticky partitioner(先填满一个分区的 batch 再换),记录在分区间均摊,失去按 key 的顺序保证。 - 不会漏。offset 是每个组各自维护的游标,互不影响。组 A 读到 100 不会"消耗"掉记录,11–100 号全都还在日志里(只要没超过保留期),组 B 会照常从 11 一路读下去。这正是"日志而非队列"最有用的一条性质:多消费组天然隔离。
原理层(§6.2)
- LEO(Log End Offset) = 某个副本下一条要写的 offset(即该副本已有记录数)。HW(高水位) = ISR 中所有副本里最小的 LEO,也就是"已被所有同步副本复制到的最高 offset"。消费者只能读到 ≤ HW 的记录,是因为 HW 以上的记录还没被全部副本复制,一旦 leader 故障可能消失——HW 门控防止消费者读到"将来可能不存在"的记录。代价:HW 在下一轮 fetch 才推进,所以已 ack 给 producer 的记录要等约一个 RTT 才对消费者可见。
- 不等于"所有副本",等于"所有 ISR 成员"。当 follower 落后被踢出 ISR、ISR 收缩到只剩 leader 一台时,
acks=all就等价于acks=1——写到一台就算"全部 ISR 确认"。所以持久性的真正来源是min.insync.replicas(要求至少这么多 ISR 成员在线,否则写入被NotEnoughReplicas拒绝),不是acks本身。 - follower 落后 leader 超过
replica.lag.time.max.ms(默认 30s)没追上,就被踢出 ISR。收缩后 ISR 成员变少,会影响 HW 的计算和acks=all的实际强度。follower 重新追上后会被加回 ISR。 - eager:每轮再平衡所有成员放弃全部分区(stop-the-world 屏障),重新分配,期间整组暂停消费。cooperative/incremental(2.4+):只放弃需要移动的分区,分两轮收敛,未变动的分区继续消费。后者解决了 eager 的全局暂停——大规模消费组扩缩容时,启动时间能从十几分钟降到约一分钟。KIP-848(4.0 GA)进一步把再平衡逻辑移到 broker 端、增量、无客户端 stop-the-world。
- broker 为每个
(PID, partition)维护一个最高序列号,只接受序列号恰好 +1 的 append;重试导致的重复(序列号已见过)被丢弃,出现空洞则抛OutOfOrderSequenceException。它顺带解决乱序,是因为这套"必须连续 +1"的校验拒绝乱序 batch——所以max.in.flight可以 >1(≤5)而不会因重试打乱顺序。 - 事务靠
transactional.id+ epoch 隔离僵尸实例:transaction coordinator 跑两阶段提交,给每个涉及的分区写 commit/abort marker,并把输出写 + 消费 offset 提交放进同一个原子事务。read_committed消费者跳过 aborted 和未决的记录、且不能越过 LSO(Last Stable Offset),从而只读到已提交事务的结果——这是 EOS 在读侧的兑现。代价:marker + LSO 带来额外延迟和队头阻塞(一个长事务未决会卡住read_committed读者)。 - 因为存活由
poll()证明,不是由心跳证明。心跳在后台线程,但它只说明"进程还活着、网络还通";真正的判活看你有没有在max.poll.interval.ms内再次调用poll()。如果在 poll 循环里做了过重的处理(DB/HTTP)迟迟不回来 poll,协调者就判定这个成员处理不过来 = 死了,把它驱逐并触发再平衡,在途批次会在新 owner 上重复处理。 - 因为 KRaft 把元数据本身做成一条日志(内部 topic
__cluster_metadata,由 controller quorum 用 Raft 复制),standby controller 持续回放并把全部元数据常驻内存。所以 active controller 故障时,新 controller 的状态已经在内存里、几乎瞬时接管。ZooKeeper 模式下 controller 是独立系统,故障切换要全量重载元数据,分区一多就慢——这就是 KRaft 能支撑百万级分区、恢复更快的原因。代价:quorum 多数派挂掉就失去可用性。
应用判别层(§6.3)
- 100 万/秒 + 可重放选 Kafka:它的顺序日志 + page cache + 零拷贝就是为高吞吐顺序流设计的,可重放是免费附赠(多消费组各读各的)。低吞吐 + 复杂逐条路由 + 重试/DLQ 选 RabbitMQ:Kafka 没有原生的逐条路由(exchange/binding)、没有内置的逐消息重试与死信队列、按分区粗粒度而非按消息精细 ack(share group 在弥补,但语义和成熟度不同)。判别的核心:"高吞吐可重放的日志" vs "灵活路由 + 逐条投递控制的 broker",是两种不同的工具,不是快慢之分。
- 把 topic 设成单分区,所有消息走同一条日志即得全局总序。代价巨大:并行度被钉死为 1(一个组只能有一个消费者在干活),吞吐被单分区的单机磁盘和单消费者卡死,还失去了 Kafka 的核心扩展能力。所以面试时要主动说:"能做,但等于放弃 Kafka 的全部并行优势——先反问业务是否真需要全局序,通常按 key 的分区内有序就够了。"
- 至少三类:① 低吞吐 + 需要复杂路由(用 RabbitMQ 这类传统 broker);② 需要内置逐条重试 / DLQ / 优先级队列(Kafka 都得自己造);③ 请求-响应 / RPC 语义(Kafka 是单向日志,不是 RPC)。补充:小团队、量也不大却要扛 Kafka 的运维成本(分区规划、再平衡、监控)时,收益盖不过复杂度。判别要点:Kafka 的强项是高吞吐可重放日志,凡是和这条不沾边的需求都该重新考虑选型。
- 因为每加一个消费者就触发一次再平衡;如果组本来就不稳定、或处理慢导致成员频繁被驱逐,就会陷入再平衡风暴——每次 stop-the-world(或部分暂停)期间消费停顿,lag 在停顿里堆积。更糟的情形是消费者数已经逼近或超过分区数,再加只是空转还白白触发再平衡。修:别在不稳定的组上扩容;上协作式再平衡 + static membership、调小
max.poll.records把单批处理时间压在max.poll.interval.ms内;先确认分区数是否够,不够要先扩分区。 - 相互拉扯的约束:① 目标吞吐(分区越多并行写/读越高)、② 消费并行度(消费者数 ≤ 分区数,要留够余量)、③ 顺序范围(同 key 必落同分区,分区数影响 key 分布)、④ 再平衡与故障切换成本(分区越多 controller/元数据负担、故障切换越慢、文件句柄越多)。定多了:抬高端到端延迟、故障切换时间、文件句柄耗尽(每 segment 2 个文件,默认 ulimit 1024)。定少了:消费并行度被卡死。之后难改:分区数只能增不能减,且给有 key 的 topic 加分区会永久打乱 key→分区映射、破坏所有下游的按 key 顺序——所以要么保守预估,要么新建 topic 迁移。
- 这是热分区 / key 倾斜问题:
userId基数虽高,但单个大客户的流量全哈希到同一分区,压垮持有该分区的消费者。换orderId做 key 能缓解——orderId 基数更高、同一客户的订单会散到多个分区,负载更均匀;代价是失去"同一 user 的事件有序"(只剩同一 order 内有序),要确认业务能接受。Kafka 不会自动均衡倾斜——分配是按分区粒度,不看每分区的实际数据量。所以治本是选高基数、分布均匀的 key,而不是指望加消费者。