深入理解Kafka核心設計及原理(二):生產者

香吧香發表於2022-04-06

轉載請註明出處:

2.1Kafka生產者客戶端架構

                                             

2.2 Kafka 進行訊息生產傳送程式碼示例及ProducerRecord物件

  kafka進行訊息生產傳送程式碼示例:

public class KafkaProducerAnalysis {
    public static final String brokerList = "localhost:9092";
    public static final String topic = "topic-demo";
    public static Properties initConfig() (
         Properties props = new Properties();
         props.put("bootstrap.servers", brokerList);
        props.put("key.serializer","org.apache.kafka.common.serialization.StringSerializer");
       props.put("value.serializer","org.apache.kafka.common.serialization.StringSerializer");
      properties. put ("client. id", "producer. client. id. demo");
         return props;
    }
    public static void main(String[] args) {
        Properties props = initConfig();
        KafkaProducer<String, String> producer = new KafkaProducer<>(props);
        ProducerRecord<String,String> record = new ProducerRecord<> (topic, "hello, Kafka1 ");
        try {
            producer.send(record);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
 }

  構建的訊息物件ProducerRecord, 它並不是單純意義上的訊息,它包含了多個屬性,原本需要傳送的與業務相關的訊息體只是其中的一個value屬性,比如"Hello, Kafka!"只是ProducerRecord物件中的一個屬性。 ProducerRecord類的定義如下:

public class ProducerRecord<K, V> {
        private final String topic; //主題
        private final Integer partition; //分割槽號
        private final Headers headers; //訊息頭部
        private final K key; //
        private final V value; //
        private final Long timestamp; //訊息的時間戳
  //省略其他成員方法和構造方法
}

    其中topic和 partition欄位分別代表訊息要發往的主題和分割槽號。headers欄位是訊息的頭部,它大多用來設定 一些與應用相關的資訊,如無需要也可以不用設定。key是用來指定訊息的鍵, 它不僅是訊息的附加資訊,還可以用來計算分割槽號進而可以讓訊息發往特定的分割槽。

    key可以讓訊息再進行二次歸類, 同 一個key的訊息會被劃分到同 一個分割槽中, 有key的訊息還可以支援日誌壓縮的功能,value是指訊息體,一般不為空,如果為空則表示特定的訊息 一墓碑訊息;timestamp是指訊息的時間戳,它有 CreateTime 和 LogAppendTime 兩種型別,前者表示訊息建立的時間,後者表示訊息追加到日誌檔案的時間.

    KafkaProducer是執行緒安全的, 可以在多個執行緒中共享單個KafkaProducer例項,也 可以將KafkaProducer例項進行池化來供其他執行緒呼叫。

2.3 傳送訊息的三種模式及實現區別

  傳送訊息主要有三種模式: 發後即忘(fire-and-forget)、同步(sync)及非同步Casync)。

  發後即忘,它只管往Kafka中傳送訊息而並不關心訊息是否正確到達。 在大多數情況下,這種傳送方式沒有什麼問題,不過在某些時候(比如發生不可重試異常時)會造成訊息的丟失。 這種傳送方式的效能最高, 可靠性也最差。

  KafkaProducer的 send()方法並非是void型別, 而是Future<RecordMetadata>型別, send()方法有2個過載方法,具體定義如下:

public Future<RecordMetadata> send(ProducerRecord<K, V> record)
public Future<RecordMetadata> send(ProducerRecord<K, V> record,Callback callback)

  實現同步的傳送方式, 可以利用返回的 Future 物件實現:

try {
    producer.send(record) .get();
} catch (ExecutionException I InterruptedException e) {
    e.printStackTrace();
}

  send()方法本身就是非同步的,send()方法返回的Future物件可以使呼叫方稍後獲得傳送的結果。 示例中在執行send()方法之後直接鏈式呼叫了get()方法來阻塞等待Kaflca的響應,直到訊息傳送成功, 或者發生異常。 如果發生異常,那麼就需要捕獲異常並交由外層邏輯處理。

try {
    Future<RecordMetadata> future = producer.send{record);
    RecordMetadata metadata= future.get();
    System.out.println(metadata.topic() + "-" +metadata.partition() + ":" + metadata.offset());
    } catch (ExecutionException I InterruptedException e) {
    e.printStackTrace () ;
}

  這樣可以獲取一個RecordMetadata物件, 在RecordMetadata物件裡包含了訊息的一些後設資料資訊,比如當前訊息的主題、分割槽號、分割槽中的偏移量(offset)、 時間戳等。

 

2.4 序列化

  生產者需要用序列化器(Serializer)把物件轉換成位元組陣列才能通過網路傳送給Kafka。 而在對側, 消費者需要用反序列化器(Deserializer)把從Kafka 中收到的位元組陣列轉換成相應的物件。

