kafka實戰篇(一):Producer消息發送實戰

一、前言

前些天和大家一起深入分析了kafka架構方法的知識,這部分內容偏向於理論,不過也是大數據開發工程師必須要掌握的知識點。

kafka架構篇系列文章:
深入分析Kafka架構(一):工作流程、存儲機制、分區策略
深入分析Kafka架構(二):數據可靠性、故障處理
深入分析Kafka架構(三):消費者消費方式、三種分區分配策略、offset維護

因此我覺得非常有必要再輔以代碼實現,來加深理解,融會貫通。因此本系列kafka實戰篇以實戰爲主,目的是使用kafka提供的JAVA API來完成消息的發送,消費,攔截等操作,用來加深對kafka架構的認知,並會把相關完整demo共享到github和咱們csdn上,感興趣的可以拿來使用。

本文爲kafka實戰系列第一篇,主要進行kafka的消息發送部分的流程解析及實戰開發。

注意:我所使用的kafka版本爲2.4.1,java版本爲1.8,本文會對一些新老版本的改動地方加以說明。

二、調試常用命令行總結

其實在日常使用kafka的過程中,很少使用命令行操作,一般命令行操作只是用來調試的時候使用。不過作爲回顧,下面列出一些常用的命令行操作,並對其進行詳細解釋。

  1. 查看當前服務器中的所有topic

    bin/kafka-topics.sh --zookeeper zookeeper主機名或ip:2181 --list
    
  2. 創建topic

    如下命令可以創建了一個3分區,2副本的topic first:

     ```
     bin/kafka-topics.sh --zookeeper zookeeper主機名或ip:2181 \
     --create --replication-factor 2 --partitions 3 --topic first
     
     選項說明:
     
     --topic 定義topic名
     --replication-factor 定義副本數
     --partitions 定義分區數
     ```
    
  3. 刪除topic

    注意:刪除topic的時候需要在server.properties中設置delete.topic.enable=true否則只是標記刪除,並沒有真正刪除。

    bin/kafka-topics.sh --zookeeper zookeeper主機名或ip:2181 \
    --delete --topic first
    
  4. 查看某個Topic的詳情

    bin/kafka-topics.sh --zookeeper zookeeper主機名或ip:2181 \
    --describe --topic first
    
  5. 發送消息

    注意:–broker-list kafka集羣裏的broker主機名或ip:9092,如果要發送給多個broker,用逗號分割就可以了。

    bin/kafka-console-producer.sh \
    --broker-list kafka集羣裏的broker主機名或ip:9092 --topic first
    輸入完上面的命令成功後會有">"標誌,就可以輸入數據了
    輸入數據,需要刪除的時候按住ctrl 在點擊backspace就可以刪除了。
    >hello world
    
  6. 消費消息

    注意:在kafka0.9.x版本之前,消費者指定的是zookeeper,但是新版本都是指定的kafka集羣。

     bin/kafka-console-consumer.sh \
    --bootstrap-server kafka集羣裏的broker主機名或ip:9092 --from-beginning --topic first
    
    --from-beginning:會把主題中以往所有的數據都讀取出來。
    
  7. 修改分區數

    注意:分區數只能增多不能減少(由於分區數減少後,把刪掉的分區的數據分配到剩餘的分區這個過程過於複雜,所以kafka沒有設計分區減少的邏輯。)

     ```
     bin/kafka-topics.sh --zookeeper zookeeper主機名或ip:2181 --alter --topic first --partitions 6 
     ```
    

三、Producer消息發送流程詳解

3.1、總體流程

我們談到消息隊列就會想到:異步,解耦,消峯。kafka自然也不例外,在新版本里,它的Producer發送消息採用的也是異步發送的方式(之前老版本有同步發送的api,新版本取消了,但是我們可以通過騷操作實現同步發送,後面會詳細解釋)。在消息發送的過程中,涉及到了兩個線程,分別是main線程(又叫主線程)和sender線程,以及一個線程共享變量(可以理解爲緩存)RecordAccumulator

總體來說,sender線程是main線程的守護線程,在工作時,main線程負責創建消息對象並將消息放在緩存RecordAccumulator,sender線程從緩存RecordAccumulator中拉取消息然後發送到kafka broker。

關於RecordAccumulator,咱們還需要知道:

  1. 在消息追加到RecordAccumulator時會對消息進行分類,發往同一分區的消息會被裝在同一個Deque中,Deque存放的是ProducerBatch表示一組消息。換句話說就是RecordAccumulator會按照分區進行隊列維護;
  2. 隊列中存放的是發往該分區的消息組,追加消息時候從隊列的尾部追加;
  3. RecordAccumulator的大小默認32M,可以通過buffer.memory配置指定;
  4. 如果內存空間用完了,追加消息將發生阻塞直到有空間可用爲止,默認最大阻塞60s,可以通過數max.block.ms配置。

