4. 生产者原理:从一条消息到成功落盘
生产者的代码,说白了就是 Kafka 的入口。你写的业务数据,最终能不能可靠地落到 Broker 上,全看这一关。我见过太多线上事故,都是因为对生产者原理一知半解导致的。今天咱们就把这块彻底掰开揉碎。
4.1 生产者整体架构
先看一张总图,把生产者的内部模块串起来。我个人习惯,学任何组件都先看架构,再看细节。
整个流程其实不复杂。你的业务线程调用 send(),数据经过拦截器、序列化器、分区器,先暂存在内存里的 RecordAccumulator。然后后台的 Sender 线程批量拉走,发到 Broker。嗯,这里要注意,send() 是异步的,它只是把消息丢进缓冲区就返回了。
4.2 消息发送流程详解
我把发送流程拆成 6 个步骤,你对照着上面的图看:
- 应用调用 send():业务代码传一个
ProducerRecord进来,包含 topic、value,可选的 key 和 partition。 - 拦截器链处理:你可以自定义拦截器,比如做消息计数、修改消息内容。我曾在项目里用拦截器统一给消息打时间戳。
- 序列化:key 和 value 分别走各自的序列化器。默认是
StringSerializer,你也可以用 Avro、Protobuf。 - 分区选择:分区器决定这条消息去哪个分区。如果指定了 partition,直接用;否则按 key 哈希;key 为 null 则轮询。
- 写入累加器:消息被包装成
ProducerBatch,塞进对应分区双端队列的尾部。每个批次默认 16KB。 - 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 使用 |
嗯,生产者原理这块就讲到这里。记住一句话:生产者的配置,本质上是在可靠性、延迟、吞吐量三者之间做权衡。没有银弹,只有根据业务场景做取舍。