  為 了方便, 訊息的key和value都使用了字串, 對應程式中的序列化器也使用了客戶端自帶的org.apache.kafka. common. serialization. StringSerializer, 除了用於String 型別的序列化器,還有ByteArray、ByteBuffer、 Bytes、 Double、Integer、 Long這幾種型別, 它們都實現了org.apache.kafka. common. serialization. Serializer介面

 

2.5 分割槽器

  訊息在通過send( )方法發往broker 的過程中,有可能需要經過攔截器(Interceptor)、 序列化器(Serializer)和分割槽器(Parttitioner)的一系列作用之後才能被真正地發往 broker。攔截器一般不是必需的, 而序列化器是必需的。 訊息 經過 序列化 之後就需要確定它發往的分割槽 ,如果訊息ProducerRecord中指定了 partitition欄位, 那麼就不需要分割槽器的作用, 因為partition代表的就是所要發往的分割槽號。

   如果訊息ProducerRecord中沒有 指定partition欄位,那麼就需要依賴分割槽器,根據key這個欄位來計算partition的值。 分割槽器的作用 就是為訊息 分配分割槽

  Kafka 中提供的預設分割槽器是org.apache.kafka.clients.producer.intemals.DefaultPartitioner, 它實現了org.apache.kafka.clients.producer.Partitioner 介面, 這個介面中定義了2個方法, 具體如下所示。

public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster);
public void close();

  其中 partition()方法用來計算分割槽號,返回值為 int 型別。partition()方法中的引數分別表示主題 、鍵、序列化後的鍵、值、序列化後的值,以及叢集的後設資料資訊,通過這些資訊可以實現功能豐富的分割槽器。 close()方法在關閉分割槽器的時候用來回收一些資源 。

  預設的分割槽器會對key 進行雜湊(採用MurmurHash2 演算法 ,具備高運算效能及低碰撞率),最終根據得到 的 雜湊值來計算分割槽號, 擁有相同 key 的訊息會被寫入同一個分割槽 。 如果 key 為 null ,那麼訊息將會以輪詢的方式發往主題內的各個可用分割槽。

 

2.6 攔截器

  生產者攔截器既可以用 來在訊息傳送前做一些準備工作 ,比如按照某個規則過濾不符合要求的消 息、修改消 息的內容等,也可以用來在傳送回撥邏輯前做一些定製化的需求,比如統計類工作。

  生產者攔截器 的 使用 也 很方便,主要是自定義實現org .apache.kafka. clients. producer.Producerlnterceptor 介面。ProducerInterceptor 接 口中包含 3 個方法 :

public ProducerRecord<K, V> onSend (ProducerRecord<K, V> record);
public void onAcknowledgement(RecordMetadata metadata, Excepti on exception );
public void close() ;

  KafkaProducer 在將訊息序列化和計算分割槽之前會呼叫 生產者攔截器 的 onSend()方法來對訊息進行相應 的定製化操作。KafkaProducer 會在訊息被應答( Acknowledgement )之前或訊息傳送失敗時呼叫生產者攔截器的onAcknowledgement()方法,優先於使用者設定的Callback 之前執行。

 

