《Kafka筆記》3、Kafka高階API

Inky發表於2020-10-20

1 Kafka高階API特性

1.1 Offset的自動控制

1.1.1 消費者offset初始策略

一般來說每個消費者消費之後,都會把自己消費到分割槽的位置(也就是offset提交給Kafka叢集),但是對於沒有消費過該分割槽的消費者,他之前並未提交給叢集自身偏移量的資訊。

Kafka消費者預設對於未訂閱的topic的offset的時候,也就是系統並沒有儲存該消費者的消費分割槽的記錄資訊(offset),預設Kafka消費者的預設首次消費策略:latest。

配置項為:auto.offset.reset=latest

可以在官方文件,找到對於各個配置項的解釋,例如 http://kafka.apache.org/20/documentation.html#brokerconfigs 可以找到auto.offset.reset配置項。

  • earliest - 自動將偏移量重置為最早的偏移量

  • latest - 自動將偏移量重置為最新的偏移量

  • none - 如果未找到消費者組的先前偏移量,則向消費者丟擲異常

消費者的配置中增加凸顯預設配置,latest可以換成earliest:

// 預設配置,如果系統中沒有該消費組的偏移量,該消費者組讀取最新的偏移量
props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG,"latest");

// 配置earliest,如果叢集沒有該消費者組的偏移量,系統會讀取該分割槽最早的偏移量開始消費
props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG,"earliest");

1.1.2 消費者offset自動提交策略

Kafka消費者在消費資料的時候預設會定期的提交消費的偏移量,這樣就可以保證所有的訊息至少可以被消費者消費1次,使用者可以通過以下兩個引數配置:

enable.auto.commit = true 預設

auto.commit.interval.ms = 5000 預設

如果使用者需要自己管理offset的自動提交,可以關閉offset的自動提交,手動管理offset提交的偏移量,注意使用者提交的offset偏移量永遠都要比本次消費的偏移量+1,因為提交的offset是kafka消費者下一次抓取資料的位置。

// 消費者自動提交開啟
props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,true);
// 配置offset自動提交時間間隔,10秒自動提交offset
props.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG,10000);

自定義偏移量提交策略,先關閉偏移量自定提交配置後,每次消費完,提交偏移量資訊給叢集:

public class KafkaConsumerDemo_02 {
    public static void main(String[] args) {
        //1.建立Kafka連結引數
        Properties props=new Properties();
        props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"CentOSA:9092,CentOSB:9092,CentOSC:9092");
        props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
        props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,StringDeserializer.class.getName());
        props.put(ConsumerConfig.GROUP_ID_CONFIG,"group01");
        // 關閉offset自動提交
        props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,false);

        //2.建立Topic消費者
        KafkaConsumer<String,String> consumer=new KafkaConsumer<String, String>(props);
        //3.訂閱topic開頭的訊息佇列
        consumer.subscribe(Pattern.compile("^topic.*$"));

        while (true){
            ConsumerRecords<String, String> consumerRecords = consumer.poll(Duration.ofSeconds(1));
            Iterator<ConsumerRecord<String, String>> recordIterator = consumerRecords.iterator();
            while (recordIterator.hasNext()){
                ConsumerRecord<String, String> record = recordIterator.next();
                String key = record.key();
                String value = record.value();
                long offset = record.offset();
                int partition = record.partition();
                
                // offset維護的Map
                Map<TopicPartition, OffsetAndMetadata> offsets=new HashMap<TopicPartition, OffsetAndMetadata>();

                // 自己維護offset,每次提交當前資訊的offset加1
                offsets.put(new TopicPartition(record.topic(),partition),new OffsetAndMetadata(offset + 1));
                // 非同步提交偏移量給叢集,且回撥列印
                consumer.commitAsync(offsets, new OffsetCommitCallback() {
                    @Override
                    public void onComplete(Map<TopicPartition, OffsetAndMetadata> offsets, Exception exception) {
                        System.out.println("完成:"+offset+"提交!");
                    }
                });
                System.out.println("key:"+key+",value:"+value+",partition:"+partition+",offset:"+offset);

            }
        }
    }
}


1.2 Acks & Retries(應答和重試)

