Kafka系列4-Kafka API 一. Producer API 二. API消費者 三. 自定義攔截器 參考:

一. Producer API

消息發送流程
  Kafka 的 Producer 發送消息採用的是異步發送的方式。在消息發送的過程中,涉及到了
兩個線程——main 線程和 Sender 線程,以及一個線程共享變量——RecordAccumulator。 main 線程將消息發送給 RecordAccumulator,Sender 線程不斷從 RecordAccumulator 中拉取 消息發送到 Kafka broker。

pom文件配置
2.2.1 爲kafka的版本

<dependency>
      <groupId>org.apache.kafka</groupId>
      <artifactId>kafka-clients</artifactId>
      <version>2.2.1</version>
    </dependency>

1.2 異步發送普通生產者

代碼:
BATCH_SIZE_CONFIG = "batch.size":消息爲batch.size大小,生產者才發送消息
LINGER_MS_CONFIG = "linger.ms":如果消息大小遲遲不爲batch.size大小,則等待linger.ms時間後直接發送

package com.bigdata.study.kafka;

/**
 * @author 只是甲
 * @date   2021-10-29
 * @remark kafka生產者 - 異步 -  不帶回調函數的API
 */

import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.ExecutionException;

import org.apache.kafka.clients.producer.*;

public class CustomProducer {
    public static void main(String[] args) throws Exception {
        Properties props = new Properties();

        // 設置集羣配置
        props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "hp2:9092,hp3:9092,hp4:9092");
        // ack機制
        props.put(ProducerConfig.ACKS_CONFIG, "all");
        // 重試次數
        props.put(ProducerConfig.RETRIES_CONFIG, 1);
        // 批次大小:消息大小爲16384才發送消息
        props.put("batch.size", 16384);
        // 等待時間:如果消息大小遲遲不爲batch.size大小,則等待linger.ms時間後直接發送
        props.put(ProducerConfig.LINGER_MS_CONFIG, 1);
        // ReadAccumulator緩衝區大小
        props.put(ProducerConfig.BUFFER_MEMORY_CONFIG, 33554432);
        // 序列化
        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");

        // 構造Producer
        Producer<String,String> producer=new KafkaProducer<>(props);

        // 生產消費
        for (int i = 0; i < 100; i++){
            producer.send(new ProducerRecord<String, String>("kafka_test1", "test_" + Integer.toString(i), "test_" + Integer.toString(i)));
        }

        producer.close();

    }
}

shell中查看消費者:

/opt/cloudera/parcels/CDH-6.3.1-1.cdh6.3.1.p0.1470567/lib/kafka/bin/kafka-console-consumer.sh --from-beginning --bootstrap-server hp2:9092 --topic kafka_test1

1.2 異步發送帶回調函數的生產者

回調函數會在producer收到ack時調用,爲異步調用,該方法有兩個參數,分別爲RecordMetaData和Exception,如果Exception爲null,說明消息發送成功,如果Exception不爲null,說明消息發送失敗。

消息發送失敗會啓動重試機制,但需要在回調函數中手動重試

代碼:

package com.bigdata.study.kafka;

/**
 * @author 只是甲
 * @date   2021-10-29
 * @remark  kafka生產者 - 異步 -  帶回調函數的API
 */

import java.util.Properties;
import java.util.concurrent.ExecutionException;

import org.apache.kafka.clients.producer.*;

public class CallBackProducer {
    public static void main(String[] args) throws Exception {
        Properties props = new Properties();

        // 設置集羣配置
        props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "hp2:9092,hp3:9092,hp4:9092");
        // ack機制
        props.put(ProducerConfig.ACKS_CONFIG, "all");
        // 重試次數
        props.put(ProducerConfig.RETRIES_CONFIG, 1);
        // 批次大小:消息大小爲16384才發送消息
        props.put("batch.size", 16384);
        // 等待時間:如果消息大小遲遲不爲batch.size大小,則等待linger.ms時間後直接發送
        props.put(ProducerConfig.LINGER_MS_CONFIG, 1);
        // ReadAccumulator緩衝區大小
        props.put(ProducerConfig.BUFFER_MEMORY_CONFIG, 33554432);
        // 序列化
        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");

