kafka之Producer

Kafka版本

  1. kafka版本1.1.1,可能絕大部分也適用於kafka 0.10.x及以上版本。

Java API

  1. 發送的一般步驟

    • 構造配置信息,即java.util.Properties對象
    • 使用Properties對象構造KafkaProducer實例
    • 構造待發送的ProducerRecord消息對象,指定key、value、topic
    • 調用KafkaProducer的send方法發送消息
    • 關閉KafkaProducer
  2. producer發送消息底層完全是異步發送,通過Future同時提供了同步發送和異步回調發送

    • 同步:通過future.get()無限等待結果返回,實現同步發送的效果
    • 異步:通過org.apache.kafka.clients.producer.Callback接口處理消息發送後的邏輯。此接口比較粗糙,只有一個onCompletion方法,其實如果提供一個onSuccess和一個OnFailed方法就好了。onCompletion方法的兩個參數RecordMetadata和Exception不會同時非空,即至少只有一個是null。消息發送成功時Exception爲null,消息發送失敗時,RecordMetadata時null。
  3. kafka的錯誤類型主要包含兩類,可重試異常和不可重試異常

  4. 可重試異常,對於可重試異常,如果在producer中配置了重試次數,只要在規定的重試次數內自定恢復了,便不會出現在onCompletion方法的exception中。如果超過了重試次數仍沒有成功,則仍然會進exception中,此時需要程序自行處理

    • LeaderNotAvailableException: 通常出現在 leader換屆選舉期間,表示分區的leader副本不可用。一般是瞬時異常,重試之後可以自行恢復

    • NotControllerException:表示Controller在經歷新一輪的選舉,controller 當前不可用。一般可以通過重試機制自行恢復

    • NetworkException:網絡瞬時故障導致的異常,可重試。

    • 所有可重試異常都繼承org.apache.kafka.common.errors.RetriableException.所以未即成此異常類的異常都屬於不可重試異常,即無法處理的問題,比如發送消息大小過大,序列化異常等

      image-20190930000052064

同步發送

  1. 發送實例代碼

    @Test
    public void testSync() {
        Properties props = new Properties();
        props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "kafka-master:9092,kafka-slave1:9093,kafka-slave2:9094");
        props.put(ProducerConfig.ACKS_CONFIG, "all");
        props.put(ProducerConfig.COMPRESSION_TYPE_CONFIG, "lz4");
        props.put(ProducerConfig.RETRIES_CONFIG, "10");
        props.put(ProducerConfig.RETRY_BACKOFF_MS_CONFIG, "1000");
        props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");
        props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");
    
        KafkaProducer<Integer, String> producer = new KafkaProducer<>(props);
        //同一個key的消息放到同一個分區,不指定key則均衡分佈,消息分區的選擇是在客戶端進行的
        String key = "test";
        String topic = "testTopic";
        for (int i = 0; i < 100; i++) {
            try {
                String messageStr = "hello world " + i;
                ProducerRecord producerRecord = new ProducerRecord(topic, key, messageStr);
                Future<RecordMetadata> future = producer.send(producerRecord);
                List<PartitionInfo> partitionInfos = producer.partitionsFor(topic);
                for (PartitionInfo partitionInfo : partitionInfos) {
                    logger.info(partitionInfo.toString());
                }
                //同步調用
                RecordMetadata recordMetadata = future.get();
                logger.info(ToStringBuilder.reflectionToString(recordMetadata));
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                logger.error(e.getMessage(), e);
            } catch (ExecutionException e) {
                logger.error(e.getMessage(), e);
            }
        }
        producer.close();
    }
    
  2. ProducerRecord

    public class ProducerRecord<K, V> {
    		//消息主題
        private final String topic;
        //消息分區
        private final Integer partition;
        //headers 字段是消息的頭部,Kafka 0.11.x 版本才引入的,它大多用來設定一些與應用相關的信息
        private final Headers headers;
        //消息Key
        private final K key;
        //消息體
        private final V value;
        //消息的時間戳,
        private final Long timestamp;
    		... 省略 ...
    }
    

