Leader 所在的 Broker 崩溃后,剩下的副本会接管,这一点前面的文章已经验证过。这篇关心的是客户端的体验:正在持续写入的 Producer 会中断多久、会不会报错、丢不丢消息,以及日志里留下了什么痕迹。这次让 Producer 用 acks=all 加幂等(enable.idempotence=true)同步逐条发送,写入过程中直接 kill -9 当前 Leader 所在的 Broker,做了两次。
命令 · 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 设成小于故障切换时间,同样的场景就会变成失败;这次没有测这个反例。
命令 · 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 秒起步的量级。
命令 · 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 的效果。
每一步都是本文上面几节kill -9 Leader、测量耗时、读取epoch checkpoint得到的原始结果。