bigywater.tech
Kafka 内部原理 · 19

Broker 崩溃后,副本如何接管 Leader

Leader 所在的 Broker 崩溃后,剩下的副本会接管,这一点前面的文章已经验证过。这篇关心的是客户端的体验:正在持续写入的 Producer 会中断多久、会不会报错、丢不丢消息,以及日志里留下了什么痕迹。这次让 Producer 用 acks=all 加幂等(enable.idempotence=true)同步逐条发送,写入过程中直接 kill -9 当前 Leader 所在的 Broker,做了两次。

验证:Leader 被杀后写入卡住 9~14 秒,但没有任何一条失败
两次试验里,被杀时刻正在发送的那一条消息都被卡住了很久(9.4 秒和 13.6 秒),最终成功;
验证:9 秒这个量级对应 broker.session.timeout.ms=9000,但第二次为什么更长没有查清
查到 KRaft 模式下 Broker 向 Controller 汇报心跳的间隔是 2000ms,超过 broker

验证:Leader 被杀后写入卡住 9~14 秒,但没有任何一条失败

命令 · java // 同步逐条发送,每条间隔 20ms,持续 40 秒,记录每条的耗时 KafkaProducer<String,String> pr = new KafkaProducer<>(p); // acks=all, enable.idempotence=true pr.send(new ProducerRecord<>("leader-failure-demo", "m"+ok)).get();
真实实测 · 本机执行输出
第一次:杀死 leader(broker2) @ 1790216135889
  慢请求 @1790216135889 耗时 9423ms
  成功 953 失败 0 最大耗时 9423ms
  Leader: 2 -> 3,Isr: 3,1

第二次:(broker2 重启并回到 ISR 后)杀死 leader(broker3) @ 1790216271633
  慢请求 @1790216271647 耗时 13614ms
  慢请求 @1790216286012 耗时 256ms
  慢请求 @1790216286515 耗时 819ms
  成功 714 失败 0 最大耗时 13614ms
  Leader: 3 -> 2,Isr: 1,2

两次试验里,被杀时刻正在发送的那一条消息都被卡住了很久(9.4 秒和 13.6 秒),最终成功;此外没有其他慢请求(第二次杀死后还有两条 256ms、819ms 的小波动)。总共 953 和 714 次发送,失败数都是 0。也就是说,对这个配置的 Producer,Leader 崩溃表现为"一次很长的延迟",而不是错误。这里要说明这个结论的前提:Producer 使用了幂等和默认的重试设置(delivery.timeout.ms 默认 120 秒),如果把 delivery.timeout.ms 设成小于故障切换时间,同样的场景就会变成失败;这次没有测这个反例。

验证:9 秒这个量级对应 broker.session.timeout.ms=9000,但第二次为什么更长没有查清

命令 · shell $ kafka-configs.sh --broker 1 --describe --all | grep -E "broker.session.timeout|broker.heartbeat.interval"
真实实测 · 本机执行输出
broker.heartbeat.interval.ms=2000
broker.session.timeout.ms=9000

查到 KRaft 模式下 Broker 向 Controller 汇报心跳的间隔是 2000ms,超过 broker.session.timeout.ms=9000(9 秒)没有心跳就被认为下线,然后才会选新的 Leader。第一次的 9423ms 与这个 9 秒很吻合;第二次的 13614ms 比 9 秒多了 4 秒多,这个差别我没有确认原因。可能与被杀的 broker3 当时是否同时承担 KRaft Controller 有关,但这只是猜测,我没有去查 Controller 的选举日志来证实。只做了两次试验,也不能据此给出一个稳定的"切换时长"数字,只能说:这个环境下,默认配置的切换耗时是 9 秒起步的量级。

验证:新 Leader 的 leader-epoch-checkpoint 精确记录了切换发生在哪个 offset

命令 · shell $ cat /private/tmp/kafka-lab/node3/leader-failure-demo-0/leader-epoch-checkpoint # 第一次切换后,新 Leader 上 $ kafka-dump-log.sh --files .../leader-failure-demo-0/00000000000000000000.log | grep baseOffset | sed -n '68,80p' $ cat .../node1/.../leader-epoch-checkpoint # 两次切换之后
真实实测 · 本机执行输出
第一次切换后 broker3 上的 checkpoint:        0 / 2 / "0 0" / "1 73"
两次切换后 broker1、broker2 上的 checkpoint: 0 / 3 / "0 0" / "1 73" / "2 965"

baseOffset: 72  CreateTime: 1790216135858
baseOffset: 73  CreateTime: 1790216135889     <- 恰好是 kill 时刻
baseOffset: 74  CreateTime: 1790216145337     <- 9448ms 之后

leader-epoch-checkpoint 每一行是"(Leader epoch, 该 epoch 的起始 offset)"。第一次切换后出现 1 73:新 Leader 的 epoch 1 从 offset 73 开始;第二次切换后又多了 2 965。对照 .log 里的时间戳,offset 73 的 CreateTime 正是 kill 的那一刻,offset 74 的时间比它晚了 9448 毫秒,与客户端测到的 9423ms 的卡顿完全对得上——被卡住的那条消息最终就落在 offset 73,由新 Leader 写入。两次切换、两个 epoch 起点,都与客户端看到的现象一一对应。offset 0 到 72 属于 epoch 0,没有丢失;第一次试验结束时查到的 log end offset 是 953,与那次试验的 953 次成功一致。

结论

结果是:默认配置下,Leader 崩溃对幂等的 acks=all Producer 表现为 9~14 秒的写入停顿而不是错误,数据没有丢失;停顿的量级与 broker.session.timeout.ms=9000 吻合;日志里 leader-epoch-checkpoint 记录着每次切换的起始 offset。如果业务不能接受这样长的写入停顿,要么缩短 broker 会话超时,要么在客户端设计上容忍长延迟;本文没有测试改小 broker.session.timeout.ms 的效果。

分步演示:一次的Leader崩溃,客户端和日志分别记录了什么

每一步都是本文上面几节kill -9 Leader、测量耗时、读取epoch checkpoint得到的原始结果。

Leader崩溃时,客户端感受到的不是错误,是一次很长的卡顿
这一步新验证的状态

    参考与说明

    • 数据来自本机 3 节点 Kafka 4.3.1 集群,topic 为 1 分区 3 副本,用 kill -9 杀死 Leader 所在的 Broker 进程;启动 Producer 时 bootstrap 只写另外两个 Broker,避免首次连接落到被杀节点上。
    • 和《Topic、Partition、Replica 到底是什么关系》一起看:那篇验证了"只有 Leader 在被杀节点上的分区才会切换",这篇看的是单分区切换时客户端的感受。
    • 没有验证:acks=1 或关闭幂等时同样场景是否丢消息或重复;delivery.timeout.ms 小于切换耗时时的失败形态;Consumer 在切换期间的表现;缩短 broker.session.timeout.ms 后切换时长的变化;第二次试验为什么比第一次更慢。
    京ICP备2026067502号-1京公网安备11010502063487号