整體流程圖可以看成下面這樣:
消息發送流程
其實知道了上面這些,就可以根據api寫一些簡單的kafka發送消息的代碼了。因爲在api裏,main線程和sender線程都被封裝的很好,很多事情是我們不需要去關心的。

不過如果你和我一樣,好奇main線程和sender線程具體都做了什麼?那就接着看下面這部分細化的的流程總結,不感興趣的話就可以直接跳到下一大節(異步發送Demo)開始擼代碼了。

3.2、分步驟細化流程

首先main線程的流程:

  1. 封裝消息對象爲ProducerRecord並調用send方法;
  2. 進入producer攔截器(攔截器可以自定義);
  3. 更新kafka集羣數據;
  4. 進行序列化,將消息對象序列化成byte數組;
  5. 使用分區器計算分區;
  6. 將消息追加到線程共享變量RecordAccumulator。

sender線程在KafkaProduer實例化結束開啓,後面就是sender線程乾的活了:

  1. sender線程將消息從RecordAccumulator中取出處理消息格式;
  2. 構建發送的請求對象Request;
  3. 將請求交給Selector,並將請求存放在請求隊列;
  4. 收到響應就移除請求隊列的請求,調用每個消息上的回調函數。

四、異步發送消息實戰

其實只要掌握了Producer消息發送的總體流程,就可以根據api寫基本demo了。下面我們層層遞進來完成異步發送demo。

4.1、引入依賴

這裏以maven依賴爲例,大家可以根據自己的kafka版本在mvn上找到適合自己的依賴,由於只是做簡單的消息發送,所以只需要引入kafka-clients依賴即可。我的kafka版本爲2.4.1,所以我需要引入的依賴爲:

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

建議:這裏建議大家引入依賴後,花點時間研究一下KafkaProducer源碼,至少看看裏面構造函數和方法的註釋也好,對kafka的學習很有幫助!

4.2、簡單異步發送Demo

這裏我們需要用到如下三個類:

  • KafkaProducer:需要創建一個生產者對象,用來發送數據;

  • ProducerConfig:獲取所需的一系列配置參數;

  • ProducerRecord:每條數據都要封裝成一個ProducerRecord對象纔可以發送。

下面就開始擼代碼,一共分爲4步:

  1. KafkaProducer有5個構造方法來初始化,這裏我們採用第3種傳遞properties的方式來初始化,所以第一步需要創建properties。

    properties是k,v結構的,種類非常多,不用刻意去記它,平時記住幾個常用的,然後額外的用到的時候在官網或者ProducerConfig源碼裏面去查就可以了。
    producerconfigs官網查詢地址: http://kafka.apache.org/documentation/#producerconfigs

    這裏我們使用到的參數設置如下:

    Properties props = new Properties();
    props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "kafka-1:9092");//kafka集羣,broker-list
    props.put(ProducerConfig.ACKS_CONFIG, "all");//all相當於-1
    props.put(ProducerConfig.RETRIES_CONFIG, 1);//重試次數
    props.put(ProducerConfig.BATCH_SIZE_CONFIG, 16384);//批次大小
    props.put(ProducerConfig.LINGER_MS_CONFIG, 1);//等待時間
    props.put(ProducerConfig.BUFFER_MEMORY_CONFIG, 33554432);//RecordAccumulator緩衝區大小
    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");
    

    在使用的時候,把上面kafka-1改爲你自己的kafka主機名或者ip就可以了。

  2. 有了properties之後就可以創建一個生產者對象了,把前面的props傳入KafkaProducer就可以了;

    Producer<String, String> producer = new KafkaProducer<>(props);
    
  3. 使用前面創建的producer對象的send方法來發送數據即可,這裏作爲demo,咱們用for循環發送100條0-99的數據;

    for (int i = 0; i < 100; i++) {
               producer.send(new ProducerRecord<String, String>("testKafka1", Integer.toString(i), Integer.toString(i)));
            }
    
  4. 發送完記得關閉生產者。

    producer.close();
    

到這裏一個簡單的異步producer Demo就寫完了,完整代碼如下:

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

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

public class AsyncProducerDemo {

    public static void main(String[] args) throws ExecutionException, InterruptedException {

        // 0.配置一系列參數
        Properties props = new Properties();
        props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "kafka-1:9092");//kafka集羣,broker-list
        props.put(ProducerConfig.ACKS_CONFIG, "all");
        props.put(ProducerConfig.RETRIES_CONFIG, 1);//重試次數
        props.put(ProducerConfig.BATCH_SIZE_CONFIG, 16384);//批次大小
        props.put(ProducerConfig.LINGER_MS_CONFIG, 1);//等待時間
        props.put(ProducerConfig.BUFFER_MEMORY_CONFIG, 33554432);//RecordAccumulator緩衝區大小
        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");

        // 1.創建一個生產者對象
        Producer<String, String> producer = new KafkaProducer<>(props);