Kafka生產者在傳送完一個的訊息之後,要求Leader所在的Broker在規定的時間Ack應答,如果沒有在規定時間內應答,Kafka生產者會嘗試n次重新傳送訊息(超時重傳)。目的是確保我們的訊息,一定要傳送的佇列中去

acks=1 預設

面試常問,kafka為什麼存在資料的寫入丟失?其中一種情況為下面的第一點

1、acks=1表示:Leader會將Record寫到其本地日誌中,但會在不等待所有Follower的完全確認的情況下做出響應。在這種情況下,如果Leader在確認記錄後立即失敗,但在Follower複製記錄之前失敗,則記錄將丟失,常用在不重要的日誌收集時

2、acks=0表示:生產者根本不會等待伺服器的任何確認。該記錄將立即新增到網路套接字緩衝區中並視為已傳送。在這種情況下,不能保證伺服器已收到記錄。這種情況是不可靠的,但是效能高

3、acks=all表示:這意味著Leader將等待全套同步副本確認記錄。這保證了只要至少一個同步副本仍處於活動狀態,記錄就不會丟失。這是最有力的保證。這等效於acks = -1設定。用在一些比較重要的系統,不允許丟資料

如果生產者在規定的時間內,並沒有得到Kafka的Leader的Ack應答,Kafka可以開啟reties機制。

request.timeout.ms = 30000 預設(30s沒有收到leader的ack則重試)

retries = 2147483647 預設(重試次數為Max_Value,預設一直重試)

超時重試

public class KafkaProducerDemo_01{
    public static void main(String[] args) {
        //1.建立連結引數
        Properties props=new Properties();
        props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"CentOSA:9092,CentOSB:9092,CentOSC:9092");
        props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,StringSerializer.class.getName());
        props.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG,UserDefineProducerInterceptor.class.getName());
        props.put(ProducerConfig.REQUEST_TIMEOUT_MS_CONFIG,1);
        props.put(ProducerConfig.ACKS_CONFIG,"-1");
        props.put(ProducerConfig.RETRIES_CONFIG,10);

        //2.建立生產者
        KafkaProducer<String,String> producer=new KafkaProducer<String, String>(props);

        //3.封賬訊息佇列
        for(Integer i=0;i< 1;i++){
            ProducerRecord<String, String> record = new ProducerRecord<>("topic01", "key" + i, "value" + i);
            producer.send(record);
        }

        producer.close();
    }
}

可以通過生產者自定義配置重複傳送的次數:

// 不包括第一次傳送,如果嘗試傳送三次,失敗,則系統放棄傳送
props.put(ProducerConfig.RETRIES_CONFIG, 3);

public class KafkaProducerDemo_02 {
    public static void main(String[] args) {
        //1.建立連結引數
        Properties props=new Properties();
        props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"CentOSA:9092,CentOSB:9092,CentOSC:9092");
        props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,StringSerializer.class.getName());
        props.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG,UserDefineProducerInterceptor.class.getName());
        props.put(ProducerConfig.REQUEST_TIMEOUT_MS_CONFIG,1);
        props.put(ProducerConfig.ACKS_CONFIG,"-1");
        props.put(ProducerConfig.RETRIES_CONFIG,3);
        props.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG,true);

        //2.建立生產者
        KafkaProducer<String,String> producer=new KafkaProducer<String, String>(props);

        //3.封賬訊息佇列
        for(Integer i=0;i< 1;i++){
            ProducerRecord<String, String> record = new ProducerRecord<>("topic01", "key" + i, "value" + i);
            producer.send(record);
        }

        producer.close();
    }
}

總結:應答和重試機制,可以盡最大可能保證我們把資料傳送到Kafka叢集。但也會伴隨著一些問題,比如重複資料的產生。在一些訂單業務場景中,比如使用者下訂單的記錄,是絕對不能出現重複資料的。怎麼保證?Kafka提供了冪等和事務機制,來解決重複資料的問題

1.3 Kafka冪等寫機制

1.3.1 Kafka冪等概念

1、HTTP/1.1中對冪等性的定義是:一次和多次請求某一個資源對於資源本身應該具有同樣的結果(網路超時等問題除外)。也就是說,其任意多次執行對資源本身所產生的影響均與一次執行的影響相同。