        // 構造Producer
        Producer<String,String> producer=new KafkaProducer<>(props);

        for (int i = 0; i < 100; i++){
            producer.send(new ProducerRecord<String, String>("kafka_test1", "test-" + Integer.toString(i),"test-" +  Integer.toString(i)), new Callback() {
                //回調函數,該方法會在 Producer 收到 ack 時調用,爲異步調用
                @Override
                public void onCompletion(RecordMetadata recordMetadata, Exception e) {
                    if ( e == null ) {
                        System.out.println("success->" + recordMetadata.offset());
                    } else {
                        e.printStackTrace();
                    }
                }
            });
        }

        producer.close();

    }
}

1.3 生產者分區策略測試

public ProducerRecord(String topic, Integer partition, Long timestamp, K key, V value, Iterable<Header> headers) {};
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) {};
public ProducerRecord(String topic, K key, V value) {};
public ProducerRecord(String topic, V value) {};

上面ProducerRecord中的partition參數即爲指定的分區(分區是有編號的,這是指定分區中的某一個,實際應該爲一個分區編號)。

這裏要注意,如果指定特定分區的話,消息是會發送到這個編號的特定分區,但是注意如果你的Topic分區只有默認的1個,而你卻要發送到分區1號,此時發送會失敗!因爲你只有1個分區,即0號分區。所以在構建的topic的時候需要注意。

默認分區構造

// 構造消息體,這裏加上具體的分區,其中的2是特定的分區編號
producer.send(new ProducerRecord<>("aaroncao",2, "test-" + i, "test-" + i), new Callback() {
    @Override
    public void onCompletion(RecordMetadata recordMetadata, Exception e) {
        if (e == null) {
            System.out.println(recordMetadata.partition() + "-" + recordMetadata.offset());
        } else {
            e.printStackTrace();
        }
    }
});

二. API消費者

2.1 簡單消費者

Kafka提供了自動提交offset的功能enable.auto.commit=true;

代碼:

package com.bigdata.study.kafka;

import org.apache.kafka.clients.consumer.*;
import org.apache.kafka.common.TopicPartition;
import org.apache.kafka.common.serialization.StringDeserializer;

import java.time.Duration;
import java.util.Collections;
import java.util.Map;
import java.util.Properties;

/**
 * @author 只是甲
 * @date   2021-10-29
 * @remark  kafka消費者
 */

public class CustomConsumer {
    public static void main(String[] args) throws Exception {
        Properties props = new Properties();

        // 設置集羣配置
        props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "hp2:9092,hp3:9092,hp4:9092");

        // 設置消費者組
        props.put(ConsumerConfig.GROUP_ID_CONFIG, "consumer_group1");
        // 設置offset的自動提交
        props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "true");
        // 設置offset自動化提交的間隔時間
        props.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, "1000");
        // 生產者是序列化,消費者則爲反序列化
        props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
        props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);

        // offset重置,需要設置自動重置爲earliest
        props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG,"earliest");

        KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);

        // 這裏需要訂閱具體的topic
        consumer.subscribe(Collections.singletonList("kafka_test1"));

        // 一直處於監聽狀態中
        while (true) {
            // 因爲消費者是通過pull獲取消息消費的,這裏設置間隔100ms
            ConsumerRecords<String, String> consumerRecords = consumer.poll(Duration.ofMillis(100));
            // 對獲取到的結果遍歷
            for (ConsumerRecord<String, String> consumerRecord : consumerRecords) {
                System.out.printf("offset=%d, key=%s, value=%s\n", consumerRecord.offset(),consumerRecord.key(),consumerRecord.value());
            }
            // 同步提交,會一直阻塞直到提交成功,這裏可以設置超時時間,如果阻塞超過超時時間則釋放
            //consumer.commitSync();

            consumer.commitAsync(new OffsetCommitCallback() {
                @Override
                public void onComplete(Map<TopicPartition, OffsetAndMetadata> offsets, Exception exception) {
                    if (exception != null) {
                        System.out.println("Commit failed, offset = " + offsets);
                    }
                }
            });

        }

    }
}

