Theory · Kafka · Replication & Consistency

Kafka 副本机制与数据一致性

ISR · LEO/HW 水位 · HW 机制的两个经典缺陷 · KIP-101 LeaderEpoch · unclean 选举 · acks 矩阵 · KRaft 架构

复制协议核心

动态 ISR(f+1 容 f)替代多数派;HW=min(ISR 的 LEO) 划定消费者可见边界

一致性深挖

HW 截断的数据丢失与错位问题 → KIP-101 LeaderEpoch 截断校验;acks=all×min.insync.replicas 矩阵

KRaft 时代

为什么移除 ZooKeeper、Controller Quorum(Raft)与 __cluster_metadata,3.3→4.0 演进线

这份 deck 是 Kafka 可靠性的核心战场,也是面试最深挖的部分。主线:ISR 动态副本集怎么工作、LEO 和 HW 两个水位怎么定义、HW 截断机制为什么有数据丢失和日志错位两个经典缺陷、KIP-101 怎么修复、acks 和 min.insync.replicas 怎么配合、最后 KRaft 架构。所有默认值都对照 4.3 源码核过,两个 HW 缺陷的推演按 KIP-101 原始讨论整理。学完能扛住"acks=all 为什么还丢消息"这条追问链。

Replicas · AR / ISR / OSR

副本与角色:Leader 读写、Follower 拉取、ISR 动态集合

Leader

分区的读写入口:produce 永远发给 leader;消费默认读 leader(2.4 起可机架就近读 follower,见下)。所有 HW/LEO 的裁决在 leader 侧进行。

Follower

被动拉取复制:与普通消费者同一套 fetch 机制(天然成批、零拷贝复用)。只从 leader 拉,推复制不存在——follower 崩溃重启不污染 leader。

AR = ISR + OSR

ISR(in-sync replicas):追得上 leader 的副本集合(含 leader 自身);OSR:掉队被移出的。ISR 是动态的:收缩与回归都由 leader 维护,不重启生效。

追问答案
怎么算"追上"?replica.lag.time.max.ms=30000(4.3 源码):follower 至少 30s 没发 fetch、或没消费到 leader 日志末端 → 移出 ISR。旧版还看消息条数差(replica.lag.max.messages,已废弃)
掉队副本怎么回归?恢复后先截断对齐(KIP-101 校验,第 7 页)→ 从 leader 全量补齐缺口 → 重新追平末端 → leader 自动拉回 ISR(unclean 选举无关,这是正常回归路径)
为什么不用多数派写?ISR 模型 f+1 副本容 f 故障:2 副本即容 1 台宕机(Raft/Paxo 需 2f+1=3)——磁盘与写放大省一半;代价是 acks=all 的提交延迟受最慢 ISR 成员影响
一句话:"ISR 是'当前有资格参与提交'的动态副本集——leader 维护、按追平度进出;数据正确性只依赖 ISR,不依赖 AR 全体。"
副本页立三个概念。Leader 是唯一读写入口,Follower 用和消费者一样的 fetch 拉取复制,这个设计让复制天然成批。AR 等于 ISR 加 OSR,ISR 是动态集合,进出都由 leader 按 30 秒追平窗口维护。三个追问:怎么算追上——30 秒内发过 fetch 且消费到末端;怎么回归——先截断对齐全量补齐再自动回 ISR,注意这和 unclean 选举无关;为什么不用多数派——f 加 1 容 f,两副本容一台宕机,省一半磁盘,代价是提交延迟受最慢成员拖累。

Shrink & Catch-up

一次完整的收缩-回归时间线:丢没丢,什么时候丢?

时刻事件ISR / HW数据视角
t03 副本全追平,producer 以 acks=all 写入 m10ISR={A,B,C} · HW=11m10 已提交:三副本都有(各自页缓存)
t1C 磁盘慢,lag 持续增长;producer 写 m11(acks=all 等到 A、B)ISR={A,B}(C 超 30s 未追上 → 移出)m11 提交于 {A,B};C 的日志停在 m10
t2A 宕机,controller 从 ISR 选 B 为新 leaderISR={B}(C 在 OSR)m11 在 B 上完好——已提交数据没丢(all 只承诺 ISR,而 C 当时不在 ISR)
t3C 恢复:LeaderEpoch 校验后截断对齐 → 全量拉回 m11 → 追平末端leader 把 C 拉回:ISR={B,C}C 上从未提交过的 m10 残差被安全截断;m11 补齐

关键判定:HW 只进不退