2、Kafka在0.11.0.0版本支援增加了對冪等的支援。冪等是針對生產者角度的特性。冪等可以保證生產者傳送的訊息,不會丟失(底層retries重試機制支撐),而且不會重複(冪等去重機制保證)。實現冪等的關鍵點就是服務端可以區分請求是否重複,過濾掉重複的請求。要區分請求是否重複的有兩點:

  • 唯一標識:要想區分請求是否重複,請求中就得有唯一標識。例如支付請求中,訂單號就是唯一標識
  • 記錄下已處理過的請求標識:光有唯一標識還不夠,還需要記錄下那些請求是已經處理過的,這樣當收到新的請求時,用新請求中的標識和處理記錄進行比較,如果處理記錄中有相同的標識,說明是重複記錄,拒絕掉。

1.3.2 Kafka冪等實現策略

1、冪等又稱為exactly once(精準一次)。要停止多次處理訊息,必須僅將其持久化到Kafka Topic中僅僅一次。在初始化期間,kafka會給生產者生成一個唯一的ID稱為Producer ID或PID。

2、PID和序列號與訊息捆綁在一起,然後傳送給Broker。由於序列號從零開始並且單調遞增,因此,僅當訊息的序列號比該PID / TopicPartition對中最後提交的訊息正好大1時,Broker才會接受該訊息。如果不是這種情況,則Broker認定是生產者重新傳送該訊息。

3、對應配置項:enable.idempotence= false 預設關閉,開啟設定為true

4、注意:在使用冪等性的時候,要求必須開啟retries=true和acks=all(保證不丟)

5、max.in.flight.requests.per.connection配置項預設是5,如果我們要保證嚴格有序,我們可以設定為1。該配置項表達的意思為:在發生阻塞之前,客戶端的一個連線上允許出現未確認請求的最大數量。

Tips: 精準一次的概念嚐嚐出現在流式處理中

冪等機制

程式碼配置實現:

public class KafkaProducerDemo_02 {
    public static void main(String[] args) {
        //1.建立連結引數
        Properties props=new Properties();
        props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"CentOSA:9092,CentOSB:9092,CentOSC:9092");
        props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,StringSerializer.class.getName());
        props.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG,UserDefineProducerInterceptor.class.getName());
        // 將檢測超時時間設定為1ms,方便觸發看到重試機制
        props.put(ProducerConfig.REQUEST_TIMEOUT_MS_CONFIG,1);
        // ACKS要設定為all
        props.put(ProducerConfig.ACKS_CONFIG,"all");
        // 重試3次
        props.put(ProducerConfig.RETRIES_CONFIG,3);
        // 開啟冪等
        props.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG,true);
        
        // 開啟冪等,客戶端的一個連線上允許出現未確認請求的最大數量要大於1小於5。設定為1可以保證順序。
        // 如果有一個傳送不成功,就阻塞,一直等待傳送成功為止
        props.put(ProducerConfig.MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION,1);

        //2.建立生產者
        KafkaProducer<String,String> producer=new KafkaProducer<String, String>(props);


        ProducerRecord<String, String> record = new ProducerRecord<>("topic01", "idempotence", "test idempotence");
        producer.send(record);
   
        producer.close();
    }
}

總結:開啟冪等會保證不會重複傳送訊息到訊息佇列,客戶端的一個連線上允許出現未確認請求的最大數量設定為1的話,可以保證順序不會亂。要需要使用冪等功能,kafka的版本需要保證在0.11.0.0版本以上

以上的保證順序,保證唯一,只是針對一個分割槽而言,如果kafka有多個分割槽,那麼就需要用Kafka的事務來控制原子性,事務能控制不重複,但無法控制多分割槽全域性有序

1.4 Kafka的事務控制

1、Kafka的冪等性,只能保證一條記錄的在分割槽傳送的原子性,但是如果要保證多條記錄(多分割槽不重複,但多分割槽無法有序,參照第一章)之間的完整性,這個時候就需要開啟kafk的事務操作。事務一般是把消費者和生產者繫結,中間業務系統對下游Kafka的生產失敗了,中間業務系統消費過上游的Kafka偏移量不提交

2、在Kafka0.11.0.0除了引入的冪等性的概念,同時也引入了事務的概念。通常Kafka的事務分為 生產者事務Only、消費者&生產者事務。一般來說預設消費者消費的訊息的級別是read_uncommited資料,這有可能讀取到事務失敗的資料,所有在開啟生產者事務之後,需要使用者設定消費者的事務隔離級別。

