Kafka Producer模塊分析

1. 概述

一、Kafka Producer包含哪些部分
其實我們討論producer時,指的是用戶接觸的clients.producer這個util包,其中包含了發送數據到哪臺機器、怎樣序列化、分批發送與拒絕消息等等的發送策略。
但這些討論不包括Kafka接到這些消息後怎樣處理的問題,這些是broker(即kafka server端)需要去處理的部分。
二、Producer與Broker的關係
producer會維護將要發送的topic表,在必要時向broker要這些topic的元信息,並在client端維護這些元信息,來決定每條消息的去向。
從broker角度來看,它提供了元信息給producer以後,就無法控制producer的行爲了。可以這麼理解——broker本身是無狀態的機器,主要提供的是元信息的消息讀寫的接口。
三、Producer發送消息的流程簡介

  1. 在發送消息時,等待元信息的更新
  2. 將key\value序列化爲byte[],計算出數據的大小是否超出限制
  3. 計算出key對應的partition,以確認將消息發往哪裏
  4. 使用accumulator將數據放到topic\partition對應的緩衝區中
  5. 使用NetworkClient類(背後是nio selector)定期的把緩衝區中的內容發出

注:本文基於Kafka 0.9版本,KafkaProducer的代碼更新時間在2015-12-06

2. 字段與主要方法

2.1 KafkaProducer中的字段

    private static final AtomicInteger PRODUCER_CLIENT_ID_SEQUENCE = new AtomicInteger(1);

    private String clientId;                           // client.id參數:用於日誌與打點,以及發請求帶的參數
    private final Partitioner partitioner;        // partitioner.class參數:指定一個Partitioner類用於計算key對應的分片,可自定義
    private final int maxRequestSize;          // max.request.size參數:單條消息的內存byte數大於此值時,拋出RecordTooLargeException異常
    private final long totalMemorySize;        // buffer.memory參數:同上,但額外作爲accumulator->BufferPool中的`The maximum amount of memory that this buffer pool can allocate`,似乎是accumulator中暫存消息最多可用的內存量。
    private final Metadata metadata;          // 維護了一些topic的元信息
    private final RecordAccumulator accumulator;    // 用於堆積消息,按情況batch發送
    private final Sender sender;      // 發送消息
    private final Thread ioThread;   // 把this.sender包裝成一個Thread
    private final CompressionType compressionType;    //compression.type參數,壓縮數據的方式,主要用在accumulator內。有none\gzip\snappy\lz4幾種
    private final Metrics metrics;      // TODO 監控信息?
    private final Sensor errors;    // TODO 監控信息?
    private final Time time;          // KafkaProducer對象的創建時間
    private final Serializer<K> keySerializer;        // key序列化方法 
    private final Serializer<V> valueSerializer;    // value序列化方法
    private final ProducerConfig producerConfig;    // 外部把配置傳入,但沒多大用處
    private final long maxBlockTimeMs;      // 來源不明TODO,用處似乎是限制send的一些IO操作總時間(waitOnMetadata堵塞的時間,與accumulator.append中waitMemory堵塞的時間之和,要低於此值)
    private final int requestTimeoutMs;    //參數`timeout.ms` or `request.timeout.ms`,用途不明TODO

2.2 KafkaProducer提供的api

2.2.1 發送一條Kafka消息

public Future<RecordMetadata> send(ProducerRecord<K, V> record, Callback callback)
  1. 該方法做一定預處理後(詳見第3節),將消息塞進accumulator緩衝區後,等待異步發送並返回,在消息實際發送完,即取到請求的返回值後,執行callback參數的回調方法。
  2. 返回體中包裝的RecordMetadata類型,帶有消息最終發到的partition與offset,可用future.get()堵塞到消息發送完成。
  3. 官方建議:由於callback會執行一個IO線程,因此建議處理速度足夠快,否則會影響到其它線程。如果需要執行堵塞或計算量大的邏輯,建議另開一個Executor來併發處理callback來的數據。
  4. 可能拋出的異常:InterruptException(堵塞時被打斷)、SerializationException(序列化失敗)、BufferExhaustedException(buffer空間用盡)