移出落后者后 min(ISR LEO) 只会不动或上移——ISR 收缩不会让"已可见"数据回退;acks=all 的承诺对象是提交时刻的 ISR,C 掉队期间写入的消息本来就不等 C。"丢"只会发生在承诺对象(ISR)本身缩小后的写入上——引出第 10 页的 min.insync.replicas 门禁。

KIP-392 follower fetching(一句话)

2.4 起:消费者与 follower 副本均可设置 client.rack,broker 按 rack 就近服务——跨机房部署时读流量可从本机架的 follower 走,省跨机房带宽;注意就近读的数据可能略落后于 leader(可见性权衡)。

这张时间线表是 ISR 机制的标准推演题。重点是 t2:A 宕机时 m11 丢没丢?没丢,因为 acks=all 的承诺对象是提交时刻的 ISR,m11 提交时 ISR 是 A 和 B,B 上有。C 掉队期间的写入本来就不等 C。t3 回归路径:先截断对齐再全量补齐,注意 C 上 m10 的残差是未提交数据,截掉是安全的。关键判定记一句:ISR 收缩不会让已可见数据回退,HW 只进不退。KIP-392 机架就近拉取一句话带过:client.rack 匹配 broker.rack,跨机房省带宽。

LEO / HW

两个水位定一致性:LEO 是末端,HW 是可见边界

LEO 与 HW:三副本分区的水位同步图 Leader B1 日志 11 条,Follower B2 复制 10 条,Follower B3 落后只有 8 条;HW 取 ISR 中最小 LEO 等于 8,消费者只能读到 8 之前的数据;第 9、10 条已写入 leader 与 B2 但未过 HW 属于未提交数据;B3 若持续落后超过 30 秒将被移出 ISR。 分区 P0 · 副本数 3 · AR = {B1, B2, B3} B1 · Leader B2 · Follower B3 · Follower 0 1 2 3 4 5 6 7 8 9 10 HW = 8 LEO = 11 0 1 2 3 4 5 6 7 8 9 LEO = 10 已追平 HW → 留在 ISR 0 1 2 3 4 5 6 7 LEO = 8(落后 3 条) 30s 未追上末端 → 移出 ISR 定义(背这三个) LEO = 日志下一条写入位置(每副本各自维护) HW = min(ISR 各副本 LEO)(leader 裁决并广播) AR = ISR ∪ OSR(全集) 消费者只能读 < HW 的数据——永远看不到"可能丢"的未提交段 未提交段(图中 8、9)会怎样? B3 追平 → HW 前移变为已提交;B3 被移出且 leader 也挂 → 未提交段随旧 leader 消失——acks=all 下这段从未 ack 给生产者 HW 的两个作用:① 消费者可见性边界(只读 < HW) ② 旧复制协议下 follower 截断的依据(由此引出两个经典问题 → 第 5/6 页) HW 推进是异步的:leader 收到 follower fetch 才知道对方 LEO → HW 永远滞后于"ISR 已有"的数据
LEO 和 HW 是一切一致性讨论的坐标系。LEO 是每副本日志的下一条写入位置,HW 是 leader 按 ISR 最小 LEO 裁决的可见边界。图里三个数字:B1 十一条、B2 十条、B3 八条,HW 等于八,消费者只能读零到七。第八九条已在 leader 和 B2 上但没过 HW,属于未提交段——它会随着 B3 追平变提交,也可能随掉队消失,但 acks=all 下它从没 ack 给生产者,所以不算丢。最后一句是引子:HW 的传播是异步的、滞后的,这个滞后就是后面两个经典缺陷的根源。

HW Defect #1 · Data Loss

问题一:HW 之下的"已 ack"消息会随 leader 一起消失

