4. 生产者原理:从一条消息到成功落盘

生产者的代码,说白了就是 Kafka 的入口。你写的业务数据,最终能不能可靠地落到 Broker 上,全看这一关。我见过太多线上事故,都是因为对生产者原理一知半解导致的。今天咱们就把这块彻底掰开揉碎。

4.1 生产者整体架构

先看一张总图,把生产者的内部模块串起来。我个人习惯,学任何组件都先看架构,再看细节。

Kafka 生产者内部架构图 业务应用 KafkaProducer 主线程 拦截器链 序列化器 分区器 RecordAccumulator Sender 线程 Kafka Broker 核心流程:业务应用 → 拦截器 → 序列化器 → 分区器 → 累加器 → Sender 线程 → Broker

整个流程其实不复杂。你的业务线程调用 send(),数据经过拦截器、序列化器、分区器,先暂存在内存里的 RecordAccumulator。然后后台的 Sender 线程批量拉走,发到 Broker。嗯,这里要注意,send() 是异步的,它只是把消息丢进缓冲区就返回了。

4.2 消息发送流程详解

我把发送流程拆成 6 个步骤,你对照着上面的图看:

  1. 应用调用 send():业务代码传一个 ProducerRecord 进来,包含 topic、value,可选的 key 和 partition。
  2. 拦截器链处理:你可以自定义拦截器,比如做消息计数、修改消息内容。我曾在项目里用拦截器统一给消息打时间戳。
  3. 序列化:key 和 value 分别走各自的序列化器。默认是 StringSerializer,你也可以用 Avro、Protobuf。
  4. 分区选择:分区器决定这条消息去哪个分区。如果指定了 partition,直接用;否则按 key 哈希;key 为 null 则轮询。
  5. 写入累加器:消息被包装成 ProducerBatch,塞进对应分区双端队列的尾部。每个批次默认 16KB。
  6. Sender 线程发送:Sender 不断轮询累加器,拉取已准备好(批次已满或超时)的批次,通过 Selector 发到 Broker。

我的经验:send() 返回的 Future 一定要处理。我曾经见过同事只调 send 不调 get,结果生产环境丢了几十万条消息都不知道。要么加回调,要么调 get 阻塞等待。

4.3 分区选择策略

分区策略直接影响数据分布和消费性能。Kafka 默认提供了三种策略,你也可以自己实现 Partitioner 接口。

策略 触发条件 行为 适用场景
指定分区 ProducerRecord 传了 partition 直接发到指定分区 需要严格顺序的场景
Key 哈希 key 不为 null 对 key 做 murmur2 哈希,再对分区数取模 保证相同 key 进入同一分区
轮询 (Sticky) key 为 null 粘性分区器会尽量把一批消息发到同一个分区,减少网络开销 负载均衡,无 key 的日志类数据

你想想看,如果 key 为 null 还用老版的轮询策略,每条消息都随机选一个分区,那批次永远攒不满,吞吐量直接腰斩。Kafka 2.4 之后引入的 StickyPartitioner 就是为了解决这个问题。

避坑指南:我曾经遇到过一个案例,业务方用时间戳作为 key,结果所有消息都落到了同一个分区。因为时间戳的哈希值在短时间内高度集中。如果你用 key 做分区,一定要确保 key 的基数足够大,分布足够均匀。

4.4 ACK 确认机制

ACK 是生产者和 Broker 之间的确认协议。它决定了消息的可靠性有多高。说白了,就是生产者要等多少个副本确认,才认为消息发送成功。

Kafka 提供了三种 ACK 模式,通过 acks 参数配置:

acks 值 行为 可靠性 延迟 吞吐量
0 生产者发完就认为成功,不等待任何确认 最低 最低 最高
1 等待 Leader 副本写入本地日志后确认 中等 中等 中等
all (-1) 等待 Leader 和所有 ISR 副本都写入后确认 最高 最高 最低

我个人的建议:

  • 日志采集、监控数据:用 acks=0 或 1,丢几条无所谓,吞吐量优先。
  • 订单、支付、交易:必须用 acks=all,配合 min.insync.replicas=2,确保数据不丢。
  • 中间态数据:用 acks=1,兼顾性能和可靠性。

核心配置示例:

// 高可靠配置
props.put("acks", "all");
props.put("min.insync.replicas", 2);
props.put("retries", Integer.MAX_VALUE);
props.put("enable.idempotence", true);

// 高性能配置
props.put("acks", "1");
props.put("batch.size", 32768);
props.put("linger.ms", 10);
props.put("compression.type", "snappy");

4.5 幂等性与事务

说到 ACK,就不得不提幂等性。你想想看,如果 Broker 返回了 ACK,但网络超时了,生产者会重试。重试可能导致 Broker 写入两条相同的消息。这就是重复问题。

Kafka 0.11 引入了幂等生产者。开启 enable.idempotence=true 后,每个生产者有一个唯一的 producerId,每条消息带一个递增的序列号。Broker 会去重,保证即使重试也不会重复写入。

如果你需要跨分区、跨 Topic 的原子性写入,那就得用事务了。事务的底层也是基于幂等性实现的。不过事务的开销比较大,非必要不用。

我的习惯:只要不是对吞吐量有极致要求的场景,我都会开启幂等性。它几乎不增加延迟,但能省掉你排查重复数据的痛苦。我曾经在生产环境排查了三天重复数据,最后发现就是没开幂等性。

4.6 生产者参数调优速查

最后给一张常用参数速查表,方便你配置时参考:

参数名 默认值 说明 调优建议
bootstrap.servers 无 Broker 地址列表 至少写 2-3 个,防止单点
acks 1 确认模式 重要数据用 all
buffer.memory 33554432 (32MB) 生产者缓冲区总大小 高吞吐场景可调大到 64MB
batch.size 16384 (16KB) 单个批次大小 大消息调大,小消息保持默认
linger.ms 0 批次等待时间 5-20ms 可显著提升吞吐
compression.type none 压缩算法 推荐 snappy 或 lz4
max.request.size 1048576 (1MB) 单次请求最大大小 大消息场景调大
retries 2147483647 重试次数 配合 acks=all 使用

嗯,生产者原理这块就讲到这里。记住一句话:生产者的配置,本质上是在可靠性、延迟、吞吐量三者之间做权衡。没有银弹,只有根据业务场景做取舍。