測試記錄:

2.2 消費者重置offset

Consumer消費數據時的可靠性很容易保證,因爲數據在Kafka中是持久化的,不用擔心數據丟失問題。但由於Consumer在消費過程中可能遭遇斷電或者宕機等故障,Consumer恢復之後,需要從故障前的位置繼續消費,所以Consumer需要實時記錄自己消費的offset位置,以便故障恢復後可以繼續消費。

offset的維護是Consumer消費數據必須考慮的問題。

// offset重置,需要設置自動重置爲earliest
props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG,"earliest");

將消費者組的id變換一下即可,否則由於一條消息只能夠被一個消費者組中的消費者消費一次,此時不會重新消費之前的消息,即使設置了offset重置也沒有作用。

注意
這裏的auto.offset.reset="earliest"的作用等同於在linux控制檯,消費者監聽的時候添加的--from-beginning命令。

auto.offset.reset取值

  1. earliest:重置offset到最早的位置
  2. latest:重置offset到最新的位置,默認值
  3. none:如果在消費者組中找不到前一個offset則拋出異常
  4. anything else:拋出異常給消費者

2.3 消費者保存offset讀取問題

enable.auto.commit=true即自動提交offset。默認是自動提交的。

2.4 消費者手動提交offset

自動提交offset十分便利,但是由於其實基於時間提交的,開發人員難以把握offset提交的時機,因此kafka提供了手動提交offset的API。

手動提交offset的方法主要有兩種:

  1. commitSync:同步提交
  2. commitAsync:異步提交

相同點: 兩種方式的提交都會將本次poll拉取的一批數據的最高的偏移量提交。

不同點: commitSync阻塞當前線程,持續到提交成功,失敗會自動重試(由於不可控因素導致,也會出現提交失敗);而commitAsync則沒有失敗重試機制,有可能提交失敗。

代碼:
同步提交

package com.bigdata.study.kafka;

import org.apache.kafka.clients.consumer.*;
import org.apache.kafka.common.serialization.StringDeserializer;

import java.time.Duration;
import java.util.Collections;
import java.util.Properties;

/**
 * @author 只是甲
 * @date   2021-10-29
 * @remark  kafka消費者 - 同步提交
 */

public class SyncCommitOffset {
    public static void main(String[] args) throws Exception {
        Properties props = new Properties();

        // 設置集羣配置
        props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "hp2:9092,hp3:9092,hp4:9092");

        // 設置消費者組
        props.put(ConsumerConfig.GROUP_ID_CONFIG, "consumer_group2");
        // 設置offset的自動提交
        props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "true");
        // 設置offset自動化提交的間隔時間
        props.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, "1000");
        // 生產者是序列化,消費者則爲反序列化
        props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
        props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);

        // offset重置,需要設置自動重置爲earliest
        props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG,"earliest");

        KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);

        // 這裏需要訂閱具體的topic
        consumer.subscribe(Collections.singletonList("kafka_test1"));

        // 一直處於監聽狀態中
        while (true) {
            // 因爲消費者是通過pull獲取消息消費的,這裏設置間隔100ms
            ConsumerRecords<String, String> consumerRecords = consumer.poll(Duration.ofMillis(100));
            // 對獲取到的結果遍歷
            for (ConsumerRecord<String, String> consumerRecord : consumerRecords) {
                System.out.printf("offset=%d, key=%s, value=%s\n", consumerRecord.offset(),consumerRecord.key(),consumerRecord.value());
            }

            // 同步提交,會一直阻塞直到提交成功,這裏可以設置超時時間,如果阻塞超過超時時間則釋放
            consumer.commitSync();

        }

    }
}