        // 2.調用send方法
        for (int i = 0; i < 100; i++) {
            producer.send(new ProducerRecord<String, String>("testKafka1", Integer.toString(i), Integer.toString(i)));
        }
        // 3.關閉生產者
        producer.close();
    }
}

然後在kafka集羣某個broker下,輸入消費者命令行來驗證代碼:

bin/kafka-console-consumer.sh --bootstrap-server kafka-1:9092 --topic testKafka1 --from-beginning

執行代碼,然後會發現命令行成功輸出數據,證明demo完成:
demo截圖
反思:如果發送失敗了怎麼辦?生產者對象調用send方法會返回什麼?支持回調函數嗎?

首先回答第一個問題,如果發送失敗了,kafka內部有自己的一套機制,這是我們代碼不可控的,它會自動進行重新發送,我們只能控制失敗後的重試次數。
究竟這套失敗重新發送的機制是怎樣的,以及後兩個問題,我們下一節帶回調函數的異步發送Demo來進行詳細介紹。

4.3、帶回調函數的異步發送Demo

4.3.1、失敗重試機制

在這裏插入圖片描述
其實kafka的producer發送消息的流程如上圖所示,它內部維護了一套失敗重試機制,可以看到具體的消息失敗重試是kafka內部自動完成的,我們只能控制失敗重試的次數

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

4.3.2、解析回調函數

通過查看KafkaProducer源碼,可以發現有兩個方法:
send方法
其中有一個支持回調函數Callback,這裏的回調函數會在producer收到ack時異步調用,該方法有兩個參數,分別是RecordMetadata和Exception,如果Exception爲null,說明消息發送成功,如果Exception不爲null,說明消息發送失敗。

注意:除了發現callback,還可以發現send方法返回的是Future對象,這裏先有個印象,在後面講同步demo的時候會詳細解釋。

4.3.3、完成帶回調函數的異步發送Demo

我們只需要修改send方法並重寫onCompletion就可以了,別的都不用更改,代碼如下:

for (int i = 0; i < 100; i++) {
      producer.send(new ProducerRecord<String, String>("testKafka1", Integer.toString(i), Integer.toString(i)), new Callback() {

          //回調函數在Producer收到ack時異步調用
          @Override
          public void onCompletion(RecordMetadata metadata, Exception exception) {
              if (exception == null) {
                  System.out.println("消息發送成功->" + metadata.offset());
              } else {
                  exception.printStackTrace();
              }
          }
      });

  }

在java1.8的情況下,還可以使用lambda表達式來優化這段代碼:

for (int i = 0; i < 100; i++) {
     //回調函數在Producer收到ack時異步調用
     producer.send(new ProducerRecord<String, String>("testKafka1", Integer.toString(i), Integer.toString(i)), (metadata, exception) -> {
         if (exception == null) {
             System.out.println("消息發送成功->" + metadata.offset());
         } else {
             exception.printStackTrace();
         }
     });

 }

執行代碼,會發現符合代碼預期結果,帶回調函數的異步發送Demo完成:
帶回調函數的異步發送消息demo

五、同步發送消息實戰

5.1、從源碼找到端倪

通過前面查看KafkaProducer源碼,我們可以發現兩個send方法都是返回Future對象,而Future有一個get方法,並且這個get方法是阻塞的。雖然send是異步的,但是隻要我們每次send後都調用它的返回對象的get方法,那就可以實現同步發送消息的目的了,不得不說,kafka的源碼設計非常棒,能學到很多東西。

5.2、同步發送Demo

我們只需要修改異步發送demo的調用send方法這裏就可以實現同步發送了,具體來說,就是調用send方法返回的get()方法就可以了。不過爲了調試方便,我們拿到get()返回的原數據RecordMetadata對象,然後輸出這條數據的offset來驗證結果。

其實可以通過get()返回的RecordMetadata對象拿到很多原數據的信息:
RecordMetadata提供的方法
修改的代碼如下,爲了便於調試,我們輸出每條信息的offset:

for (int i = 0; i < 100; i++) {
    RecordMetadata metadata = producer.send(new ProducerRecord<String, String>("testKafka1", Integer.toString(i), Integer.toString(i))).get();
    System.out.println("offset = "+metadata.offset());
}

運行代碼,查看結果,符合咱們的預期,同步發送消息demo完成:
驗證同步發送demo

六、總結

本文對kafka生產者發送消息的流程進行了詳細的解釋和實戰,其中包含了新版本的kafka對於同步發送消息和異步發送消息的api實現,以及kafka源碼裏的回調函數和架構內部的失敗重試機制等都給出了底層的詳細解釋及實戰demo。
完整的代碼已上傳,感興趣的可以下載查看。
csdn:https://download.csdn.net/download/qq_26803795/12351587
github:https://github.com/ropleData/kafkaProducerDemo

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章