开启幂等后,Broker 靠两样东西识别重复:Producer ID(PID,每个 Producer 实例一个)和每个分区上单调递增的序列号(Sequence Number)。这次先直接看日志文件里这两个字段长什么样,再看幂等对配置的强制要求,最后在的 Leader 宕机场景下,对比开启和关闭幂等时 topic 里的重复和乱序情况。
命令 · shell # 三个 Producer 实例依次往同一个分区各同步发送 25 条: # runA: 默认配置(幂等);runB: 另一个默认配置的实例;runC: enable.idempotence=false, acks=1 $ kafka-dump-log.sh --files /private/tmp/kafka-lab/node1/idem-demo-0/00000000000000000000.log | grep baseOffset
baseOffset: 0 baseSequence: 0 producerId: 14011 producerEpoch: 0 baseOffset: 24 baseSequence: 24 producerId: 14011 producerEpoch: 0 baseOffset: 25 baseSequence: 0 producerId: 16002 producerEpoch: 0 <- runB:新的 PID,序列号从 0 重来 baseOffset: 26 baseSequence: 1 producerId: 16002 producerEpoch: 0 baseOffset: 49 baseSequence: 24 producerId: 16002 producerEpoch: 0 baseOffset: 50 baseSequence: -1 producerId: -1 producerEpoch: -1 <- runC:关闭幂等,没有 PID 也没有序列号 baseOffset: 74 baseSequence: -1 producerId: -1 producerEpoch: -1 各 producerId 出现的批数:25 个 -1,25 个 14011,25 个 16002
同一个 Producer 实例写入的每个批次,baseSequence 严格按 0、1、2……递增,PID 不变;第二个实例拿到了另一个 PID(16002),序列号从 0 重新开始;关闭幂等的实例写入的批次,PID 和序列号都是 -1,Broker 对它们没有任何去重信息可用。这里因为是同步逐条发送,每个批次只有 1 条消息,所以 baseSequence 恰好等于这个实例内的消息序号;批次里有多条消息时,baseSequence 是这个批次第一条的序号。Broker 就是靠"PID 加分区加序列号"这个三元组判断一条消息是不是重复,以及是不是按顺序到达。
命令 · java new KafkaProducer<>(props); // 分别设置几种冲突配置
默认 enable.idempotence = true,默认 acks = all,默认 max.in.flight.requests.per.connection = 5,默认 retries = 2147483647 enable.idempotence=true + acks=1 -> ConfigException: Must set acks to all in order to use the idempotent producer. enable.idempotence=true + max.in.flight=6 -> ConfigException: ... max.in.flight.requests.per.connection must be set to at most 5. Current value is 6. enable.idempotence=true + retries=0 -> ConfigException: Must set retries to non-zero when using the idempotent producer. enable.idempotence=false + acks=1 + max.in.flight=6 -> 创建成功 只设置 acks=1(不显式设置幂等) -> 创建成功
这个版本的默认值里 enable.idempotence 已经是 true,同时默认 acks=all、max.in.flight.requests.per.connection=5、retries 是最大整数——也就是说不做任何配置,Producer 默认就是幂等的。显式开启幂等的同时,把 acks 改成 1、把并发请求数改成 6、或者把重试次数设成 0,都会在创建 Producer 时被拒绝。最后一行值得注意:只设置了 acks=1 而不显式设置幂等时,Producer 可以正常创建。我没有去确认此时幂等是否被悄悄关闭了,只知道创建没有报错;结合上一节日志里 runC 的 PID 是 -1(那次显式关闭了幂等),这个场景下的实际 PID 状态没有单独查。
命令 · java // 同一个程序:异步发送 30000 条 "seq-N"(max.in.flight=5, acks=all, batch.size=2000, 每 50 条 sleep 1ms), // 发到第 6000 条时 kill -9 分区的 Leader 所在 Broker;发送结束后从头消费,统计重复条数和乱序次数 // (乱序 = 出现了比前面已经见过的序号更小的序号);每次都让 bootstrap 不包含被杀的 Broker
关闭幂等: 第 1 次: 回调成功 30000 / 失败 0 topic 共 30000 条,重复 0 条,乱序 150 次 第 2 次: 回调成功 30000 / 失败 0 topic 共 30100 条,重复 100 条,乱序 450 次 第 3 次: 回调成功 30000 / 失败 0 topic 共 30100 条,重复 100 条,乱序 400 次 开启幂等: 第 1 次: 回调成功 30000 / 失败 0 topic 共 30000 条,重复 0 条,乱序 0 次 第 2 次: 回调成功 30000 / 失败 0 topic 共 30000 条,重复 0 条,乱序 0 次 第 3 次: 回调成功 30000 / 失败 0 topic 共 30000 条,重复 0 条,乱序 0 次
六次运行里,Producer 的回调都报告"成功 30000、失败 0",从客户端看不出任何区别。差别在 topic 里:关闭幂等的三次都出现了乱序(150、450、400 次),其中两次还多出 100 条重复的消息(30100 条,唯一 30000);开启幂等的三次,重复和乱序都是 0。乱序的原因是 Leader 被杀后,多个在途的批次被重试,先失败的批次可能在后面的批次之后才被写入;重复来自"消息其实已经写入但确认没有回到客户端,于是重发"。这两个解释是按现象和机制的推断,我没有单独抓取重试日志去逐条证实。每组只有三次,重复条数两次都恰好是 100,这个数字的原因我没有去查(可能与批次大小有关,但没有验证)。
幂等 Producer 的机制是"PID 加序列号":日志里每个批次都记录着 PID 和 baseSequence,Broker 据此拒绝重复和乱序的批次。在这次 Leader 宕机的对照里,关闭幂等的三次都出现了乱序,其中两次有 100 条重复,开启幂等的三次没有任何重复和乱序。这个版本默认已经开启幂等,但它对 acks、max.in.flight.requests.per.connection、retries 有硬性要求。幂等只覆盖"同一个 Producer 实例在一个分区上的重试",换一个实例就是新的 PID,不受这个保护。
每一步都是本文上面几节用kafka-dump-log.sh读取日志、kill -9 Leader后统计得到的原始结果。