旧 HW 截断机制下的数据丢失时序推演 两副本 acks 等于 1 场景:t1 消息 m2 写入 leader 并回执但未及复制;t2 follower B 重启按本地 HW 截断;t3 leader A 宕机,controller 选 B 为新 leader;t4 B 的日志只有 m1,已回执的 m2 永久丢失。 前提:副本数 2 · acks=1 · min.insync.replicas=1 · 旧复制协议(KIP-101 之前,截断只看本地 HW) t0t1t2t3t4 A(Leader) m1 写入 · LEO=1 acks=1 → 回执 ✓ B 同步中 A(Leader) m2 写入 · LEO=2 acks=1 → 回执客户端 ✓ m2 尚未复制到 B! 本地 HW=1(只推进到 B 已复制处) A(Leader) 日志 [m1, m2] HW 推进至 2 (B 已回线上并复制) A 宕机 ✗ ISR 只剩 B controller 选 B 为新 leader (合法选举,非 unclean) A 恢复后 以 B 为准 截断重同步 B(Follower) 复制到 m1 · LEO=1 本地 HW=1 B(Follower) LEO=1(还没拉到 m2) 本地 HW=1 B 重启(t2 事故点) 旧规则:截断至本地 HW = 只留 [m1] (重启恢复的标准动作) B(新 Leader) 日志只有 [m1] m2 永久丢失 而 m2 曾 ack 给客户端 结论 已 ack ≠ 已冗余 acks=1 的固有风险 与 unclean 无关 根因:截断依据(本地 HW)与 leader 真实日志不同步——"已 ack"的消息可能只存在于即将宕机的 leader 上 解法:acks=all + min.insync.replicas=2(提交即冗余);协议层修复见 KIP-101(第 7 页)
这是 KIP-101 讨论里的原始数据丢失场景,要能按格子讲。t1 是关键:m2 写入 leader 且 acks=1 已经回执,但还没复制到 B,此时 m2 只存在于即将出事的 A 上。t2 B 重启,旧规则按本地 HW 截断,这个动作本身没问题。t3 A 宕机,controller 合法地把 B 选为新 leader——注意这不是 unclean 选举。t4 B 日志里只有 m1,m2 永久消失,而它是 ack 过的。根因一句话:截断依据和 leader 真实日志不同步。解法两层:业务层 acks=all 加 min.insync 等于二,协议层就是 KIP-101。

HW Defect #2 · Inconsistency

问题二:老 leader 复活后的日志错位——不一致与重复

时刻事件A 的日志 / 本地 HWB 的日志 / 本地 HW
t0A 为 leader;m1 已提交(HW=1),m2 已写入 A 但未提交(LEO=2)[m1, m2] · HW=1[m1] · HW=1
t1A 短暂失联(GC/网络),controller 切主到 B;producer 未再写入失联中[m1] · HW=1(成为 leader)
t2A 恢复:按旧机制先截断至本地 HW=1 → m2 被截掉[m1](m2 丢失)[m1]
t3变体①:A 以 follower 重新拉取——行为正常;变体②:老机制 + sticky leader 场景下 A 可能被再次选回,而它的日志已被截短分叉风险

不一致怎么发生

截断发生在"重新加入"时,但本地 HW 是滞后的快照:若 A 的本地 HW 记录偏小、或 A 被再次选主,不同副本会以不同位置为"真相"——消费者在切换前后可能读到不同内容的同一位移(脏读),或已读消息重新出现(重复)。

与问题一的区别(答题加分点)

问题一丢的是已 ack 的数据(acks=1 场景);问题二是已提交数据的一致性视图被破坏——即使 acks=all,HW 的异步传播仍可能让截断决策与当期 leader 脱节。共同根源:HW 是滞后的、异步传播的截断依据

面试一句话:"HW 机制的两个缺陷同根:用'滞后的全局水位'指导'本地的截断决策'。前者表现为已 ack 数据丢失(快故障),后者表现为副本间日志错位与消费视图不一致(慢故障)——KIP-101 用 LeaderEpoch 把截断决策改成'向当期 leader 问边界'来一并解决。"
问题二比问题一更绕,抓住时间线:A 是 leader,m1 已提交 m2 未提交;A 短暂失联切主到 B;A 恢复后按本地 HW 截断,m2 被截掉;麻烦在变体:老机制加 sticky leader 选举可能把 A 再选回去,而它的日志已经被截短了——不同副本以不同位置为真相,消费者可能读到不同内容的同一位移,这就是不一致和重复。和问题一的区别要主动说:问题一丢已 ack 数据,问题二破坏已提交数据的一致性视图,根源相同——滞后的水位指导本地截断。这句总结完直接引出 KIP-101。

KIP-101 · LeaderEpoch(0.11+)

KIP-101:把"盲截 HW"改成"向 leader 问 epoch 边界"