異步發送

  1. 異步發送實例。對於同一個分區來說,如果消息1在消息2之前發送,那麼KafkaProducer可以保證對應的callback1在callback2之前調用,即回調函數的調用可以保證分區有序

    @Test
    public void testASync() {
        Properties props = new Properties();
        props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "kafka-master:9092,kafka-slave1:9093,kafka-slave2:9094");
        props.put(ProducerConfig.ACKS_CONFIG, "all");
        props.put(ProducerConfig.COMPRESSION_TYPE_CONFIG, "lz4");
        props.put(ProducerConfig.RETRIES_CONFIG, "10");
        props.put(ProducerConfig.RETRY_BACKOFF_MS_CONFIG, "1000");
        props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");
        props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");
    
        KafkaProducer<Integer, String> producer = new KafkaProducer<>(props);
        String key = "testAsync";
        String topic = "testTopic";
        CountDownLatch countDownLatch = new CountDownLatch(100);
        for (int i = 100; i < 200; i++) {
            String messageStr = "hello world " + i;
            ProducerRecord producerRecord = new ProducerRecord(topic, key, messageStr);
            producer.send(producerRecord, new Callback() {
                @Override
                public void onCompletion(RecordMetadata recordMetadata, Exception e) {
                    //exception與recordMetadata不會同時非空,即至少有一個爲null
                    if (e != null) {
                        if (e instanceof RetriableException) {
                            //處理可重試瞬時異常
                        } else {
                            //處理不可重試瞬時異常
                            logger.error(e.getMessage(), e);
                        }
                    }
                    //消息發送成功
                    if (recordMetadata != null) {
                        logger.info(ToStringBuilder.reflectionToString(recordMetadata));
                    }
                    countDownLatch.countDown();
                }
            });
        }
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        producer.close();
    }
    

