acks 决定 Producer 在什么条件下认为一条消息"写成功"。这次做了一个最直接的实验:用只有 1 个副本的 topic,在杀掉那个唯一副本所在 Broker 的同时发送消息,分别看 acks=0 和 acks=all 的反应。
命令 · java // topic 只有 1 个副本,杀掉这个副本所在的 Broker,跟发送消息同时进行 KafkaProducer<String,String> producer = new KafkaProducer<>(props); // acks=0 for (int i = 0; i < 500; i++) { producer.send(new ProducerRecord<>("acks0-loss-demo", "msg-" + i)); // fire-and-forget,不调用 get() }
客户端认为已经发送了 500 条(acks=0,没有等待任何服务端确认) --- 重启被杀掉的 Broker 后,用 kafka-get-offsets.sh 查这个 topic 的 log end offset --- acks0-loss-demo:0:0
的 kill -9 和的 500 次 send() 几乎同时发生,客户端在 acks=0 模式下不等待任何服务端响应,所以全部 500 次调用都"成功"返回了——没有一次抛异常、没有一次超时。但重启 Broker 后用 kafka-get-offsets.sh 查这个分区的 Log End Offset,结果是 0:一条消息都没有落地。这不是模拟出来的数字,是杀进程、重启、查 offset 得到的结果——acks=0 的"成功"只代表"客户端把字节交给了操作系统的网络层",跟"数据有没有真的到 Broker、有没有真的落盘"完全是两回事。
命令 · java // 同样的单副本 topic,同样杀掉这个副本所在的 Broker KafkaProducer<String,String> producer = new KafkaProducer<>(props); // acks=all, delivery.timeout.ms=5000 try { producer.send(new ProducerRecord<>("acksall-loss-demo", "msg-" + i)).get(); } catch (Exception e) { System.out.println("失败: " + e.getCause()); }
第一次失败: org.apache.kafka.common.errors.TimeoutException: Expiring 1 record(s) for acksall-loss-demo-0:5000 ms has passed since batch creation. The request has not been sent, or no server response has been received yet.
同样是杀掉唯一副本所在的 Broker,acks=all 配置下抛出了 TimeoutException,客户端能明确知道"这条消息没有被确认",可以选择重试、报错、走降级逻辑。这跟 acks=0 的行为是的、可复现的差别:不是"哪个更快"的取舍,而是"故障发生时,客户端到底知不知道"的根本区别。
实验直接回答了"Producer 什么时候认为写成功":acks=0 认为"发出去就算成功",哪怕目标 Broker 在消息真正到达前就已经挂了,客户端也不会知道——这次测出 500 条"成功"对应 0 条落地;acks=all 会等到确认失败,用一个可以被捕获处理的异常告诉调用方"这条没写成功"。选 acks 不是选性能数字,是选"故障发生时,你的系统会不会有一段时间对丢数据一无所知"。
每一步都是本文上两节杀进程、查offset、捕获异常得到的原始结果。