旧 HW 截断与 LeaderEpoch 截断对比 旧机制 follower 重启后按本地 HW 盲目截断,存在数据丢失与日志错位风险;LeaderEpoch 机制下 follower 重启先发 OffsetsForLeaderEpoch 查询当前 epoch 的末端 offset,只截断到该确定边界,保证同 epoch 内与 leader 完全一致,两个缺陷均被消除。 旧机制:截断只看本地 HW follower 重启恢复 本地日志 [m1, m2] · 本地 HW=1(滞后) 盲截:truncate to 本地 HW → m2 被截掉(决策与 leader 真实日志无关) 从截断点重新拉取 若此时发生切主 → 以截短后的日志参与选举 两个缺陷(对应第 5/6 页) ① 快故障:截断后 leader 宕机 → 已 ack 数据丢失 ② 慢故障:本地 HW 偏小多截 / 老 leader 复活错位 → 副本日志分叉、消费视图不一致或重复 共同根源:滞后的水位 × 本地的截断决策 KIP-101:LeaderEpoch 截断校验(0.11+) follower 重启:带着当前 epoch 查询 每条日志段/批都带 partitionLeaderEpoch 发 OffsetsForLeaderEpoch(epoch) 给 leader 问:这个 epoch 在你那里到哪条为止? leader 返回该 epoch 的末端 offset 即使自己已切更高 epoch,也返回旧 epoch 的结束点 只截到该确定边界 → 续拉 同 epoch 内与 leader 完全一致;跨 epoch 由选举保证衔接 两个缺陷如何被解决 ① 截断点=当期 leader 数据的确定边界 → 已 ack 不再丢 ② 每次截断先校验 → 杜绝盲截与错位 LeaderEpochCache:[(epoch=0, start=0), (1, 20), (2, 55)] —— 每个 epoch 的起始 offset,随日志持久化(checkpoint 文件) 消息批头携带 partitionLeaderEpoch → 日志即自带"任一点属于哪任 leader"的坐标;KRaft 的 Raft 日志同源思想
KIP-101 的精髓一句话:把截断决策从"查本地滞后的水位"改成"问当期 leader 要确定的边界"。流程四步:重启带 epoch 查询、发 OffsetsForLeaderEpoch、leader 返回该 epoch 的末端 offset、只截到这个点。为什么能同时解决两个问题:第一,截断点是当期 leader 数据的确定边界,已 ack 的数据不可能被截掉;第二,每次截断都先校验,盲截和错位不存在了。结构上记 LeaderEpochCache:epoch 到起始 offset 的映射随日志持久化,消息批头自带 partitionLeaderEpoch,整条日志就是带版本标记的历史。

Truncation · Cross-DB View

日志截断的三个场景,以及与 MySQL 主从复制的对照