2.2.2 刷新緩存

public void flush()

此方法會將accumulator緩衝區中,尚未發出的那些消息變成可發送狀態。調用此方法會讓當前線程堵塞住,並保證調用flush()之前曾發送的所有消息都執行完成,即kafka服務端確實收到這些消息
值得注意的是,在調用flush陷入堵塞時,其它線程仍然可以繼續調用send發送消息。
以下是官方舉例的一個使用case:

// 消費100條消息,打入另一個topic後,確保所有數據發送成功,再commit消費進度
for(ConsumerRecord<String, String> record: consumer.poll(100))
     producer.send(new ProducerRecord("my-topic", record.key(), record.value());
producer.flush();
consumer.commit();

2.2.3 topic的partition信息

public List<PartitionInfo> partitionsFor(String topic)

可獲取指定topic的partition元信息,若本地metadata緩存中沒有此topic,則堵塞等待更新元信息。
超時時間定爲maxBlockTimeMs,堵塞可被打斷拋出InterruptException。

2.2.4 指標信息

public Map<MetricName, ? extends Metric> metrics()

獲取監控信息,或說kafka使用過程中的打點。(TODO待補充)

2.2.5 關閉producer

public void close(long timeout, TimeUnit timeUnit)

關閉當前producer,堵塞當前線程等待所有sent請求完成。

3. 發送細節與機制詳述

3.1 獲取Metadata

以下爲Metadata中的字段

    private final long refreshBackoffMs;  // 更新失敗時最小的再次刷新間隔時間
    private final long metadataExpireMs;  // 過期時間, 默認60s
    private int version;  // 每次更新version自增
    private long lastRefreshMs;  // 最近的更新時的時間
    private long lastSuccessfulRefreshMs; // 最近成功更新的時間
    private Cluster cluster;  // 保存topic與partition、結點與partition等等的關係
    private boolean needUpdate; // 需要更新 metadata
    private final Map<String, Long> topics; // topic與對應的過期時間的對應關係
    private final List<Listener> listeners; // 事件監控者
    private boolean needMetadataForAllTopics; // 是否強制更新所有的 metadata

producer在發送前執行waitOnMetadata(String topic, long maxWaitMs)來獲取元信息。此方法判斷緩存中是否有topic信息,沒有的話進入以下循環:

            int version = metadata.requestUpdate();    // 告訴metadata信息需要更新了
            sender.wakeup();                           // 喚醒sender來更新metadata
            metadata.awaitUpdate(version, remainingWaitMs);  // 進入堵塞直到版本更新

上述的awaitUpdate操作中,metadata會堵塞住,等待sender的一系列請求成功後調用Metadata.update來喚醒自己。sender是kafka client的一個network util包裝,後面再詳細介紹。

如果這個過程超時了,或者topic unauthorized(TODO:想想unauthorized是什麼情況?),就開始拋出異常。如果沒有異常的順利完成,topic對應的partition信息就會被producer收到並cache在內存中,producer可用此信息來發送消息。

3.2 消息的發送與緩衝區

一、計算partition與結點
經過3.1中討論的元信息獲取,producer就獲取了這條消息topic有幾個partition(分片)以及這些分片對應的結點信息。
需要發往哪一個partition:

  1. 觀察消息是否指定了partition,未指定的話走partition類計算數據分片;
  2. 默認partitioner類:如果消息帶有key則按hash(keyBytes)%numPartitions,不帶有key則按round-robin輪流發往各個partition
  3. 可以通過partitioner.class配置,自定義partitioner類,按照業務需求來自定義partition計算方式。
    確認了消息發往哪一個partition,也就能從元信息中找到partition對應的leader結點了。

二、accumulator提供的消息緩衝
確認了消息要發送的結點後,下一步就是將消息內容與結點信息塞進accumulator。
accumulator是一個buffer,負責把producer發送的消息累積在那裏,直到特定條件時發出。其buffer採用一個map<TopicPartition, Deque >來存儲,每一個topic\partition下都是一個RecordBatch隊列。
隊列中每一個RecordBatch元素,都代表一組kafka消息。在一個RecordBatch滿了,或者其它線程調用flush()\close()等操作時,producer都會喚醒sender,來負責把buffer中的RecordBatch發到kafka服務端去。

3.3 消息緩衝的flush機制

在accumulator中使用了一個AtomicInteger flushesInProgress來表示是否在flush狀態。
在Sender類(在4.2中會詳細介紹Sender類)每次迭代時,會先算出現在要往哪些結點發送消息。計算邏輯是一個緩衝區滿了、超時、沒空間,或當前在flush狀態,就會認爲消息需要發出去了。
因此:當我們將flushesInProgress加一,則會在Sender下一輪迭代時,將所有待發數據的結點全標成待發送的。flush()流程示意如下:

  1. 將flushesInProgress++
  2. 將sender喚醒去幹活
  3. 等待所有未完成的緩衝消息發送完成
  4. 將flushesInProgress–

因此我們可以理解爲,flush()操作會將當前所有緩存都發出一次。這就保證了在flush()前所有消息都會被髮送完成,flush狀態才結束;但保持flush狀態的過程中新加入的消息,我們無法確定其狀態。

4. 拓展:Sender

前面提到更新metadata,或發送accumulator中的批量消息,以及flush(),此類網絡I/O操作,都會執行一個相同的代碼sender.wakeup()。所以這個萬能的sender到底是什麼東西呢?

4.1 初始化與啓動

下面是producer的構造器中,把Sender初始化出來的地方:

            this.sender = new Sender(client,
                    this.metadata,
                    this.accumulator,
                    config.getInt(ProducerConfig.MAX_REQUEST_SIZE_CONFIG),
                    (short) parseAcks(config.getString(ProducerConfig.ACKS_CONFIG)),
                    config.getInt(ProducerConfig.RETRIES_CONFIG),
                    this.metrics,
                    new SystemTime(),
                    clientId,
                    this.requestTimeoutMs);
            String ioThreadName = "kafka-producer-network-thread" + (clientId.length() > 0 ? " | " + clientId : "");
            this.ioThread = new KafkaThread(ioThreadName, this.sender, true);
            this.ioThread.start();

producer構造了一個Runnable的sender的成員變量,並開啓線程執行。

4.2 Sender做了什麼

其主要工作是不斷的循環以下操作:

  1. 通過accumulator中可發出的數據包,以及metadata信息,算出現在準備發送消息的結點。(若找不到leader信息,標記一下更新metadata)
  2. "倒出"accumulator中的數據(把超時未發的數據打個記錄),把數據包和對應結點打包創建出ClientRequest。
  3. 把這些請求塞進sender中維護的client(使用nio維護的請求隊列),並通過client.poll來發送出去。(TODO 網絡相關的操作補充)
  4. poll()的時候會執行一個metadataUpdater.maybeUpdate(now)的操作:
    • 先計算出下次刷新時間:根據是否請求刷新元數據(見本節第1條及3.1節中的刷新策略),或metadata.max.age.ms配置中的最久沒有成功刷新的時間,並考慮到retry.backoff.ms配置中的最短刷新間隔,得出一個下次刷新的時間。
    • 考慮timeToNextReconnectAttempt,以及是否正在獲取metadata信息,來更新下次刷新時間。
    • 如果已經到了可刷新時間點,獲取leastLoadedNode,並將打向這個node的請求塞進client,也就搭上了這一波client.poll網絡請求的車。

可以舉個例子幫助解釋:

  1. 第一輪迭代中,如果需要某些node A的元信息並發現缺失了,此時sender會請求更新metadata。那麼在當前這一輪迭代結束的poll(now)操作中,通過metadataUpdater.mayBeUpdate(now)發現需要更新metadata,併發出更新的網絡請求。
  2. 第二輪迭代中:由於metadata的更新請求只發出沒處理,因此依然沒有缺失node的信息。但是在這一輪迭代結束的poll()中,如果metadata請求返回了,就可以通過返回值補上node A的信息。
  3. 第三輪迭代中:可正常獲取node A的信息,並向A發消息了。

5. 其它

TODO,待補充kafka 0.9到現在的更新內容

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