bigywater.tech
Kafka 内部原理 · 20

Unclean Leader Election:为什么可能丢消息

默认情况下,只有 ISR 里的副本才能成为新 Leader。unclean.leader.election.enable=true 允许一个不在 ISR 里、数据落后的副本上位。这次在集群里把这个场景完整走一遍:先让 Follower 掉出 ISR,Leader 独自写入 100 条并被 acks=all 确认,然后 Leader 也宕机,只让落后的 Follower 活着,观察关闭和开启 unclean 选举两种情况下分区和数据的变化。

验证:只有落后副本存活时,默认配置下分区没有 Leader,写入超时
kill -9 Leader 并让落后的 broker2 回来之后,这个分区的状态是 Leader: none、Isr
验证:开启 unclean 选举后,落后副本上位,100 条已被确认的消息消失
打开开关几秒后 broker2 成为 Leader,Isr 只有它自己。

验证:只有落后副本存活时,默认配置下分区没有 Leader,写入超时

命令 · shell $ kafka-topics.sh --create --topic unclean-demo --replica-assignment 1:2 \ --config min.insync.replicas=1 --config unclean.leader.election.enable=false # 1) 两个副本都在线时写入 base-1 ~ base-100,acks=all,offset=100,两边日志都是 1489 字节 # 2) kill -9 broker2(Follower),等 ISR 收缩到 "Isr: 1" # 3) 只有 broker1 在 ISR 时写入 onlyleader-1 ~ onlyleader-100,acks=all,成功,offset=200 # broker1 日志 3578 字节,broker2 日志仍是 1489 字节 # 4) kill -9 broker1(Leader),重启 broker2 $ kafka-topics.sh --describe --topic unclean-demo $ echo "probe-while-offline" | kafka-console-producer.sh --topic unclean-demo --command-property acks=all \ --command-property delivery.timeout.ms=6000
真实实测 · 本机执行输出
Leader: none  Replicas: 1,2  Isr:   Elr: 1  LastKnownElr: 1

TimeoutException: Expiring 1 record(s) for unclean-demo-0:6001 ms has passed since batch creation.

kill -9 Leader 并让落后的 broker2 回来之后,这个分区的状态是 Leader: none、Isr 为空。broker2 明明活着,也是这个分区的副本,但它不在 ISR 里,默认配置不允许它成为 Leader,分区就这样离线了;写入尝试 6 秒后超时。这个版本的输出里多了 Elr: 1(Eligible Leader Replicas,可当选副本),显示 broker1 是唯一有资格当 Leader 的副本,而它已经死了。这也是"选择可用性还是一致性"的一面:默认配置选了一致性,宁可不可用。

验证:开启 unclean 选举后,落后副本上位,100 条已被确认的消息消失

命令 · shell $ kafka-configs.sh --entity-type topics --entity-name unclean-demo --alter --add-config unclean.leader.election.enable=true $ kafka-topics.sh --describe --topic unclean-demo $ kafka-get-offsets.sh --topic unclean-demo --time -1 $ kafka-console-consumer.sh --topic unclean-demo --partition 0 --offset 0 # 按前缀统计
真实实测 · 本机执行输出
Leader: 2  Replicas: 1,2  Isr: 2
unclean-demo:0:100

100 base            <- 只剩这 100 条
(onlyleader-* 一条也没有)

打开开关几秒后 broker2 成为 Leader,Isr 只有它自己。log end offset 是 100,不是 200:在 broker1 上被 acks=all 确认过的 onlyleader-1 到 onlyleader-100 这 100 条消息,在新的 Leader 上根本不存在,消费者从 offset 0 读到的 100 条全是 base-*。当时那 100 条写入 Producer 是成功返回的,也就是说这是"已确认写入后消息消失",而不是写入失败。原因很直接:这 100 条只存在于 broker1 的磁盘上(日志 3578 字节,而 broker2 只有 1489 字节),broker2 从来没有拉取过。

验证:旧 Leader 回来后,多出来的数据被截断

命令 · shell # 在新 Leader 上再写 20 条 after-*,offset=120;然后重启 broker1 $ ls -l /private/tmp/kafka-lab/node1/unclean-demo-0/*.log $ grep "Truncating" /tmp/broker1-restart3.log $ cat /private/tmp/kafka-lab/node1/unclean-demo-0/leader-epoch-checkpoint
真实实测 · 本机执行输出
Leader: 2  Replicas: 1,2  Isr: 1,2
broker1 日志: 3578 字节 -> 1841 字节
WARN [UnifiedLog partition=unclean-demo-0] Truncating unclean-demo-0 to offset 100 below high watermark 200
分别连 broker1、broker2 查询 log end offset,都返回 120
leader-epoch-checkpoint: 0 / 2 / "0 0" / "2 100"

重启后的 broker1 回到 ISR 之前,日志里有一条 WARN:Truncating unclean-demo-0 to offset 100 below high watermark 200。它把自己 offset 100 以后的内容截断,磁盘文件也从 3578 字节缩小到 1841 字节(100 条 base 加 20 条 after),随后分别连 broker1、broker2 查询 log end offset 都是 120(这个命令返回的是分区 Leader 的值,不是各自磁盘的独立读数),Isr: 1,2 恢复。leader-epoch-checkpoint 里新增的 2 100 记录了新 Leader 从 offset 100 开始写。注意这条 WARN 里"below high watermark"的措辞:被截断的部分本来是已经达到高水位 200 的数据,也就是集群曾经对外承诺过的数据。数据丢失是在这里被最终确定的。

结论

unclean.leader.election.enable 是一个的取舍:关闭时(默认),只有落后副本存活,分区会一直没有 Leader,写入超时,但已确认的数据不会丢;打开后,分区能立刻恢复,代价是这次实验里 100 条已被 acks=all 确认的消息永久丢失,旧 Leader 回来还会被强制截断。

分步演示:一次构造的双故障,数据是怎么丢掉的

每一步都是本文上面几节kill -9、切换配置、核对offset和日志大小得到的原始结果。

开启unclean选举,换来的是100条已确认消息永久消失
这一步新验证的状态

    参考与说明

    • 数据来自本机 3 节点 Kafka 4.3.1 集群:topic 副本指定在 broker1、broker2,broker3 全程保留以维持 KRaft Controller 多数派,所以所有管理命令都连接 broker2/3。min.insync.replicas=1 是让 ISR 收缩到只剩 Leader 时 acks=all 依然能写入的前提,这也是丢消息能发生的条件之一;如果 min.insync.replicas=2,第 3 步的写入会被拒绝。
    • 和《acks=0/1/all:Producer 到底什么时候认为写成功》一起看:那篇看到 acks=all 依赖 ISR,这篇看到当 ISR 只剩一个副本时,"全部确认"的保证也会随之缩水。
    • 没有验证:min.insync.replicas=2 的对照组;三副本下同样的场景;消费者在丢失前已经读到那 100 条时的表现(会看到 offset 回退)。
    京ICP备2026067502号-1京公网安备11010502063487号