異步提交

consumer.commitAsync(new OffsetCommitCallback() {
                @Override
                public void onComplete(Map<TopicPartition, OffsetAndMetadata> offsets, Exception exception) {
                    if (exception != null) {
                        System.out.println("Commit failed, offset = " + offsets);
                    }
                }
            });

2.5 數據漏消費和重複消費分析

無論是同步提交還是異步提交offset,都可能會造成數據的漏消費或者重複消費,先提交offset後消費,有可能造成數據的漏消費,而先消費再提交offset,有可能會造成數據的重複消費。

2.6 自定義存儲offset

Kafka0.9版本之前,offset存儲在zookeeper中,0.9版本及之後的版本,默認將offset存儲在Kafka的一個內置的topic中,除此之外,Kafka還可以選擇自定義存儲offset數據。offse的維護相當繁瑣,因爲需要考慮到消費者的rebalance過程:

當有新的消費者加入消費者組、已有的消費者退出消費者組或者訂閱的主體分區發生了變化,會觸發分區的重新分配操作,重新分配的過程稱爲Rebalance。

消費者發生Rebalace之後,每個消費者消費的分區就會發生變化,因此消費者需要先獲取到重新分配到的分區,並且定位到每個分區最近提交的offset位置繼續消費。(High Water高水位)

代碼:

package com.bigdata.study.kafka;

import org.apache.kafka.clients.consumer.*;
import org.apache.kafka.common.TopicPartition;
import org.apache.kafka.common.serialization.StringDeserializer;

import java.time.Duration;
import java.util.*;

/**
 * @author 只是甲
 * @date   2021-10-29
 * @remark  Kafka自定義offset提交
 */

public class CustomOffsetCommit {
    private static Map<TopicPartition, Long> currentOffset = new HashMap<>();

    public static void main(String[] args) {
        Properties props = new Properties();
        props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "hp2:9092,hp3:9092,hp4:9092");
        // 設置消費者組
        props.put(ConsumerConfig.GROUP_ID_CONFIG, "consumer_group4");
        // 設置offset的自動提交爲false
        props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false");
        props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
        props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
        KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
        // 這裏的意思是訂閱的時候同時定義Consumer重分配的監聽器接口
        consumer.subscribe(Collections.singletonList("kafka_test1"), new ConsumerRebalanceListener() {
            // rebalance發生之前調用
            @Override
            public void onPartitionsRevoked(Collection<TopicPartition> partitions) {
                commitOffset(currentOffset);
            }

            // rebalance發生之後調用
            @Override
            public void onPartitionsAssigned(Collection<TopicPartition> partitions) {
                currentOffset.clear();
                for (TopicPartition partition : partitions) {
                    // 定位到最新的offset位置
                    consumer.seek(partition, getOffset(partition));
                }
            }
        });
        while (true) {
            ConsumerRecords<String, String> consumerRecords = consumer.poll(Duration.ofMillis(100));
            for (ConsumerRecord<String, String> consumerRecord : consumerRecords) {
                System.out.printf("offset=%d, key=%s, value=%s\n", consumerRecord.offset(), consumerRecord.key(), consumerRecord.value());
                // 記錄下當前的offset
                currentOffset.put(new TopicPartition(consumerRecord.topic(), consumerRecord.partition()), consumerRecord.offset());
            }
        }
    }

    // 獲取某分區最新的offset
    private static long getOffset(TopicPartition topicPartition) {
        return 0;
    }

    // 提交該消費者所有分區的offset
    private static void commitOffset(Map<TopicPartition, Long> currentOffset) {

    }
}

即自己記錄下需要提交的offset,利用Rebalance分區監聽器監聽rebalance事件,一旦發生rebalance,先將offset提交,分區之後則找到最新的offset位置繼續消費即可