分區策略

  1. ProducerRecord中的key有兩個用途

    • 作爲消息的附加信息
    • 決定消息該寫到Topic(主題)的哪個Partition(分區),默認分區策略將擁有相同key的消息寫到同一個Partition(分區)
  2. 如果key爲null,並且使用了默認分區器,則消息將被隨機發送到Topic(主題)內各個**可用的Partition(分區)**上,默認分區器使用輪訓算法(Round Robin)將消息均衡地分佈到各個Partition(分區)上。

  3. 如果key不爲null,並且使用了默認分區器,kafka客戶端會對key進行散列(使用kafka自己的算法,java版本的升級不影響散列值),根據散列值把消息映射到特定的Partition(分區)上。在進行映射時,會使用Topic(主題)所有的Partition(分區)(不僅僅是可用Partition(分區)),如果寫入數據的Partition(分區)是不可用的就會發生錯誤。

  4. 只有在不改變Topic(主題)Partition(分區)數量的情況下,keyPartition(分區)之間的映射才能保持不變。如果要使用key來映射分區,最好在創建Topic(主題)的時候就把Partition(分區)規劃好,並且永遠不要增加新的Partition(分區)

  5. 默認的分區策略實現類org.apache.kafka.clients.producer.internals.DefaultPartitioner

    public class DefaultPartitioner implements Partitioner {
    
        private final ConcurrentMap<String, AtomicInteger> topicCounterMap = new ConcurrentHashMap<>();
    
        public void configure(Map<String, ?> configs) {}
    
    
        public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
            List<PartitionInfo> partitions = cluster.partitionsForTopic(topic);
            int numPartitions = partitions.size();
            if (keyBytes == null) {
                int nextValue = nextValue(topic);
                List<PartitionInfo> availablePartitions = cluster.availablePartitionsForTopic(topic);
                if (availablePartitions.size() > 0) {
                    int part = Utils.toPositive(nextValue) % availablePartitions.size();
                    return availablePartitions.get(part).partition();
                } else {
                    // no partitions are available, give a non-available partition
                    return Utils.toPositive(nextValue) % numPartitions;
                }
            } else {
                // hash the keyBytes to choose a partition
                return Utils.toPositive(Utils.murmur2(keyBytes)) % numPartitions;
            }
        }
    
        private int nextValue(String topic) {
            AtomicInteger counter = topicCounterMap.get(topic);
            if (null == counter) {
                counter = new AtomicInteger(ThreadLocalRandom.current().nextInt());
                AtomicInteger currentCounter = topicCounterMap.putIfAbsent(topic, counter);
                if (currentCounter != null) {
                    counter = currentCounter;
                }
            }
            return counter.getAndIncrement();
        }
    
        public void close() {}
    
    }
    
  6. 自定義分區策略

    • 實現org.apache.kafka.clients.producer.Partitioner接口
    • Properties對象中設置 partitioner.class參數
  7. 自定義分區策略

    public class SmsPartition implements Partitioner {
    
        /**
         * @param topic topic名稱
         * @param key   消息key或者null
         * @param keyBytes 消息鍵值序列化字節數組或 null
         * @param value 消息體或 null
         * @param valueBytes 消息體序列化字節數組或 null
         * @param cluster  集羣元數據
         */
        @Override
        public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
    
            List<PartitionInfo> partitionInfos = cluster.partitionsForTopic(topic);
    
            //這裏定義key的類型是字符串,序列化也是字符串
            if (keyBytes == null || !(key instanceof String)) {
                throw new IllegalArgumentException("key不能爲空,且必須是字符串類型");
            }
            int size = partitionInfos.size();
    
            if (size <= 1) {
                return size;
            } else {
                String keyString = (String) key;
                //key值爲sms的消息分配最後一個分區
                if ("sms".equals(keyString)) {
                    return size - 1;
                }
                return Math.abs(Utils.murmur2(keyBytes) % (size - 1));
            }
    
        }
    
        @Override
        public void close() {
            //關閉分區,主要爲了關閉那些創建分區時初始化的系統資源等
        }
    
        @Override
        public void configure(Map<String, ?> configs) {
    
        }
    }
    
  8. 自定義分區案例

    
     /**
      * 1. 創建topic
      * bin/kafka-topics.sh --create --zookeeper zookeeper:2181 --replication-factor 3 --partitions 5 --topic testTopic
      * 2. 自定義分區
      * 3. 運行後分區的消息數
      * bin/kafka-run-class.sh kafka.tools.GetOffsetShell --broker-list kafka-master:9092 -topic testTopic --time -1
      * testTopic:2:0
      * testTopic:4:100
      * testTopic:1:0
      * testTopic:3:0
      * testTopic:0:200
      */
     @Test
     public void testPartition() {
         Properties props = new Properties();
         props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "kafka-master:9092,kafka-slave1:9093,kafka-slave2:9094");
         props.put(ProducerConfig.ACKS_CONFIG, "all");
         props.put(ProducerConfig.COMPRESSION_TYPE_CONFIG, "lz4");
         props.put(ProducerConfig.RETRIES_CONFIG, "10");
         props.put(ProducerConfig.RETRY_BACKOFF_MS_CONFIG, "1000");
         props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");
         props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");
    
         props.put(ProducerConfig.PARTITIONER_CLASS_CONFIG, "cn.jannal.kafka.partition.SmsPartition");
         KafkaProducer<Integer, String> producer = new KafkaProducer<>(props);
         String key = "sms";
         String topic = "testTopic";
         for (int i = 200; i < 300; i++) {
             try {
                 String messageStr = "hello world " + i;
                 ProducerRecord producerRecord = new ProducerRecord(topic, key, messageStr);
                 Future<RecordMetadata> future = producer.send(producerRecord);
                 List<PartitionInfo> partitionInfos = producer.partitionsFor(topic);
                 for (PartitionInfo partitionInfo : partitionInfos) {
                     logger.info(partitionInfo.toString());
                 }
                 //同步調用
                 RecordMetadata recordMetadata = future.get();
                 logger.info(ToStringBuilder.reflectionToString(recordMetadata));
             } catch (InterruptedException e) {
                 Thread.currentThread().interrupt();
                 logger.error(e.getMessage(), e);
             } catch (ExecutionException e) {
                 Thread.currentThread().interrupt();
                 logger.error(e.getMessage(), e);
             }
         }
         producer.close();
    
     }
    