3、預設配置項為:isolation.level = read_uncommitted

4、該選項有兩個值read_committed|read_uncommitted,如果開始事務控制,消費端必須將事務的隔離級別設定為read_committed,能夠保證在回滾後清除kafka中儲存的該條傳送資訊

5、開啟的生產者事務的時候,只需要指定transactional.id屬性即可,一旦開啟了事務,預設生產者就已經開啟了冪等性。但是要求"transactional.id"的取值必須是唯一的,同一時刻只能有一個"transactional.id"儲存在,其他的將會被關閉。

1.4.1 生產者事務only使用場景

1、生產者

public class KafkaProducerDemo02 {
    public static void main(String[] args) {

        //1.生產者&消費者的配置項
        KafkaProducer<String,String> producer=buildKafkaProducer();

        producer.initTransactions();//1、初始化事務

        try{
            while(true){
           
                //2、開啟事務控制
                producer.beginTransaction();
                for(i=0; i<10; i++) {
                    if(i == 8) {
                        // 異常
                        int j = 10/0;
                    }
                    //建立Record
                    ProducerRecord<String,String> producerRecord=
                    new ProducerRecord<String,String>("topic01","transation","error......");
                    
                    producer.send(producerRecord);
                    // 事務終止前,把之前資料刷入kafka佇列
                    ptoducer.flush();
                }
                //3、提交事務
                producer.sendOffsetsToTransaction(offsets,"group01");
                producer.commitTransaction();
            }
        }catch (Exception e){
            producer.abortTransaction();//4、終止事務
        }finally {
            producer.close();
        }
    }
    
    // 生產者在生產環境的一些常規配置
    public static KafkaProducer<String,String> buildKafkaProducer(){
        Properties props=new Properties();
        props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"CentOSA:9092,CentOSB:9092,CentOSC:9092");
        props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,StringSerializer.class.getName());
        // 必須配置事務id,且唯一
        props.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG,"transaction-id" + UUID.randomUUID().toString());
        // 配置批處理大小,達到1024位元組需要提交
        props.put(ProducerConfig.BATCH_SIZE_CONFIG,1024);
        // 當沒達到1024位元組,但是時間達到了5ms,也需要提交給叢集的topic
        props.put(ProducerConfig.LINGER_MS_CONFIG,5);
        // 配置冪等,和重試
        props.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG,true);
        // ack
        props.put(ProducerConfig.ACKS_CONFIG,"all");
        // 請求超時重發時間20ms
        props.put(ProducerConfig.REQUERT_TIMEOUT_MS_CONFIG,20000);
        return new KafkaProducer<String, String>(props);
    }
    
    public static KafkaConsumer<String,String> buildKafkaConsumer(String group){
        Properties props=new Properties();
        props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"CentOSA:9092,CentOSB:9092,CentOSC:9092");
        props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
        props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,StringDeserializer.class.getName());
        props.put(ConsumerConfig.GROUP_ID_CONFIG,group);
        props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,false);
        props.put(ConsumerConfig.ISOLATION_LEVEL_CONFIG,"read_committed");

        return new KafkaConsumer<String, String>(props);
    }
}

2、消費者

public class KafkaConsumerDemo {
    public static void main(String[] args) {
        //1.建立Kafka連結引數
        Properties props=new Properties();
        props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"CentOSA:9092,CentOSB:9092,CentOSC:9092");
        props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
        props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,StringDeserializer.class.getName());
        // 消費者所屬的消費組
        props.put(ConsumerConfig.GROUP_ID_CONFIG,"group01");
        // 設定消費者消費事務的隔離級別read_committed,消費者不可能讀到未提交的資料
        props.put(ConsumerConfig.ISOLATION_LEVEL_CONFIG,"read_committed");

        //2.建立Topic消費者
        KafkaConsumer<String,String> consumer=new KafkaConsumer<String, String>(props);
        //3.訂閱topic開頭的訊息佇列
        consumer.subscribe(Pattern.compile("topic01"));

        while (true){
            ConsumerRecords<String, String> consumerRecords = consumer.poll(Duration.ofSeconds(1));
            Iterator<ConsumerRecord<String, String>> recordIterator = consumerRecords.iterator();
            while (recordIterator.hasNext()){
                ConsumerRecord<String, String> record = recordIterator.next();
                String key = record.key();
                String value = record.value();
                long offset = record.offset();
                int partition = record.partition();
                System.out.println("key:"+key+",value:"+value+",partition:"+partition+",offset:"+offset);
            }
        }
    }
}

