一、生产者消息发送流程
1.1 发送原理
在消息发送的过程中,涉及到了两个线程——main
线程和 Sender
线程。在 main
线程 中创建了一个双端队列 RecordAccumulator
。main 线程将消息发送给 RecordAccumulator
, Sender
线程不断从 RecordAccumulator
中拉取消息发送到 Kafka Broker
。
1.2 生产者参数列表
bootstrap.servers
: 生产者连接集群所需的broker
地址清单。例如kafka-01:9092,kafka-02:9092
,可以设置1
个或者多个,中间用逗号隔开。注意这里并非需要所有的broker
地址,因为生产者可以从给定的broker
里查找到其他broker
信息。key.serializer
: 指定发送消息的key
的序列化类型value.serializer
: 指定发送消息的value
的序列化类型buffer.memory
:RecordAccumulator
缓冲区总大小,默认32m
。batch.size
: 缓冲区一批数据最大值,默认16k
。适当增加该值,可 以提高吞吐量,但是如果该值设置太大,会导致数据 传输延迟增加。linger.ms
:如果数据迟迟未达到batch.size
,sender
等待linger.time
之后就会发送数据。单位ms
,默认值是0ms
,表示没有延迟。生产环境建议该值大小为5-100ms
之间。acks
:0
: 生产者发送过来的数据,不需要等数据落盘应答。1
: 生产者发送过来的数据,Leader 收到数据后应答。-1(all)
: 生产者发送过来的数据,Leader+
和isr
队列 里面的所有节点收齐数据后应答。默认值是-1
,-1
和all
是等价的。
max.in.flight.requests.per.connection
: 允许最多没有返回ack
的次数,默认为 5,开启幂等性 要保证该值是1-5
的数字。retries
: 当消息发送出现错误的时候,系统会重发消息。retries
表示重试次数。默认是int
最大值,2147483647
。 如果设置了重试,还想保证消息的有序性,需要设置MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION=1
否则在重试此失败消息的时候,其他的消息可能发送 成功了。retry.backoff.ms
: 两次重试之间的时间间隔,默认是100ms
。enable.idempotence
: 是否开启幂等性,默认true
,开启幂等性。compression.type
: 生产者发送的所有数据的压缩方式。默认是none
,也 就是不压缩。 支持压缩类型:none
、gzip
、snappy
、lz4
和zstd
。
二、消息发送API
2.1 普通异步发送
@Test
public void sendMessage() {
Properties properties = new Properties();
properties.setProperty(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "kafka-21:9092,kafka-22:9092");
properties.setProperty(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
properties.setProperty(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
KafkaProducer<String, String> producer = new KafkaProducer<>(properties);
String topic = "test-topic-0";
for (int i = 0; i < 10; i++) {
String message = "value-" + i;
ProducerRecord<String, String> producerRecord = new ProducerRecord<>(topic, message);
producer.send(producerRecord);
}
producer.close();
}
2.2 带回调的异步发送
@Test
public void sendMessage() {
Properties properties = new Properties();
properties.setProperty(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "kafka-21:9092,kafka-22:9092");
properties.setProperty(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
properties.setProperty(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
KafkaProducer<String, String> producer = new KafkaProducer<>(properties);
String topic = "test-topic-0";
for (int i = 0; i < 10; i++) {
String message = "value-" + i;
ProducerRecord<String, String> producerRecord = new ProducerRecord<>(topic, message);
producer.send(producerRecord, (recordMetadata, exception) -> {
if (exception != null) {
exception.printStackTrace();
} else {
log.info("[kafka producer]topic={}, partition={}", recordMetadata.topic(), recordMetadata.partition());
}
});
}
producer.close();
}
2.3 同步发送消息
@Test
public void sendMessage() throws ExecutionException, InterruptedException {
Properties properties = new Properties();
properties.setProperty(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "kafka-21:9092,kafka-22:9092");
properties.setProperty(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
properties.setProperty(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
KafkaProducer<String, String> producer = new KafkaProducer<>(properties);
String topic = "test-topic-0";
for (int i = 0; i < 10; i++) {
String message = "value-" + i;
ProducerRecord<String, String> producerRecord = new ProducerRecord<>(topic, message);
// 使用 .get() 同步发送信息
producer.send(producerRecord).get();
}
producer.close();
}
四、生产者分区
4.1 分区的好处
-
便于合理使用存储资源,每隔Partition在一个Broker上存储,可以把海量的数据按照分区切割成一块一块数据存储在多台Broker上。合理控制分区的任务,可以实现负载均衡的效果。
-
提高并行度,生产者可以以分区为单位发送数据;消费者可以以分区为单位进行消费数据。
4.2 生产者发送消息的分区策略
-
默认的分区器
DefaultPartitioner
签名
public class DefaultPartitioner implements Partitioner
-
分区详情
ProducerRecord
类中关于发送消息的方法// 直接指定分区,则将数据发送到指定分区 public ProducerRecord(String topic, Integer partition, Long timestamp, K key, V value) public ProducerRecord(String topic, Integer partition, K key, V value, Iterable<Header> headers) public ProducerRecord(String topic, Integer partition, K key, V value) // 没有指定分区,但是指定了key,则使用key的hashcode对topic的partition数取余,计算partition public ProducerRecord(String topic, K key, V value) // 没有指定partition,也没有指定key,则使用Sticky Partition(黏性分区器),会随机算则一个分区,并尽可能一直使用该分区,待该分区的batch已满或者linger.ms设置的时间到,kafka再随机选一个分区使用(和上一次的分区不同) public ProducerRecord(String topic, V value)
-
指定partition
@Test public void sendMessage() { Properties properties = new Properties(); properties.setProperty(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "kafka-21:9092,kafka-22:9092"); properties.setProperty(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); properties.setProperty(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); KafkaProducer<String, String> producer = new KafkaProducer<>(properties); String topic = "test-topic-0"; for (int i = 0; i < 10; i++) { String message = "value-" + i; // 将数据发送到指定的partition ProducerRecord<String, String> producerRecord = new ProducerRecord<>(topic, 2, "key", message); producer.send(producerRecord, (recordMetadata, exception) -> { if (exception != null) { exception.printStackTrace(); } else { log.info("[kafka producer]topic={}, partition={}", recordMetadata.topic(), recordMetadata.partition()); } }); } producer.close(); }
-
指定key
@Test public void sendMessage() { Properties properties = new Properties(); properties.setProperty(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "kafka-21:9092,kafka-22:9092"); properties.setProperty(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); properties.setProperty(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); KafkaProducer<String, String> producer = new KafkaProducer<>(properties); String topic = "test-topic-0"; for (int i = 0; i < 10; i++) { String message = "value-" + i; // 不指定partition,但是指定key,使用key的hashcode对topic的partition数取余,得到partition ProducerRecord<String, String> producerRecord = new ProducerRecord<>(topic, i + "", message); producer.send(producerRecord, (recordMetadata, exception) -> { if (exception != null) { exception.printStackTrace(); } else { log.info("[kafka producer]topic={}, partition={}", recordMetadata.topic(), recordMetadata.partition()); } }); } producer.close(); }
-
-
自定义分区器
package com.example.partition; import org.apache.kafka.clients.producer.Partitioner; import org.apache.kafka.common.Cluster; import java.util.Map; public class MyPartitioner implements Partitioner { @Override public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) { int message = Integer.parseInt(String.valueOf(value)); int partition; if (message % 2 == 0) { partition = 2; } else { partition = 1; } return partition; } @Override public void close() { } @Override public void configure(Map<String, ?> configs) { } }
使用自定义的分区器
@Test public void sendMessage() { Properties properties = new Properties(); properties.setProperty(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "kafka-21:9092,kafka-22:9092"); properties.setProperty(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); properties.setProperty(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); // 使用自定义的分区器 properties.setProperty(ProducerConfig.PARTITIONER_CLASS_CONFIG, MyPartitioner.class.getName()); KafkaProducer<String, String> producer = new KafkaProducer<>(properties); String topic = "test-topic-0"; for (int i = 0; i < 10; i++) { String message = String.valueOf(i); // 不指定partition,但是指定key,使用key的hashcode对topic的partition数取余,得到partition ProducerRecord<String, String> producerRecord = new ProducerRecord<>(topic, message); producer.send(producerRecord, (recordMetadata, exception) -> { if (exception != null) { exception.printStackTrace(); } else { log.info("[kafka producer]topic={}, partition={}", recordMetadata.topic(), recordMetadata.partition()); } }); } producer.close(); }
五、提高生产者吞吐量
batch.size
:批次大小,默认16klinger.ms
:等待时间,默认0ms,可以设置到5-100mscompression.type
:压缩数据,RecordAccumulator
:缓冲区大小,默认32m
@Test
public void sendMessage() {
Properties properties = new Properties();
properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "kafka-21:9092,kafka-22:9092");
properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
// batch.size:批次大小,默认16k
properties.put(ProducerConfig.BATCH_SIZE_CONFIG, 16384);
// linger.ms:等待时间,默认0ms
properties.put(ProducerConfig.LINGER_MS_CONFIG, 5);
// buffer.memory:缓冲区大小,默认32mb
properties.put(ProducerConfig.BUFFER_MEMORY_CONFIG, 33554432);
// compression.type: 压缩数据,默认 none,可配置的值有:gzip,snappy,lz4,zstd
properties.setProperty(ProducerConfig.COMPRESSION_TYPE_CONFIG, "snappy");
KafkaProducer<String, String> producer = new KafkaProducer<>(properties);
String topic = "test-topic-0";
for (int i = 0; i < 10; i++) {
String message = String.valueOf(i);
// 不指定partition,但是指定key,使用key的hashcode对topic的partition数取余,得到partition
ProducerRecord<String, String> producerRecord = new ProducerRecord<>(topic, i + "", message);
producer.send(producerRecord, (recordMetadata, exception) -> {
if (exception != null) {
exception.printStackTrace();
} else {
log.info("[kafka producer]topic={}, partition={}", recordMetadata.topic(), recordMetadata.partition());
}
});
}
producer.close();
}
六、数据可靠性
ack应答原理:
-
0:生产者发送数据后,不需要等数据落盘响应
-
1:生产者发送数据后,leader收到数据后响应
-
-1:生产者发送数据后,
leader
和isr
队列里面的所有节点收齐数据后响应Leader
维护了一个动态的in-sync replica set(**ISR**)
,意为和Leader
保持同步的Follower+Leader
集合(leader:0,isr:0,1,2)
。如果
Follower
长时间未向Leader
发送通信请求或同步数据,则 该Follower
将被踢出ISR
。该时间阈值由**replica.lag.time.max.ms
**参 数设定,默认30s
。例如2
超时,(leader:0, isr:0,1)
。这样就不用等长期联系不上或者已经故障的节点。
数据可靠性分析:
如果分区副本设置为
1
个,或者ISR
里应答的最小副本数量 (min.insync.replicas
默认为1
)设置为1
,和ack=1
的效果是一 样的,仍然有丢数的风险(leader:0,isr:0)
。数据完全可靠条件 = ACK级别设置为**-1** + 分区副本大于等于2 + ISR里应答的最小副本数量大于等于2
可靠性总结:
-
acks=0,生产者发送过来数据就不管了,可靠性差,效率高;
-
acks=1,生产者发送过来数据Leader应答,可靠性中等,效率中等;
-
acks=-1,生产者发送过来数据Leader和ISR队列里面所有Follwer应答,可靠性高,效率低;
在生产环境中,acks=0很少使用;acks=1,一般用于传输普通日志,允许丢个别数据;acks=-1,一般用于传输和钱相关的数据, 对可靠性要求比较高的场景。
数据重复:
acks: -1(all):生产者发送过来的数据,Leader和ISR队列里面的所有节点收齐数据后应答。
当leader和ISR队列中的所有节点都收到数据后,但是还没有响应时,leader宕机,ack失败。此时会重新选择一个leader,生产者会重新发送数据,就会导致数据重复。
-
七、数据去重
-
数据传递语义
-
至少一次(AtLeastOnce)= ACK级别设置为-1+分区副本大于等于2+ISR里应答的最小副本数量大于等于2
-
最多一次(AtMostOnce)= ACK级别设置为0
-
总结:
-
At Least Once可以保证数据不丢失,但是不能保证数据不重复;
-
At Most Once可以保证数据不重复,但是不能保证数据不丢失。
-
-
精确一次(Exactly Once): 对于一些非常重要的信息,比如和钱相关的数据,要求数据既不能重复也不丢失。 Kafka 0.11版本以后,引入了一项重大特性: 幂等性和事务。
-
-
幂等性
幂等性就是指
Producer
不论向Broker
发送多少次重复数据,Broker
端都只会持久化一条,保证了不重复。精确一次(
Exactly Once
) = 幂等性 + 至少一次(ack=-1
+ 分区副本数**>=2
+ISR
最小副本数量>=2
**)重复数据的判断标准:具有<
PID, Partition, SeqNumber
>相同主键的消息提交时,Broker
只会持久化一条。其 中PID
是Kafka
每次重启都会分配一个新的;Partition
表示分区号;Sequence Number
是单调自增的。所以幂等性只能保证的是在单分区单会话内不重复
使用幂等性:开启参数
enable.idempotence
默认为true
,false
关闭 -
kafka事务
说明:开启事务,必须开启幂等性
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3p0w29vo-1653657359826)(https://you-blog.oss-cn-shenzhen.aliyuncs.com/2022/05/20220508152303.png)]
@Test public void sendMessage() { Properties properties = new Properties(); properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "kafka-21:9092,kafka-22:9092"); properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); // 设置事务id properties.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG, "transaction_id_0"); properties.put(ProducerConfig.RETRIES_CONFIG, 3); KafkaProducer<String, String> producer = new KafkaProducer<>(properties); // 初始化事务 producer.initTransactions(); // 开始事务 producer.beginTransaction(); String topic = "test-topic-0"; try { for (int i = 0; i < 10; i++) { String message = String.valueOf(i); // 不指定partition,但是指定key,使用key的hashcode对topic的partition数取余,得到partition ProducerRecord<String, String> producerRecord = new ProducerRecord<>(topic, 1, "", message); producer.send(producerRecord, (recordMetadata, exception) -> { if (exception != null) { exception.printStackTrace(); } else { log.info("[kafka producer]topic={}, partition={}", recordMetadata.topic(), recordMetadata.partition()); } }); } // 提交事务 producer.commitTransaction(); } catch (Exception e) { // 出现异常,终止事务 producer.abortTransaction(); } finally { // 关闭资源 producer.close(); } }
八、数据有序
-
kafka在1.x版本之前保证数据单分区有序,条件如下:
max.in.flight.requests.per.connection=1(不需要考虑是否开启幂等性)
-
kafka在1.x及以后版本保证数据单分区有序,条件如下:
-
未开启幂等性
max.in.flight.requests.per.connection需要设置为1。
-
开启幂等性
max.in.flight.requests.per.connection需要设置小于等于5。
原因说明: 因为在kafka1.x以后,启用幂等后,kafka服务端会缓存producer发来的最近5个request的元数据, 故无论如何,都可以保证最近5个request的数据都是有序的。
-