三. 自定義攔截器

攔截器原理
Producer攔截器interceptor是在Kafka0.10版本引入的,主要用於Clients端的定製化控制邏輯。對於Producer而言,interceptor使得用戶在消息發送之前以及Producer回調邏輯之前有機會對消息做一些定製化需求,比如修改消息的展示樣式等,同時Producer允許用戶指定多個interceptor按序作用於同一條消息從而形成一個攔截鏈interceptor chain,Interceptor實現的接口爲ProducerInterceptor,主要有四個方法:

  1. configure(Map<String, ?> configs):獲取配置信息和初始化數據時調用

  2. onSend(ProducerRecord record):該方法封裝在KafkaProducer.send()方法中,運行在用戶主線程中,Producer確保在消息被序列化之前及計算分區前調用該方法,並且通常都是在Producer回調邏輯出發之前。

  3. onAcknowledgement(RecordMetadata metadata, Exception exception):onAcknowledgement運行在Producer的IO線程中,因此不要再該方法中放入很重的邏輯,否則會拖慢Producer的消息發送效率。

  4. close():關閉inteceptor,主要用於執行資源清理工作。

Inteceptor可能被運行到多個線程中,在具體使用時需要自行確保線程安全,另外倘若指定了多個interceptor,則producer將按照指定順序調用它們,並緊緊是捕獲每個interceptor可能拋出的異常記錄到錯誤日誌中而非向上傳遞。

自定義加入時間戳攔截器
TimeInterceptor

package com.bigdata.study.kafka;

import org.apache.kafka.clients.producer.ProducerInterceptor;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.clients.producer.RecordMetadata;

import java.util.Map;

/**
 * @author 只是甲
 * @date   2021-10-29
 * @remark  Kafka  消息攔截器
 */

public class TimeInterceptor implements ProducerInterceptor<String, String> {
    @Override
    public ProducerRecord<String, String> onSend(ProducerRecord<String, String> record) {
        return new ProducerRecord(record.topic(), record.partition(), record.timestamp(), record.key(),
                "TimeInterceptor:" + System.currentTimeMillis() + "," + record.value());
    }

    @Override
    public void onAcknowledgement(RecordMetadata recordMetadata, Exception e) {

    }

    @Override
    public void close() {

    }

    @Override
    public void configure(Map<String, ?> map) {

    }
    
}

CounterInterceptor

package com.bigdata.study.kafka;

import org.apache.kafka.clients.producer.ProducerInterceptor;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.clients.producer.RecordMetadata;

import java.util.Map;

/**
 * @author 只是甲
 * @date   2021-10-29
 * @remark  Kafka  消息攔截器
 */

public class CounterInterceptor implements ProducerInterceptor<String, String> {
    private int errorCounter = 0;
    private int successCounter = 0;

    @Override
    public ProducerRecord<String, String> onSend(ProducerRecord<String, String> record) {
        return record;
    }

    @Override
    public void onAcknowledgement(RecordMetadata metadata, Exception exception) {
        if (exception == null) {
            successCounter++;
        } else {
            errorCounter++;
        }
    }

    @Override
    public void close() {
        // 輸出結果,結束輸出
        System.out.println("Sent successful:" + successCounter);
        System.out.println("Sent failed:" + errorCounter);
    }

    @Override
    public void configure(Map<String, ?> map) {

    }
}

在CustomProducer中加入攔截器

// 加入攔截器
List<Object> interceptors = new ArrayList<>();
interceptors.add(TimeInterceptor.class);
interceptors.add(CounterInterceptor.class);
props.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG, interceptors);

測試記錄:

/opt/cloudera/parcels/CDH-6.3.1-1.cdh6.3.1.p0.1470567/lib/kafka/bin/kafka-console-consumer.sh --from-beginning --bootstrap-server hp2:9092 --topic kafka_test1

參考:

  1. https://blog.csdn.net/cao1315020626/article/details/112590786
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章