截断场景触发与规则安全边界
① follower 恢复截断重启/长时间掉队后重新加入:先向 leader 校验 LeaderEpoch 末端 → 截到确定边界 → 续拉只截未提交段(校验后);已提交数据不动
② 选举后对齐截断新 leader 日志可能短于旧 follower(旧 leader 未截断的未提交段):原 ISR 成员以新 leader 为准截齐截掉的均为未过 HW 的未提交数据
③ unclean 选举后全量重同步开启 unclean 后新 leader 可能远落后:存留副本需大幅截断会出现已提交数据丢失(见下页)
概念KafkaMySQL 主从(对照记忆)
复制坐标offset + LeaderEpoch(epoch 起始 offset 表)binlog (file, pos) / GTID 集合——GTID 同样是"复制身份"思想
截断规则以当期 leader 日志为准(epoch 校验)从库重做/回滚对齐主库;GTID 自动跳过已执行事务
掉队判定replica.lag.time.max.ms=30s 收缩 ISRSeconds_Behind_Master 监控(无自动剔除)
提交门禁acks=all + min.insync.replicas半同步复制 rpl_semi_sync_master_wait_for_slave_count
跨 deck 串联:"Kafka 的 ISR+LeaderEpoch 与 MySQL 的主从+GTID 是同一组问题(复制坐标、截断、掉队、提交门禁)的两套实现——面试被问差异时按这张表的行逐一对照。"(半同步与 binlog 提交侧详见 MySQL 事务 deck
截断场景收拢成三种:follower 恢复、选举后对齐、unclean 后全量重同步,前两种只动未提交段,第三种会丢已提交数据,这个安全边界是判断一切复制异常的标尺。下半张表是和 MySQL 主从复制的对照:GTID 和 LeaderEpoch 都是复制身份思想,semi-sync 和 acks 加 min.insync 都是提交门禁。面试被问跨系统差异就按行对照,这张表也是和相关 deck 串联的桥。

Unclean Leader Election

unclean.leader.election.enable:C 与 A 的总开关

false(默认,4.3 源码核实)

选举只允许从 ISR 成员中挑。ISR 全部不可用时:分区不可读不可写(新 leader 选不出),直到任一 ISR 副本恢复——宁肯停服,不丢已提交数据。金融/订单类链路的正确选择。

true

ISR 全挂时允许非 ISR 副本(OSR)上位:它的日志落后,上位即发生截断 → 已提交(已 ack 给生产者)的消息可能永久丢失。换取分区立即可用——日志类、可从上游重放的数据可以接受。

追问答案
默认 false 时"副本全挂"到底什么现象?produce 全部失败(超时/重试耗尽),consume 不可用;客户端视角是可用性事故而非数据事故——恢复后从 ISR 副本继续,数据零丢失
和 min.insync.replicas 什么关系?两层独立的门:min.insync 管"ISR 够不够资格收写"(写入可用性),unclean 管"非 ISR 能不能当 leader"(故障切换语义)。都不开 → 一致性最强、可用性最弱
运维建议?默认保持 false;确要开启按 topic 粒度(broker 默认值 + topic 覆盖)只对可容忍丢失的 topic 打开,并配监控告警"ISR 收缩到 1"提前介入
答题模板:"这是 CAP 里 C 与 A 的显式交换:unclean=false 保 C(可能不可用),true 保 A(可能丢已提交消息)。Kafka 把选择权交给 per-topic 配置,默认 false——先保证不骗人(不丢 ack 过的数据),可用性靠 ISR 健康度和容量规划来保。"
unclean 选举是 CAP 取舍的显式开关。默认 false:只能从 ISR 里选 leader,ISR 全挂就停服,宁可不可用不丢已提交数据。true:OSR 副本可以上位,日志落后上位即截断,已 ack 的消息可能永久丢。两个追问:默认 false 时全挂的现象是可写不可读的可用性事故,恢复后数据零丢;它和 min.insync 是两层独立的门,一个管写入资格一个管选举资格。运维建议:保持 false,确要开按 topic 粒度只开可容忍的,再配 ISR 收缩告警提前介入。

acks=all × min.insync.replicas × ISR

为什么 acks=all 仍可能丢:承诺对象是"当时的 ISR"

acksmin.insync.replicas当前 ISR 数结果解读
all23✓ 正常:等 ISR 全部落盘(含 leader)提交即冗余 3 份
all22✓ 正常(贴线运行)再掉一台 → 见下一行
all21拒写 NotEnoughReplicas可用性换一致性:宁可停写不降级提交
all11退化为 acks=1:leader 落盘即 ack高频考点:all 只等 ISR——ISR 收缩到 1 时承诺消失,单点故障即丢
1任意≥1leader 落盘即回执未及复制即宕机 → 丢(第 5 页场景)
0发出即成功不重试不保证;乱序也可能

两个异常别混淆

NotEnoughReplicasException:append 前发现 ISR < min.insync → 拒收(日志无此消息),producer 重试直至超时;NotEnoughReplicasAfterAppendException:append 后 ISR 掉到门禁之下 → 消息在 leader 保留但不推进 HW/不 ack,等 ISR 恢复或重试超时。两者都是可重试异常。

生产组合拳(推荐基线)

topic:replication.factor=3 + min.insync.replicas=2;producer:acks=all(默认)+ enable.idempotence=true(默认)+ 回调兜底;broker:unclean.leader.election.enable=false(默认)。丢消息四查:acks 配置 → min.insync → unclean → 客户端是否处理回调异常。

标准答案句:"acks=all 的语义是'等 ISR 全部'而不是'等全部副本'——所以它防的是 ISR 内的丢失;ISR 本身收缩到 1 时的承诺真空,靠 min.insync.replicas 拒写来堵,这就是两者必须成对配置的原因。"
这页是"acks=all 为什么还丢"的完整答案。核心一句:all 只等 ISR 不等 AR。矩阵里最重要的一行是第四行:min.insync 配一、ISR 收缩到一,all 退化成 acks=1,单点故障即丢——这就是为什么 min.insync 必须显式调成二,默认值一等于没设防。两个异常的区分:append 前拒收和 append 后不推进 HW,都是可重试异常。最后给生产基线四件套:RF3、min.insync 二、unclean 关、幂等开,加上丢消息四查的排查顺序。

KRaft · Why

为什么移除 ZooKeeper:三宗罪与一次自我救赎

① 元数据双系统

集群真相存在 ZK,controller 要与 ZK 双向同步——两套一致性系统并存:ZK 仲裁 + Kafka 自身 ISR。controller 切换需从 ZK 重读全量元数据,故障切换慢且存在不一致窗口(元数据缓存与 ZK 不同步的脑裂风险)。

② 扩容瓶颈

ZK 的 znode 写入与 watch 通知吞吐限制了分区规模:每次分区状态变更、leader 切换都要写 ZK 并广播。KIP-500 的目标就是把元数据做成 Kafka 自己的 Raft 日志,支撑百万级分区

③ 运维复杂

两套系统两套部署/监控/权限/安全模型:ZK 集群本身要维护、升级、扩容;跨系统故障排查(ZK 会话过期 vs broker 心跳)认知负担重。KRaft 后一个集群一套软件

追问答案
KRaft 的核心思路一句话?元数据也是日志:把集群元数据(broker 注册、topic/分区/配置、ISR 变更)做成内部 Raft 日志主题 __cluster_metadata,由 controller quorum 用 Kafka 自己的 Raft 实现(KIP-595)复制
ZK 时代 controller 怎么选主?KRaft 后呢?ZK 时代靠 ZK 的临时节点/锁选 controller(单点活跃,切换慢);KRaft 后 controller quorum 内部跑 Raft 选主——复用与数据面同源的共识机制,秒级且更稳
移除 ZK 对已有用户意味着什么?4.0 起 ZK 模式代码整体移除(不是弃用):升级必须先迁 KRaft(zk2kraft 迁移路径);配置体系同步清理(zookeeper.connect 等退出历史舞台)
为什么移除 ZooKeeper 归纳为三宗罪:元数据双系统,两套一致性并存,controller 切换要重读 ZK,慢且有不一致窗口;扩容瓶颈,ZK 写吞吐限制分区规模,KIP-500 的目标就是百万级分区;运维复杂,两套部署监控权限。KRaft 的核心思路一句话就能立住:元数据也是日志,存进 __cluster_metadata 由 controller quorum 用 Raft 复制。选主对比也是常问点:ZK 时代靠临时节点锁,KRaft 后 quorum 内部跑 Raft 选主。版本口径:4.0 是整体移除不是弃用,升级必须先迁。

KRaft · Architecture

KRaft 架构:Controller Quorum + __cluster_metadata + 演进线

KRaft 架构与版本演进 三个 controller 节点组成 Raft 元数据仲裁,active controller 处理元数据写入并落盘到 __cluster_metadata 日志,多数派确认;broker 通过快照与增量拉取同步元数据到本地 MetadataCache;底部时间线展示 2.8 预览、3.3 production ready、4.0 移除 ZooKeeper、4.3 现行版本的演进。 CONTROLLER QUORUM · 3 / 5 个(奇数,多数派 2f+1 容 f)· KRaft 协议(KIP-595 Kafka Raft) Controller 1(active leader) 处理元数据写入 · 发布记录 RackId / broker 心跳超时裁决 Controller 2(follower) Raft 追日志 · 热备 Controller 3(follower) Raft 追日志 · 热备 Raft 多数派提交后元数据才算生效——与数据面 ISR 思想同源,但这里是真正的多数派 quorum __cluster_metadata:元数据 Raft 日志(broker 注册 · topic/分区/配置 · ISR/leader 变更 · 配额…) MetadataSnapshot / 增量 fetch Broker 1 数据日志(分区副本) 本地 MetadataCache(元数据副本) Broker 2 同构:数据面照旧工作 produce/fetch 与 ZK 时代协议一致 Broker 3 小集群可 combined 模式: controller 与 broker 同进程 2.8KIP-500 预览 3.3KIP-833:KRaft production ready 4.0(2025-03)彻底移除 ZooKeeper · KRaft-only 4.3现行版本(KRaft 唯一模式)
KRaft 架构看三层。上层 controller quorum,三或五个奇数节点跑 Kafka 自己的 Raft,active controller 处理元数据写入,多数派落盘到 __cluster_metadata 才算生效——注意这是真正的多数派,和数据面 ISR 是两套机制。中层 broker 用快照加增量 fetch 把元数据同步成本地 MetadataCache,数据面协议基本没变。底部时间线背四个点:2.8 预览、3.3 production ready、4.0 彻底移除 ZK、4.3 现行。追问点:小集群可以 combined 模式让 controller 和 broker 同进程,生产大集群建议分离部署。

Interview QA · 1/2

ISR 与一致性 8 连问

1 · ISR 是什么?怎么进入和退出?

动态副本集30s 追平窗口

in-sync replicas:与 leader 保持追平的副本集合(含 leader)。退出:30s(replica.lag.time.max.ms)内没发 fetch 或没追到日志末端 → 移出 OSR。回归:截断对齐 + 全量补齐 + 追平末端 → leader 自动拉回,无需重启。

2 · LEO 和 HW 是什么?消费者能读到什么?

LEO 末端HW 可见边界

LEO=各副本日志下一条写入位置;HW=leader 按 min(ISR LEO) 裁决的可见边界,随 fetch 响应异步广播。消费者只读 < HW——永远看不到可能随宕机消失的未提交数据。

3 · follower 什么时候被截断日志?

恢复/选举后对齐

两类正常场景:重启/掉队恢复时按 LeaderEpoch 校验截到确定边界;leader 切换后以新 leader 为准对齐(截掉的均为未过 HW 的未提交段)。unclean 选举后才有"截掉已提交数据"的例外。

4 · acks=all 为什么还可能丢消息?

只等 ISRISR 可收缩到 1

all 的承诺对象是"提交时刻的 ISR"而非全部副本:ISR 收缩到 1 时退化为 acks=1,leader 单点故障即丢。解法:min.insync.replicas=2 拒绝低冗余写入(NotEnoughReplicas)+ unclean=false + 幂等回调兜底。

5 · min.insync.replicas 与两个异常的区别?

写入门禁拒收 vs 不提交

它是 broker 端写入门禁:ISR 数不足则拒。NotEnoughReplicasException=append 前拒收(消息没进日志);NotEnoughReplicasAfterAppendException=append 后 ISR 掉线,消息保留但不推进 HW 不 ack。两者均可重试。

6 · unclean.leader.election.enable 默认值与权衡?

false(默认)C 换 A

默认 false(4.3 源码):只从 ISR 选 leader,ISR 全挂则分区不可用但已提交数据零丢;true 允许 OSR 上位,可能丢已 ack 消息,换取立即可用。按 topic 粒度对可容忍丢失的数据开启。

7 · Kafka 复制为什么不用多数派写?

f+1 容 f省一半副本

ISR 模型 f+1 副本容 f 故障(多数派需 2f+1):2 副本容 1 台宕机,磁盘与写放大省一半。代价:acks=all 的提交延迟受最慢 ISR 成员影响(多数派只等最快过半)。可用性靠 min.insync 门禁与 unclean 开关显式取舍。

8 · 副本全挂了会怎样?

不可用 vs 丢数据

unclean=false(默认):分区不可读不可写,客户端超时/重试失败——可用性事故但数据安全,任一 ISR 副本恢复即服务恢复;unclean=true:OSR 上位立即恢复服务,但截断导致已提交消息丢失。先监控 ISR 收缩,别等到全挂。

上半场八题覆盖 ISR 主干。第二题两句话定义 LEO 和 HW,加上异步传播这个性质。第四题是核心考点:all 只等当时的 ISR,ISR 收缩到一就退化,答案落在 min.insync 等于二加 unclean 关闭。第五题两个异常要分清拒收和不提交。第七题说明白为什么不用多数派:f 加一容 f 省一半副本,代价是提交延迟受最慢成员拖累。第八题把全挂现象说成"可用性事故而非数据事故",这句话能体现对默认值设计的理解。

Interview QA · 2/2

HW 缺陷、KIP-101 与 KRaft 8 连问

9 · HW 机制有哪两个经典缺陷?

数据丢失日志错位

① 快故障:acks=1 下已 ack 的消息只存在于 leader,follower 截断后 leader 宕机 → 该消息永久丢失;② 慢故障:本地 HW 滞后导致多截、老 leader 复活错位 → 副本日志分叉、消费视图不一致/重复。共同根源:滞后的水位指导本地截断决策。

10 · KIP-101 怎么修复的?

LeaderEpochOffsetsForLeaderEpoch

日志带 partitionLeaderEpoch;follower 截断前发 OffsetsForLeaderEpoch(当前 epoch),leader 返回该 epoch 的末端 offset → 只截到确定边界再续拉。同 epoch 内与 leader 严格一致:① 已 ack 数据不再被截 ② 盲截与错位消失(0.11+ 实现)。

11 · LeaderEpoch 的数据结构?

epoch→startOffset随日志持久化

LeaderEpochCache 维护 [(epoch, startOffset)] 向量,如 [(0,0),(1,20),(2,55)],checkpoint 文件随日志持久化;每条消息批头携带 partitionLeaderEpoch。选举成功即 epoch+1——"任"是复制协议的一致性分界。

12 · 与 MySQL 主从复制对照?

GTID ↔ LeaderEpochsemi-sync ↔ acks 门禁

复制坐标:binlog(GTID) ↔ offset+epoch;掉队处理:Seconds_Behind 监控 ↔ 30s 收缩 ISR;提交门禁:semi-sync 副本数 ↔ acks=all+min.insync;截断:GTID 跳过已执行 ↔ epoch 校验截断。同一组问题两套实现。

13 · 为什么移除 ZooKeeper?

双系统扩容瓶颈运维复杂

① 元数据真相在 ZK:双一致性系统、controller 切换慢且有不一致窗口;② ZK 写吞吐限制分区规模(KIP-500 目标百万分区);③ 两套部署/监控/权限的认知负担。KRaft 把元数据做成自己的 Raft 日志,一套软件闭环。

14 · KRaft 的架构要点?

Controller Quorum__cluster_metadata

3/5 个奇数 controller 组成 Raft 仲裁(KIP-595 协议),active controller 处理元数据写入,多数派落盘 __cluster_metadata(单分区元数据日志);broker 通过快照+增量 fetch 维护本地 MetadataCache;数据面 produce/fetch 协议与 ZK 时代基本一致。

15 · KRaft 的版本演进口径?

3.3 production ready4.0 移除 ZK

2.8 KIP-500 预览(early access);3.3 KIP-833 标记 production ready;4.0(2025-03)成为首个无 ZooKeeper 的大版本——ZK 模式代码整体移除,升级必须先迁 KRaft(zk2kraft)。4.3 为现行版本,KRaft 是唯一模式。

16 · KIP-392 follower fetching 是什么?

机架就近读2.4+

消费者/ follower 配置 client.rack,broker 按 broker.rack 把 fetch 路由到本机架的副本 follower——跨机房部署省跨区带宽与延迟。注意就近读可见性略滞后于 leader;默认不开启(默认读 leader)。

下半场八题。第九第十连着答:两个缺陷同根同源,KIP-101 四步流程要能脱稿讲,OffsetsForLeaderEpoch 这个 API 名要报得出来。第十一题报结构:epoch 加 startOffset 的 checkpoint 文件,批头带 partitionLeaderEpoch。第十二题 MySQL 对照是跨领域加分题。第十三到十五是 KRaft 三连:为什么移除、架构要点、版本演进口径,三个版本号必须准确。第十六题 KIP-392 一句话收尾:机架就近读省跨机房带宽,默认不开。

Related Decks

相关知识点与答题串联

本库相关 deck

答题串联 · 一图流

ISR 动态集合(30s 窗口)→ LEO/HW 水位
HW 截断 → 丢失(快)/错位(慢)两缺陷
KIP-101 → OffsetsForLeaderEpoch 校验截断
acks=all × min.insync × ISR → 拒写矩阵
元数据一致性 → KRaft(3.3 ready → 4.0 去 ZK)

收尾第一页给同领域链接与答题串联主线:ISR 水位、两个缺陷、KIP-101、acks 矩阵、KRaft——五行走完,这条 deck 的骨架就复现了。四条链接各对应一次跨 deck 复习:写路径、客户端语义、消费可见性、MySQL 对照。

References

参考来源

本 deck 版本敏感结论均可溯源至下列一手材料(2026-08 核实)

kafka.apache.org/documentation(#design · #datacenters · #configuration)复制设计、ISR 语义、acks/min.insync/unclean 配置说明
github.com/apache/kafka(4.3)server/.../config/ReplicationConfigs.java · storage/.../LogConfig.java · server-common/.../ServerLogConfigs.java源码级默认值核实:replica.lag.time.max.ms=30000 · unclean.leader.election.enable=false · min.insync.replicas=1
cwiki.apache.org/confluence/display/KAFKA/KIP-101(Alter Replication Protocol to use Leader Epoch rather than High Watermark for Log Truncation)两个 HW 缺陷的原始推演与 LeaderEpoch 方案(lists.apache.org 讨论线程同源)
cwiki.apache.org/confluence/display/KAFKA/KIP-392(Allow consumers to fetch from closest replica)机架就近拉取(client.rack/broker.rack,2.4+)
KIP-500 / KIP-595 / KIP-631 / KIP-833(cwiki.apache.org/confluence/display/KAFKA/)KRaft:元数据 quorum 提案、Kafka Raft 协议、quorum-based controller、3.3 production ready
kafka.apache.org/blog/2025/03/18/apache-kafka-4.0.0-release-announcement/4.0 发布公告:首个完全移除 ZooKeeper 的大版本(KRaft-only)

最值得原文精读:KIP-101——两个 HW 缺陷的原始推演就写在它的讨论线程里。

键盘操作: 翻页 · T 换主题 · S 演讲者模式 · O 总览。

溯源清单页。最值得精读的是 KIP-101 原文——两个缺陷的原始推演就写在讨论线程里,以及 4.3 源码的三个默认值(30s 追平窗口、unclean 默认 false、min.insync 默认 1)。所有版本敏感结论都标了出处,被深挖时直接报材料。