kafka学习四:kafka之Producer API

目录

  • 熟练掌握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
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值