目录
- 熟练掌握Kafka之Producer API
- 了解Producer各项重点配置
- 熟练掌握Producer负载均衡等高级特性
Producer发送模式
- 同步发送
- 异步发送
- 异步回调发送
kafka异步发送
/**
- producer异步发送演示,一般情况下使用这种发送方式
*/
public static void producerSend(){
Properties properties = new Properties();
properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"192.163.124.3:9092");
properties.put(ProducerConfig.ACKS_CONFIG,"all");
properties.put(ProducerConfig.RETRIES_CONFIG,"0");
properties.put(ProducerConfig.BATCH_SIZE_CONFIG,"16384");
properties.put(ProducerConfig.BUFFER_MEMORY_CONFIG,"33554432");
properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG,"org.apache.kafka.common.serialization.StringSerializer");
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,"org.apache.kafka.common.serialization.StringSerializer");
Producer<String, String> producer = new KafkaProducer<>(properties);
//ProducerRecord 消息对象
for (int i = 0; i < 10; i++) {
ProducerRecord<String, String> producerRecord = new ProducerRecord<>(TOPIC_NAME,"key-"+i, "value-"+i);
producer.send(producerRecord);
}
producer.close();
}
kafka同步或异步阻塞发送
/**
* producer同步发送演示 异步阻塞发送 阻塞方式很少用
* key-0partition:0,offset:14
* key-1partition:0,offset:15
* key-2partition:0,offset:16
* key-3partition:0,offset:17
* key-4partition:0,offset:18
* key-5partition:0,offset:19
* key-6partition:0,offset:20
* key-7partition:0,offset:21
* key-8partition:0,offset:22
* key-9partition:0,offset:23
*/
public static void producerSyncSend() throws Exception{
Properties properties = new Properties();
properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"192.163.124.3:9092");
properties.put(ProducerConfig.ACKS_CONFIG,"all");
properties.put(ProducerConfig.RETRIES_CONFIG,"0");
properties.put(ProducerConfig.BATCH_SIZE_CONFIG,"16384");
properties.put(ProducerConfig.BUFFER_MEMORY_CONFIG,"33554432");
properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG,"org.apache.kafka.common.serialization.StringSerializer");
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,"org.apache.kafka.common.serialization.StringSerializer");
Producer<String, String> producer = new KafkaProducer<>(properties);
//ProducerRecord 消息对象
for (int i = 0; i < 10; i++) {
String key = "key-"+i;
String value = "value-"+i;
ProducerRecord<String, String> producerRecord = new ProducerRecord<>(TOPIC_NAME,key, value);
Future<RecordMetadata> send = producer.send(producerRecord);
//由于Future对象发送之后就不管了属于异步,如果使用get方法,会阻塞在这里因此叫同步发送
RecordMetadata recordMetadata = send.get();
System.out.println(key+"partition:"+recordMetadata.partition()+",offset:"+recordMetadata.offset());
}
producer.close();
}
kafka异步回调发送
/**
- producer异步回调方法 对于消息发送的结果需要做记录等操作使用这种
*/
public static void producerSendWithCallBack() throws Exception{
Properties properties = new Properties();
properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"192.163.124.3:9092");
properties.put(ProducerConfig.ACKS_CONFIG,"all");
properties.put(ProducerConfig.RETRIES_CONFIG,"0");
properties.put(ProducerConfig.BATCH_SIZE_CONFIG,"16384");
properties.put(ProducerConfig.BUFFER_MEMORY_CONFIG,"33554432");
properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG,"org.apache.kafka.common.serialization.StringSerializer");
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,"org.apache.kafka.common.serialization.StringSerializer");
Producer<String, String> producer = new KafkaProducer<>(properties);
//ProducerRecord 消息对象
for (int i = 0; i < 10; i++) {
String key = "key-"+i;
String value = "value-"+i;
ProducerRecord<String, String> producerRecord = new ProducerRecord<>(TOPIC_NAME,key, value);
Future<RecordMetadata> send = producer.send(producerRecord, new Callback() {
@Override
public void onCompletion(RecordMetadata recordMetadata, Exception e) {
System.out.println(key+"partition:"+recordMetadata.partition()+",offset:"+recordMetadata.offset());
}
});
//由于Future对象发送之后就不管了属于异步,如果使用get方法,会阻塞在这里因此叫同步发送
RecordMetadata recordMetadata = send.get();
System.out.println(key+"partition:"+recordMetadata.partition()+",offset:"+recordMetadata.offset());
}
producer.close();
}
Producer调用流程源码分析
Producer本质上大致分为两步,一个是构建producer对象,一个是发送出去也就是send,其中KafkaProducer是构建上,send是在发送上。
1.构建上通过构造函数点进去,会生成一个clientId,主要用于构建MetricConfig监控对象,其实就是上报的指标。
2.构建MetricConfig对象。
3.加载负载均衡器,也就是this.partition = config.getConfiguredInstance(…)
4.初始化keySerializer和valueSerializer
5.最主要是初始化了一个RecordAccumulator计数器
6. 初始化一个守护线程this.sender = newSender(logContext,kafkaClient,this.metadata);其实在我们构建kafka的时候就已经开始做发送的事情了,通过5、6两步说明kafka不是接到一条发一条的,而是批量发送的。靠的就是LINGER_MS_CONFIG和BATCH_SIZE_CONFIG,多长时间发送和达到什么样的批次再发送,这也是kafka快的原因。批量发送有两个好处,减少网络io的操作,还有kafka的日志是追加形式的,在追加之前就已经做好了排序。
send
1.第一步就是int partition = partition(record,serializerdKey,serializedValue,cluster)计算分区,计算出消息具体进入哪一个partition
2.第二步就是计算批次,每次调用都进行append计算批次,每一批发送多少数据,达到一定阈值就开始发送,其实最终发送靠的是sender这个守护进程进行发送
- 构建KafkaProducer对象
- KafkaProducer对象send消息
KafkaProducer 的流程是把消息封装成 ProducerRecord 然后通过拦截链 根据指定序列化方式进行序列化 其次根据分区策略进行分区封装成 TopicPartition 形成 topic 与 partition 的映射关系 最后把消息存入RecordAccumulator这个暂存器,当 RecordAccumulator 达到一定阀值之后唤醒 sender 线程发送消息。
- 接下来来分析一下 RecordAccumulator
在 RecordAccumulator 中比较关键的字段:
appendsInProgress:它是标记往recordAccumulator 添加数据的线程的数量 因为 Producer KafkaProducer 是一个安全的类所以使用 Atomic 保证内存可见 下面会出现大量的内置锁来确定线程安全
compression:这是消息压缩的方式默认是 none 其他方式有 gzip,snappy,lz4,zstd
free:BufferPool 对象 kafka 的内存模型
batches:TopicPartition 与 ProducerBatch的映射关系 类型是 CopyOnwriteMap 是线程安全的
incomplete:未完成发送的 ProducerBatch 集合,类型是 HashSet
整体流程:
(1) 通过 TopicPartition 在 batches 里查找是否有 Deque 如果有就返回,没有就创建一个并添加进去
(2) 使用内置锁加锁(synchronized)确保线程线程,调用 tryAppend 方法尝试把消息追加到 Deque 最后一个 ProducerBatch 当中如果失败关闭这个添加操作成功则返回这个 ProducerBatch 是否已经满了
(3) 添加成功就直接返回。如果没有成功,内置锁解锁,分配一个新的 ByteBuffer
(4) 继续使用内置锁加锁 和第二步过程一样
(5) 把 TopicPartition 刚刚分配的 byteBuffer 封装成 ProducerBatch
(6) 把 batch 存入 Deque 的末尾
(7) 把 batch 存入未发送的队列里,若 sender 线程把发送后会从中移除
(8) 完成这一系列操作后 finally 块会释放ByteBuffer
因为 Deque 是非线程安全的类所以在第二步,第四步中队 Deque 进行操作的时候要使用内置锁保证线程安全,按照常理是可以放在一个内置锁当中,在第三步中它去申请分配了一个 ByteBuffer 它会阻塞。在消息过多频繁需要分配新的 ByteBuffer 的时候,此时它还持有 Deque 的锁,这样会长时间的等待。如果消息多小的时候如果前面的线程迟迟没有释放锁,也会导致长时间的等待,这样就会降低吞吐量。
唤醒 sender 线程的条件 最后一个 batch 是否满了,是否创建了新的 batch。
mark 一个知识点:producer 有两个线程,一个是 main 线程用于把消息放到 RecordAccumulator 寄存器中寄存。另一个线程是 sender线程会通过 IO 和 kafka server 进行交互发送消息
kafka Producer客户端原理解析
Producer发送原理解析
首先kafka的生产者做了三个事情,
1.直接发送,kafka的producer会将消息发送到分区leader的broker上,一般不会受到其他的干预,因为kafka是集群模式,哪怕只有一个节点,一定会有一个leader节点,所有数据都是奔着leader节点进行发送。
2.缓存leader节点的列表,如果leader节点出问题,会动态刷新
3.通过负载均衡器和计算分区起到负载均衡作用,不自定义就会使用默认的,kafka的producer发送的数据是可以控制在哪个partition上的,数据控制在哪个partition上不是kafka控制的,是由用户去控制,可以通过自定义的partition来操作。
4.异步发送,获取Future对象,可以不获取,发送就发送了,还进行了批量发送,单次io消耗很大,将单次合并批量提高吞吐率,首先在内存中积累数据,当单次发送达到一定阈值就是时间和大小,就按照批次发送到kafka上
- 直接发送
- 负载均衡
- 异步发送
Producer自定义Partition
package com.sun.kafka.producer;
import org.apache.kafka.clients.producer.Partitioner;
import org.apache.kafka.common.Cluster;
import java.util.Map;
public class SimplePartition implements Partitioner {
@Override
public int partition(String topic, Object key, byte[] bytes, Object o1, byte[] bytes1, Cluster cluster) {
/**
* key-1
* key-2
* key-3
*/
String keyStr = key + "";
System.out.println(keyStr);
keyStr = keyStr.substring(4);
System.out.println(keyStr);
return Integer.parseInt(keyStr)%2;
}
@Override
public void close() {
}
@Override
public void configure(Map<String, ?> map) {
}
}
public static void producerSendWithCallBackAndPart() throws Exception{
Properties properties = new Properties();
properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"192.163.124.3:9092");
properties.put(ProducerConfig.ACKS_CONFIG,"all");
properties.put(ProducerConfig.RETRIES_CONFIG,"0");
properties.put(ProducerConfig.BATCH_SIZE_CONFIG,"16384");
properties.put(ProducerConfig.BUFFER_MEMORY_CONFIG,"33554432");
properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG,"org.apache.kafka.common.serialization.StringSerializer");
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,"org.apache.kafka.common.serialization.StringSerializer");
properties.put(ProducerConfig.PARTITIONER_CLASS_CONFIG,"com.sun.kafka.producer.SimplePartition");
Producer<String, String> producer = new KafkaProducer<>(properties);
//ProducerRecord 消息对象
for (int i = 0; i < 10; i++) {
String key = "key-"+i;
String value = "value-"+i;
ProducerRecord<String, String> producerRecord = new ProducerRecord<>(TOPIC_NAME,key, value);
Future<RecordMetadata> send = producer.send(producerRecord, new Callback() {
@Override
public void onCompletion(RecordMetadata recordMetadata, Exception e) {
System.out.println(key+"partition:"+recordMetadata.partition()+",offset:"+recordMetadata.offset());
}
});
//由于Future对象发送之后就不管了属于异步,如果使用get方法,会阻塞在这里因此叫同步发送
RecordMetadata recordMetadata = send.get();
System.out.println(key+"partition:"+recordMetadata.partition()+",offset:"+recordMetadata.offset());
}
producer.close();
}
Producer消息传递保障
- kafka提供了三个传递保障
- 最多一次:收到0到1次,把acks参数设置为0,意思就是我的KafkaProducer在客户端,只要把消息发送出去,不管那条数据有没有在哪怕Partition Leader上落到磁盘,我就不管他了,直接就认为这个消息发送成功了。如果你采用这种设置的话,那么你必须注意的一点是,可能你发送出去的消息还在半路。结果呢,Partition Leader所在Broker就直接挂了,然后结果你的客户端还认为消息发送成功了,此时就会导致这条消息就丢失了。
- 至少一次:收到1到多次,把acks参数设置为1,只要Partition Leader接收到消息而且写入本地磁盘了,就认为成功了,不管他其他的Follower有没有同步过去这条消息了。这种设置其实是kafka默认的设置,默认情况下,你要是不管acks这个参数,只要Partition Leader写成功就算成功。但是这里有一个问题,万一Partition Leader刚刚接收到消息,Follower还没来得及同步过去,结果Leader所在的broker宕机了,此时也会导致这条消息丢失,因为人家客户端已经认为发送成功了。
- 正好一次:有且仅有一次,就是设置acks=all,这个意思就是说,Partition Leader接收到消息之后,还必须要求ISR列表里跟Leader保持同步的那些Follower都要把消息同步过去,才能认为这条消息是写入成功了。如果说Partition Leader刚接收到了消息,但是结果Follower没有收到消息,此时Leader宕机了,那么客户端会感知到这个消息没发送成功,他会重试再次发送消息过去。此时可能Partition 2的Follower变成Leader了,此时ISR列表里只有最新的这个Follower转变成的Leader了,那么只要这个新的Leader接收消息就算成功了。acks=all 就可以代表数据一定不会丢失了吗?当然不是,如果你的Partition只有一个副本,也就是一个Leader,任何Follower都没有,你认为acks=all有用吗?当然没用了,因为ISR里就一个Leader,他接收完消息后宕机,也会导致数据丢失。所以说,这个acks=all,必须跟ISR列表里至少有2个以上的副本配合使用,起码是有一个Leader和一个Follower才可以。这样才能保证说写一条数据过去,一定是2个以上的副本都收到了才算是成功,此时任何一个副本宕机,不会导致数据丢失。
- 传递保障依赖于Producer和Consumer共同实现
- 传递保障主要依赖于Producer