攔截器

  1. interceptor主要用於實現 clients 端的定製化控制邏輯,對於 producer 而言, interceptor 使得用戶在消息發送前以及 producer 回調邏輯前對消息做一些定製化需求,比如修改消息、統計等,多個interceptor形成一個攔截器鏈

  2. 自定義攔截器需要實現org.apache.kafka.clients.producer.ProducerInterceptor接口

  3. interceptor 可能運行在多個線程中,因此在具體實現時需要自行確保線程安全。另外,若指定了多個 interceptor,則 producer 將按照指定順序調用它們,同時把每個interceptor 中捕獲的異常記錄到錯誤日誌中而不是向上傳遞

    public class CounterProducerlnterceptor implements ProducerInterceptor {
        private static AtomicInteger sendCounter = new AtomicInteger(0);
        private static AtomicInteger successCounter = new AtomicInteger(0);
        private static AtomicInteger failedCounter = new AtomicInteger(0);
    
        /**
         * 1. 消息被序列化以計算分區前調用該方法,該方法中可以對消息做任何操作,
         * 但最好保證不要修改消息所屬的topic和分區
         * 2. 該方法運行在發送主線程中
         */
        @Override
        public ProducerRecord onSend(ProducerRecord record) {
            sendCounter.incrementAndGet();
            return record;
        }
    
        /**
         * 1. 在消息被應答之前或消息發送失敗時調用
         * 2. 該方法在producer的I/O線程中,儘量不要放入耗時的業務邏輯
         */
        @Override
        public void onAcknowledgement(RecordMetadata metadata, Exception exception) {
            if (exception == null) {
                successCounter.incrementAndGet();
            } else {
                failedCounter.incrementAndGet();
            }
        }
    
        /**
         * 關閉interceptor執行一些資源清理工作
         * 關閉producer時,會調用此方法
         */
        @Override
        public void close() {
            System.out.println("發送個數" + sendCounter.get());
            System.out.println("成功個數" + successCounter.get());
            System.out.println("失敗個數" + failedCounter.get());
        }
    
        @Override
        public void configure(Map<String, ?> configs) {
    
        }
    }
    
  4. 實際案例

    @Test
    public void testProducerInterceptor() {
        Properties props = new Properties();
        props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "kafka-master:9092,kafka-slave1:9093,kafka-slave2:9094");
        props.put(ProducerConfig.ACKS_CONFIG, "all");
        props.put(ProducerConfig.COMPRESSION_TYPE_CONFIG, "lz4");
        props.put(ProducerConfig.RETRIES_CONFIG, "10");
        props.put(ProducerConfig.RETRY_BACKOFF_MS_CONFIG, "1000");
        props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");
        props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");
    
        //構建攔截器鏈
        List<String> interceptors = new ArrayList<>();
        interceptors.add("cn.jannal.kafka.interceptor.CounterProducerlnterceptor");
        props.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG, interceptors);
    
        KafkaProducer<Integer, String> producer = new KafkaProducer<>(props);
        String topic = "testTopic";
        for (int i = 0; i < 100; i++) {
            try {
                String messageStr = "hello world " + i;
                ProducerRecord producerRecord = new ProducerRecord(topic, null, messageStr);
                Future<RecordMetadata> future = producer.send(producerRecord);
                List<PartitionInfo> partitionInfos = producer.partitionsFor(topic);
                for (PartitionInfo partitionInfo : partitionInfos) {
                    logger.info(partitionInfo.toString());
                }
                //同步調用
                RecordMetadata recordMetadata = future.get();
                logger.info(ToStringBuilder.reflectionToString(recordMetadata));
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                logger.error(e.getMessage(), e);
            } catch (ExecutionException e) {
                logger.error(e.getMessage(), e);
            }
        }
        //關閉producer時,纔會調用interceptor的close方法
        producer.close();
    }
    

多線程多進程發送

  1. 實際環境中爲了達到最大的吞吐量,需要多線程或者多進程同時給kafka集羣發送消息
  • 多線程單KafkaProducer 實例:因爲KafkaProducer是線程安全的,所以需要構造一個全局的KafkaProducer實例,在多線程中共享使用。所有線程共享一個內存緩衝區(需要調大),對於分區數量比較少的集羣環境,可以使用這種方式
  • 多線程多 KafkaProducer實例:每個發送線程構造一個KafkaProducer實例,每個發送線程共享自己的KafkaProducer實例、緩衝區以及參數配置,不同線程之間的KafkaProducer實例相互獨立,互不影響。對於分區數量很多的集羣環境,可以使用這種方式,方便管理。

防止producer消息丟失

  1. producer丟失消息的場景:若 I/O線程發送之前 producer崩潰,則存儲緩衝區中的消息全部丟失
  2. producer需要的配置
    • max.block.ms=60000ms:默認60000ms
    • acks=all或者-1
    • retries:Integer.MAX_VALUE:無限重試可恢復的異常
    • max.in.flight.requests.per.connection = 1:防止消息亂序,但是可能會降低吞吐量
  3. broker端配置
    • unclean.leader.election.enable =false:不允許非ISR中的副本被選舉爲leader
    • replication.factor=3:多副本備份
    • min.insync.replicas=2:控制某條消息至少被寫入到 ISR 中的多少個副本纔算成功。producer設置acks=-1此參數纔有意義
    • replication.factor >min.insync.replicas:如果設置相等,只要有一個副本掛掉,分區就無法正常工作。一般配置replication.factor = min.insync.replicas+1
  4. 防止producer宕機,而導致消息丟失
    • 在發送消息前,先將消息寫入redis,然後再發送,發送成功後刪除redis中的消息。
    • 單獨啓動一個job,對於長時間沒有被刪除的key做重試處理(有可能會產生重複消息,consumer來做冪等處理)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章