1.4.1 生產者消費者事務

public class KafkaProducerDemo02 {
    public static void main(String[] args) {

        //1.生產者&消費者
        KafkaProducer<String,String> producer=buildKafkaProducer();
        KafkaConsumer<String, String> consumer = buildKafkaConsumer("group01");
        
        // 消費者先訂閱消費topic資料
        consumer.subscribe(Arrays.asList("topic01"));
        producer.initTransactions();//初始化事務

        try{
            while(true){
                // 消費者1秒鐘拉取一次資料
                ConsumerRecords<String, String> consumerRecords = consumer.poll(Duration.ofSeconds(1));
                // 獲取的訊息迭代
                Iterator<ConsumerRecord<String, String>> consumerRecordIterator = consumerRecords.iterator();
                //開啟事務控制
                producer.beginTransaction();
                // 維護偏移量
                Map<TopicPartition, OffsetAndMetadata> offsets=new HashMap<TopicPartition, OffsetAndMetadata>();
                // 消費者讀取到訊息後進行業務處理
                while (consumerRecordIterator.hasNext()){
                    ConsumerRecord<String, String> record = consumerRecordIterator.next();
                    //業務處理,這裡建立Record,通過消費topic01的訊息記錄,傳送到topic02
                    ProducerRecord<String,String> producerRecord=
                    new ProducerRecord<String,String>("topic02",record.key(),record.value()+"to topic02");
                    producer.send(producerRecord);
                    //記錄後設資料下次需要提交的便宜量
                    offsets.put(new TopicPartition(record.topic(),record.partition()),
                    new OffsetAndMetadata(record.offset()+1));
                }
                //提交事務,先提交消費者的偏移量,需要指定消費者組
                producer.sendOffsetsToTransaction(offsets,"group01");
                // 提交事務,再提交生產者的偏移量
                producer.commitTransaction();
            }
        }catch (Exception e){
            // 消費者端業務處理邏輯出現錯誤,要捕獲回滾
            producer.abortTransaction();//終止事務
        }finally {
            producer.close();
        }
    }
    public static KafkaProducer<String,String> buildKafkaProducer(){
        Properties props=new Properties();
        props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"CentOSA:9092,CentOSB:9092,CentOSC:9092");
        props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
        props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,StringSerializer.class.getName());
        // 必須配置事務id,且唯一
        props.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG,"transaction-id" + UUID.randomUUID().toString());
        // 配置批處理大小,達到1024位元組需要提交
        props.put(ProducerConfig.BATCH_SIZE_CONFIG,1024);
        // 當沒達到1024位元組,但是時間達到了5ms,也需要提交給叢集的topic
        props.put(ProducerConfig.LINGER_MS_CONFIG,5);
        // 配置冪等,和重試
        props.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG,true);
        // ack
        props.put(ProducerConfig.ACKS_CONFIG,"all");
        // 請求超時重發時間20ms
        props.put(ProducerConfig.REQUERT_TIMEOUT_MS_CONFIG,20000);
        return new KafkaProducer<String, String>(props);
    }
    public static KafkaConsumer<String,String> buildKafkaConsumer(String group){
        Properties props=new Properties();
        props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"CentOSA:9092,CentOSB:9092,CentOSC:9092");
        props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
        props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,StringDeserializer.class.getName());
        props.put(ConsumerConfig.GROUP_ID_CONFIG,group);
        // 消費者自動提交offset策略,關閉,必須設定
        props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,false);
        // 設定消費者消費事務的隔離級別read_committed,消費者不可能讀到未提交的資料
        props.put(ConsumerConfig.ISOLATION_LEVEL_CONFIG,"read_committed");

        return new KafkaConsumer<String, String>(props);
    }
}

總結:消費端掌握偏移量控制,生產者端掌握超時重傳應答重試和分割槽冪等。實際的生產開發過程中,要熟練掌握事務控制,包括生產者only和生產者&消費者事務控制。kafka事務在分散式微服務的開發中,有比較強的應用。

相關文章