2.6 訊息累加器

  整個生產者客戶端由兩個執行緒協調執行,這兩個執行緒分別為主執行緒和 Sender 執行緒 (傳送執行緒)。在主執行緒中由 KafkaProducer 建立訊息,然後通過可能的攔截器、序列化器和分割槽器的作用之後快取到訊息累加器( RecordAccumulator,也稱為訊息收集器〉中。Sender 執行緒負責從RecordAccumulator 中 獲取訊息並將其傳送到 Kafka 中 。

  RecordAccumulator 主要用來快取訊息 以便Sender 執行緒可以批量傳送,進而減少網路傳輸的資源消耗以提升效能 。RecordAccumulator 快取的大 小可以通過生產者客戶端引數buffer. memory 配置,預設值為 33554432B ,即32MB 。 如果生產者傳送訊息的速度超過傳送到伺服器的速度 ,則會導致生產者空間不足,這個時候 KafkaProducer 的 send()方法呼叫要麼被阻塞,要麼丟擲異常,這個取決於引數 max. block . ms 的配置,此引數的預設值為 6 0000,即 60 秒 。

   Sender 從RecordAccumulator 中 獲取快取的訊息之後,會進一 步將原本<分割槽,Deque<Producer Batch>>的儲存形式轉變成<Node , List< ProducerBatch>的形式,其中 Node 表示 Kafka叢集的 broker 節點 。對於網路連線來說,生產者客戶端是與具體 的 broker 節點建立的連線,也就是 向具體的broker 節點傳送訊息,而並不關心訊息屬於哪一個分割槽;而對於KafkaProducer的應用邏輯而言 ,我們只 關注向哪個分割槽中傳送哪些訊息,所 以在這裡需要做一個應用邏輯層面到網路 1/0 層面的轉換。

  後設資料是指 Kafka 叢集的後設資料,這些後設資料具體記錄了叢集中有哪些主題,這些主題有哪些分割槽,每個分割槽的 leader 副本分配在哪個節點上,follower 副本分配在哪些節點上,哪些副本在 AR 、ISR 等集合中,叢集中有哪些節點,控制器節點又是哪一個等資訊。

 

2.7 重要的生產者引數

  1.acks

     這個引數用來指定分割槽中必須要有多少個副本收到這條訊息,之後生產者才會認為這條訊息是成功寫入的。acks 是生產者客戶端中一個非常重要的引數,它涉及訊息的可靠性和吞吐量之間的權衡。  acks 引數有 3 種型別的值(都是字串型別)。

    acks =1 : 預設值即為l 。生產者傳送訊息之後,只要分割槽的leader 副本成功寫入訊息,那麼它就會收到來自服務端的成功響應 。 如果訊息無法寫入 leader 副本,比如在leader 副本崩潰、重新選舉新的leader 副本的過程中,那麼生產者就會收到一個錯誤的響應,為了避免訊息丟失,生產者可以選擇重發訊息 。如果訊息寫入 leader 副本並返回成功響應給生產者,且在被其他 follower 副本拉取之前 leader 副本崩潰,那麼此時訊息還是會丟失,因為新選舉的 leader 副本中並沒有這條對應的訊息 。 acks 設定為l ,是訊息可靠性和吞吐量之間的折中方案。

     acks = 0 :生產者傳送消 息之後不需要等待任何服務端的響應。如果在訊息從傳送到寫入 Kafka 的過程中出現某些異常,導致 Kafka 並沒有收到這條訊息,那麼生產者也無從得知,訊息也就丟失了。在其他配置環境相同的情況下,acks 設定為 0 可以達到最大的吞吐量。

     acks =- l 或 acks =all : 生產者在消 息傳送之後,需要等待 ISR 中的所有副本都成功寫入訊息之後才能夠收到來自服務端的成功響應。在其他配置環境相同的情況下,acks 設定為-1(all )可以達到最強的可靠性。但這並不意味著訊息就一定可靠,因為 ISR 中可能只有 leader 副本,這樣就退化成了 acks= l 的情況。

  2.max.request.size

     這個引數用來限制生產者客戶端能傳送的訊息的最大值,預設值為1048576B ,即lMB 。一般情況下,這個預設值就可以滿足大多數的應用場景了。

  3.retries 和 retry. backoff.ms

     retries 引數用來配置生產者重試的次數,預設值為 0,即在發生異常的時候不進行任何重試動作。訊息在從生產者發出到成功寫入伺服器之前可能發生一些臨時性的異常,比如網路抖動、leader 副本的選舉等,這種異常往往是可以自行恢復的,生產者可以通過配置 retries大於 0 的值,以此通過 內 部重試來恢復而不是一昧地將異常拋給生產者的應用程式。 如果重試達到設定的次數 ,那麼生產者就會放棄重試並返回異常。

    不過並不是所有的異常都是可以通過重試來解決的,比如訊息太大,超過 max.request.size 引數配置的值時,這種方式就不可行了。 重試還和另一個引數 retry.backoff.ms 有關,這個引數的預設值為100 ,它用來設定兩次重試之間的時間間隔,避免無效的頻繁重試。在配置 retries 和retry.backoff.ms之前,最好先估算一下可能的異常恢復時間,這樣可以設定總的重試時間大於這個異常恢復時間,以此來避免生產者過早地放棄重試 。

  4.compression.type

     這個引數用來指定訊息的壓縮方式,預設值為“ none ”,即預設情況下,訊息不會被壓縮。該引數還可以配置為“ gzip ”,“ snappy ” 和“ lz4 ”。 對訊息進行壓縮可以極大地減少網路傳輸量 、降低網路 IO ,從而提高整體的效能 。訊息壓縮是一種使用時間換空間的優化方式,如果對時延有一定的要求,則不推薦對訊息進行壓縮 。

  5. request.timeout.ms

     這個引數用來配置 Producer 等待請求響應的最長時間,預設值為 3 0000( ms )。請求超時之後可以選擇進行重試。注意這個引數需要 比 broker 端引數 replica.lag.time.max.ms 的值要大 ,這樣可以減少因客戶端重試而引起的訊息重複的概率。

 

相關文章