Producer 生产者
Kafka 生产者是负责将消息发送到 Kafka 集群的组件。生产者可以是各种应用程序,如 Web 服务器、日志收集器等,它们将数据以消息的形式发送到 Kafka 主题。
生产者消息发送流程
- 消息创建:生产者应用程序创建要发送的消息,消息包含键(Key)、值(Value)和可选的时间戳等信息。
- 序列化:将消息的键和值转换为字节数组,以便在网络上传输。Kafka 提供了多种序列化器,如 StringSerializer、IntegerSerializer 等,也可以自定义序列化器。
- 分区选择:根据消息的键或其他规则,确定消息要发送到的主题分区。
- 消息累加器:将消息暂时存储在消息累加器(RecordAccumulator)中,它会对消息进行批量处理,提高发送效率。
- 发送请求:当消息达到一定数量或达到一定时间间隔时,生产者将消息从消息累加器中取出,封装成请求(ProduceRequest)发送到 Kafka 集群的 Broker 节点。
- 接收响应:生产者等待 Broker 节点的响应,确认消息是否成功发送。
发送原理
Kafka 生产者采用异步发送的方式,通过消息累加器和 Sender 线程实现高效的消息发送。消息累加器负责缓存消息,Sender 线程负责将缓存中的消息发送到 Kafka 集群。这种设计使得生产者可以在后台异步处理消息发送,提高了发送效率。
异步发送 API
普通异步发送
import org.apache.kafka.clients.producer.*;
import java.util.Properties;
public class AsyncProducer {
public static void main(String[] args) {
// 配置生产者属性
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
// 创建生产者实例
Producer<String, String> producer = new KafkaProducer<>(props);
// 创建消息
ProducerRecord<String, String> record = new ProducerRecord<>("test_topic", "key", "value");
// 异步发送消息
producer.send(record);
// 关闭生产者
producer.close();
}
}
带回调函数的异步发送
import org.apache.kafka.clients.producer.*;
import java.util.Properties;
public class AsyncProducerWithCallback {
public static void main(String[] args) {
// 配置生产者属性
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
// 创建生产者实例
Producer<String, String> producer = new KafkaProducer<>(props);
// 创建消息
ProducerRecord<String, String> record = new ProducerRecord<>("test_topic", "key", "value");
// 异步发送消息并添加回调函数
producer.send(record, new Callback() {
@Override
public void onCompletion(RecordMetadata metadata, Exception exception) {
if (exception != null) {
System.err.println("消息发送失败: " + exception.getMessage());
} else {
System.out.println("消息发送成功,分区: " + metadata.partition() + ", 偏移量: " + metadata.offset());
}
}
});
// 关闭生产者
producer.close();
}
}
同步发送 API
import org.apache.kafka.clients.producer.*;
import java.util.Properties;
public class SyncProducer {
public static void main(String[] args) {
// 配置生产者属性
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
// 创建生产者实例
Producer<String, String> producer = new KafkaProducer<>(props);
// 创建消息
ProducerRecord<String, String> record = new ProducerRecord<>("test_topic", "key", "value");
try {
// 同步发送消息
RecordMetadata metadata = producer.send(record).get();
System.out.println("消息发送成功,分区: " + metadata.partition() + ", 偏移量: " + metadata.offset());
} catch (Exception e) {
System.err.println("消息发送失败: " + e.getMessage());
}
// 关闭生产者
producer.close();
}
}
生产者分区
分区好处
- 提高并发性能:Kafka 主题可以划分为多个分区,每个分区可以在不同的 Broker 节点上存储和处理,生产者可以并行地向多个分区发送消息,消费者也可以并行地从多个分区消费消息,从而提高系统的并发处理能力。
- 实现数据负载均衡:通过合理的分区策略,可以将消息均匀地分布到不同的分区中,避免某些 Broker 节点负载过高,实现数据的负载均衡。
- 支持数据的顺序性:在同一个分区内,消息是按照写入顺序存储的,可以保证分区内消息的顺序性。
默认分区规则
- 如果消息的键为 null:Kafka 会使用轮询(Round Robin)的方式将消息均匀地分配到各个分区中。
- 如果消息的键不为 null:Kafka 会对键进行哈希计算,然后根据哈希值将消息分配到相应的分区中,保证具有相同键的消息总是发送到同一个分区。
自定义分区
import org.apache.kafka.clients.producer.Partitioner;
import org.apache.kafka.common.Cluster;
import java.util.Map;
public class CustomPartitioner implements Partitioner {
@Override
public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
// 获取主题的分区数量
int numPartitions = cluster.partitionsForTopic(topic).size();
// 自定义分区逻辑,这里简单地将键转换为字符串并取哈希值对分区数量取模
if (key == null) {
return 0;
} else {
return Math.abs(key.toString().hashCode()) % numPartitions;
}
}
@Override
public void close() {
// 关闭分区器时的清理操作
}
@Override
public void configure(Map<String, ?> configs) {
// 配置分区器时的初始化操作
}
}
在生产者配置中指定自定义分区器:
props.put("partitioner.class", "com.example.CustomPartitioner");
生产者如何提高吞吐量
- 增加批次大小:通过调整
batch.size
参数,增加每个批次发送的消息数量,减少网络请求次数,提高发送效率。 - 增加缓冲区大小:增大
buffer.memory
参数,为消息累加器分配更多的内存,允许缓存更多的消息,提高消息的批量处理能力。 - 使用压缩算法:通过设置
compression.type
参数,如gzip
、snappy
或lz4
等,对消息进行压缩,减少网络传输的数据量,提高吞吐量。 - 异步发送:使用异步发送方式,让生产者在后台异步处理消息发送,避免同步发送时的阻塞等待。
生产者提高数据可靠性
ack 应答原理
Kafka 生产者通过 acks
参数来控制消息发送的可靠性,acks
参数有以下几种取值:
- acks = 0:生产者发送消息后,不需要等待 Broker 的确认,立即发送下一条消息。这种方式发送速度最快,但可能会丢失消息,因为如果消息在发送过程中出现问题,生产者不会得到通知。
- acks = 1:生产者发送消息后,只要 Leader 分区成功写入消息,就会收到 Broker 的确认响应。这种方式可以保证消息在 Leader 分区不丢失,但如果 Leader 分区在发送确认响应后发生故障,而消息还未同步到 Follower 分区,消息可能会丢失。
- acks = -1 或 all:生产者发送消息后,需要等待 Leader 分区和所有的 Follower 分区都成功写入消息,才会收到 Broker 的确认响应。这种方式可以保证消息不会丢失,但发送速度最慢,因为需要等待所有副本都写入成功。
可靠性分析
- acks = 0:可靠性最低,可能会丢失大量消息,但发送速度最快,适用于对数据可靠性要求不高的场景,如日志收集。
- acks = 1:可靠性适中,在大多数情况下可以保证消息不丢失,但在某些极端情况下可能会出现消息丢失,适用于对数据可靠性有一定要求的场景。
- acks = -1 或 all:可靠性最高,几乎可以保证消息不会丢失,但发送速度最慢,适用于对数据可靠性要求极高的场景,如金融交易。
数据重复分析
当 acks = -1 或 all
时,如果生产者在发送消息后没有收到 Broker 的确认响应,可能会重试发送消息,导致消息重复。另外,在 Broker 节点故障恢复过程中,也可能会出现消息重复的情况。
数据去重
- 业务层去重:在消费者端,通过业务逻辑对消息进行去重,如使用唯一标识(如消息 ID)来判断消息是否已经处理过。
- Kafka 幂等性:Kafka 0.11.0.0 版本引入了幂等性特性,通过设置
enable.idempotence = true
,Kafka 会自动为每个生产者分配一个唯一的 ID,并为每条消息分配一个唯一的序列号,Broker 会根据序列号来判断消息是否重复,从而实现消息的去重。
数据传递语义
- 最多一次(At Most Once):消息可能会丢失,但不会重复发送,对应
acks = 0
的情况。 - 至少一次(At Least Once):消息不会丢失,但可能会重复发送,对应
acks = 1
或acks = -1 或 all
的情况。 - 精确一次(Exactly Once):消息只会被处理一次,不会丢失也不会重复,通过 Kafka 的幂等性和事务特性可以实现精确一次的消息传递语义。
幂等性
Kafka 的幂等性是指生产者在重试发送消息时,Broker 能够保证相同的消息只会被处理一次。通过设置 enable.idempotence = true
,Kafka 会自动为每个生产者分配一个唯一的 ID(PID),并为每条消息分配一个唯一的序列号,Broker 会根据序列号来判断消息是否重复,从而避免消息的重复处理。
生产者事务
Kafka 0.11.0.0 版本引入了事务特性,允许生产者在一个事务中发送多条消息,保证这些消息要么全部成功发送,要么全部失败。通过以下步骤可以实现生产者事务:
import org.apache.kafka.clients.producer.*;
import java.util.Properties;
public class TransactionalProducer {
public static void main(String[] args) {
// 配置生产者属性
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("enable.idempotence", "true");
props.put("transactional.id", "my-transactional-id");
// 创建生产者实例
Producer<String, String> producer = new KafkaProducer<>(props);
// 初始化事务
producer.initTransactions();
try {
// 开始事务
producer.beginTransaction();
// 发送消息
ProducerRecord<String, String> record1 = new ProducerRecord<>("test_topic", "key1", "value1");
ProducerRecord<String, String> record2 = new ProducerRecord<>("test_topic", "key2", "value2");
producer.send(record1);
producer.send(record2);
// 提交事务
producer.commitTransaction();
} catch (ProducerFencedException | OutOfOrderSequenceException | AuthorizationException e) {
// 生产者被隔离或权限不足,需要关闭生产者
producer.close();
} catch (KafkaException e) {
// 事务失败,回滚事务
producer.abortTransaction();
}
// 关闭生产者
producer.close();
}
}
数据有序
数据乱序
在异步发送消息时,如果生产者在发送消息时使用了多个线程或消息在网络传输过程中出现延迟,可能会导致消息到达 Broker 的顺序与发送顺序不一致,从而出现数据乱序的情况。
生产者核心参数配置
- bootstrap.servers:指定 Kafka 集群的地址,用于生产者连接 Kafka 集群。
- key.serializer:指定消息键的序列化器,将键转换为字节数组。
- value.serializer:指定消息值的序列化器,将值转换为字节数组。
- acks:控制消息发送的可靠性,取值为 0、1、-1 或 all。
- batch.size:指定每个批次发送的消息大小,单位为字节。
- linger.ms:指定生产者在发送批次之前等待更多消息的时间,单位为毫秒。
- buffer.memory:指定消息累加器的缓冲区大小,单位为字节。
- compression.type:指定消息的压缩算法,如
gzip
、snappy
或lz4
等。 - enable.idempotence:是否开启幂等性特性,取值为
true
或false
。 - transactional.id:指定生产者的事务 ID,用于实现生产者事务。