【生產者原始碼分析系列第八篇】圖解 Kafka 原始碼之 Sender 執行緒架構設計

ITPUB社群發表於2023-02-22

閱讀本文大約需要 20 分鐘。

大家好,我是 華仔, 又跟大家見面了。

上篇主要帶大家深度剖析了「號稱承載 Kafka 客戶端訊息快遞倉庫 RecordAccmulator 的架構設計」,訊息被暫存到累加器中,今天主要聊聊「傳送網路 I/O 的 Sender 執行緒的架構設計」,深度剖析下訊息是如何被髮送出去的。

這篇文章乾貨很多,希望你可以耐心讀完。
【生產者原始碼分析系列第八篇】圖解 Kafka 原始碼之 Sender 執行緒架構設計

01 總的概述

透過「場景驅動」的方式,來看看訊息是如何從客戶端傳送出去的。

在上篇中,我們知道了訊息被暫存到 Deque<ProducerBatch> 的 batches 中,等「批次已滿」或者「有新批次被建立」後,喚醒 Sender 子執行緒將訊息批次傳送給 Kafka Broker 端。

【生產者原始碼分析系列第八篇】圖解 Kafka 原始碼之 Sender 執行緒架構設計

接下來我們就來看看,「Sender 執行緒的架構實現以及傳送處理流程」,為了方便大家理解,所有的原始碼只保留骨幹。

02 Sender 執行緒架構設計

《圖解Kafka生產者初始化核心流程》這篇中我們知道 KafkaProducer 會啟動一個後臺守護程式,其執行緒名稱:kafka-producer-network-thread + "|" + clientId

在 KafkaProducer.java 類有常量定義:NETWORK_THREAD_PREFIX,並啟動 守護執行緒 KafkaThread 即 ioThread,如果不主動關閉 Sender 執行緒會一直執行下去。

github 原始碼地址如下:

public class KafkaProducer<KVimplements Producer<KV{
    public static final String NETWORK_THREAD_PREFIX = "kafka-producer-network-thread";
    // visible for testing
    @SuppressWarnings("unchecked")
    KafkaProducer(Map<String, Object> configs,Serializer<K> keySerializer,
              Serializer<V> valueSerializer,ProducerMetadata metadata,
              KafkaClient kafkaClient,ProducerInterceptors<K, V> interceptors,Time time) {
        try {
            ...
            this.sender = newSender(logContext, kafkaClient, this.metadata);
            String ioThreadName = NETWORK_THREAD_PREFIX + " | " + clientId;
            this.ioThread = new KafkaThread(ioThreadName, this.sender, true);
            this.ioThread.start();
            ...
            log.debug("Kafka producer started");
        } catch (Throwable t) {
           ...
        }
    }
}

從上面得出 Sender 類是一個執行緒類, 我們來看看 Sender 執行緒的重要欄位和方法,並講解其是如何傳送訊息和處理訊息響應的

02.1 關鍵欄位

 /**
 * The background thread that handles the sending of produce requests to the Kafka cluster. This thread makes metadata
 * requests to renew its view of the cluster and then sends produce requests to the appropriate nodes.
 */

public class Sender implements Runnable {
    /* the state of each nodes connection */
    private final KafkaClient client; // 為 Sender 執行緒提供管理網路連線進行網路讀寫
    /* the record accumulator that batches records */
    private final RecordAccumulator accumulator; // 訊息倉庫累加器
    /* the metadata for the client */
    private final ProducerMetadata metadata; // 生產者後設資料
    /* the flag indicating whether the producer should guarantee the message order on the broker or not. */
    private final boolean guaranteeMessageOrder; // 是否保證訊息在 broker 端的順序性 
    /* the maximum request size to attempt to send to the server */
    private final int maxRequestSize; //傳送訊息最大位元組數。  
    /* the number of acknowledgements to request from the server */
    private final short acks; // 生產者的訊息傳送確認機制
    /* the number of times to retry a failed request before giving up */
    private final int retries; // 傳送失敗後的重試次數,預設為0次

    /* true while the sender thread is still running */
    private volatile boolean running; // Sender 執行緒是否還在執行中     
    /* true when the caller wants to ignore all unsent/inflight messages and force close.  */
    private volatile boolean forceClose; // 是否強制關閉,此時會忽略正在傳送中的訊息。
    /* the max time to wait for the server to respond to the request*/
    private final int requestTimeoutMs; // 等待服務端響應的最大時間,預設30s       
    /* The max time to wait before retrying a request which has failed */
    private final long retryBackoffMs; // 失敗重試退避時間      
    /* current request API versions supported by the known brokers */
    private final ApiVersions apiVersions; // 所有 node 支援的 api 版本
    /* all the state related to transactions, in particular the producer id, producer epoch, and sequence numbers */
    private final TransactionManager transactionManager; // 事務管理,這裡忽略 後續會有專門一篇講解事務相關的
    // A per-partition queue of batches ordered by creation time for tracking the in-flight batches
    private final Map<TopicPartition, List<ProducerBatch>> inFlightBatches; // 正在執行傳送相關的訊息批次集合, key為分割槽,value是 list<ProducerBatch> 。

從該類屬性欄位來看比較多,這裡說幾個關鍵欄位:

  1. client:KafkaClient 型別,是一個介面類,Sender 執行緒主要用它來實現真正的網路I/O,即 NetworkClient。該欄位主要為 Sender 執行緒提供了網路連線管理和網路讀寫操作能力。
  2. accumulator:RecordAccumulator型別,上一篇的內容  圖解 Kafka 原始碼之快遞倉庫 RecordAccumulator 架構設計,Sender 執行緒用它獲取待傳送的 node 節點及批次訊息等能力。
  3. metadata:ProducerMetadata型別,生產者後設資料。因為傳送訊息時要知道分割槽 Leader 在哪些節點,以及節點的地址、主題分割槽的情況等。
  4. guaranteeMessageOrder:是否保證訊息在 broker 端的順序性,引數:max.in.flight.requests.per.connection。
  5. maxRequestSize:單個請求傳送訊息最大位元組數,預設為1M,它限制了生產者在單個請求傳送的記錄數,以避免傳送大量請求。
  6. acks:生產者的訊息傳送確認機制。有3個可選值:0,1,-1/all。
【生產者原始碼分析系列第八篇】圖解 Kafka 原始碼之 Sender 執行緒架構設計
  1. retries:生產者傳送失敗後的重試次數。預設是0次。
  2. running:Sender執行緒是否還在執行中。
  3. forceClose:是否強制關閉,此時會忽略正在傳送中的訊息。
  4. requestTimeoutMs:生產者傳送請求後等待服務端響應的最大時間。如果超時了且配置了重試次數,會再次傳送請求,待重試次數用完後在這個時間範圍內返回響應則認為請求最終失敗,預設 30 秒。
  5. retryBackoffMs:生產者在傳送請求失敗後可能會重新傳送失敗的請求,其目的就是防止重發過快造成服務端壓力過大。預設100 ms。
  6. apiVersions:ApicVersions類物件,儲存了每個node所支援的api版本。
  7. inFlightBatches:正在執行傳送相關的訊息批次集合, key為分割槽,value是 list<|ProducerBatch>。

02.2 關鍵方法

Sender 類的方法也不少,這裡針對關鍵方法逐一講解下,原文完整版在星球裡,感興趣的可以掃文末二維碼加入

02.2.1 run()

Sender 執行緒實現了 Runnable 介面,會不斷的呼叫 runOnce(),這是一個典型的迴圈事件機制。

/**
  * The main run loop for the sender thread
  */

@Override
public void run() {
        log.debug("Starting Kafka producer I/O thread.");
        // 這裡使用 volatile boolean 型別的變數 running,判斷 Sender 執行緒是否在執行狀態中。
        // 1. 如果 Sender 執行緒在執行狀態即 running=true,則一直迴圈呼叫 runOnce() 方法。
        while (running) {
            try {
                // 將緩衝區的訊息傳送到 broker。
                runOnce();
            } catch (Exception e) {
                log.error("Uncaught error in kafka producer I/O thread: ", e);
            }
        }
        log.debug("Beginning shutdown of Kafka producer I/O thread, sending remaining records.");
        // 2. 如果(沒有強制關閉 && ((訊息累加器中還有剩餘訊息待傳送 || 還有等待未響應的訊息 ) || 還有事務請求未完成)),則繼續傳送剩下的訊息。
        while (!forceClose && ((this.accumulator.hasUndrained() || this.client.inFlightRequestCount() > 0) || hasPendingTransactionalRequests())) {
            try {
                // 繼續執行將剩餘的訊息傳送完畢
                runOnce();
            } catch (Exception e) {
                log.error("Uncaught error in kafka producer I/O thread: ", e);
            }
        }
        // 3. 對進行中的事務進行中斷,則繼續傳送剩下的訊息。
        while (!forceClose && transactionManager != null && transactionManager.hasOngoingTransaction()) {
            if (!transactionManager.isCompleting()) {
                log.info("Aborting incomplete transaction due to shutdown");
                transactionManager.beginAbort();
            }
            try {
                runOnce();
            } catch (Exception e) {
                log.error("Uncaught error in kafka producer I/O thread: ", e);
            }
        }
        // 4. 如果強制關閉,則關閉事務管理器、終止訊息的追加並清空未完成的批次
        if (forceClose) {
            if (transactionManager != null) {
                log.debug("Aborting incomplete transactional requests due to forced shutdown");
                // 關閉事務管理器
                transactionManager.close();
            }
            log.debug("Aborting incomplete batches due to forced shutdown");
            // 終止訊息的追加並清空未完成的批次
            this.accumulator.abortIncompleteBatches();
        }
        // 5. 關閉 NetworkClient
        try {
            this.client.close();
        } catch (Exception e) {
            log.error("Failed to close network client", e);
        }
        log.debug("Shutdown of Kafka producer I/O thread has completed.");
}

當 Sender 執行緒啟動後會直接執行 run() 方法,該方法在 4種情況下會一直迴圈呼叫去傳送訊息到 Broker。

02.2.2 runOnce()

我們來看看這個執行業務處理的方法,關於事務的部分後續專門文章講解。

  /**
 * Run a single iteration of sending
 */

void runOnce() {
        if (transactionManager != null) {
           ... //事務處理方法 後續文章專門講解 
        }
        // 1. 獲取當前時間的時間戳。
        long currentTimeMs = time.milliseconds();
        // 2. 呼叫 sendProducerData 傳送訊息,但並非真正的傳送,而是把訊息快取在 把訊息快取在 KafkaChannel 的 Send 欄位裡。下一篇會講解 NetworkClient。
        long pollTimeout = sendProducerData(currentTimeMs);
        // 3. 讀取訊息實現真正的網路傳送
        client.poll(pollTimeout, currentTimeMs);
}

該方法比較簡單,主要做了3件事情:

  1. 獲取當前時間的時間戳。
  2. 呼叫 sendProducerData 傳送訊息,但並非真正的傳送,而是把訊息快取在 NetworkClient 的 Send 欄位裡。下一篇會講解 NetworkClient。
  3. 讀取 NetworkClient 的 send 欄位訊息實現真正的網路傳送。

02.2.3 sendProducerData()

該方法主要是獲取已經準備好的節點上的批次資料並過濾過期的批次集合,最後暫存訊息。

 private long sendProducerData(long now) {
        // 1. 獲取後設資料
        Cluster cluster = metadata.fetch();
        // get the list of partitions with data ready to send
        // 2. 獲取已經準備好的節點以及找不到 Leader 分割槽對應的節點的主題
        RecordAccumulator.ReadyCheckResult result = this.accumulator.ready(cluster, now);
        // if there are any partitions whose leaders are not known yet, force metadata update
        // 3. 如果主題 Leader 分割槽對應的節點不存在,則強制更新後設資料
        if (!result.unknownLeaderTopics.isEmpty()) {
            // 新增 topic 到沒有拉取到後設資料的 topic 集合中,並標識需要更新後設資料
            for (String topic : result.unknownLeaderTopics)
                this.metadata.add(topic, now);
            ...
            // 針對這個 topic 集合進行後設資料更新
            this.metadata.requestUpdate();
        }
        // 4. 迴圈 readyNodes 並檢查客戶端與要傳送節點的網路是否已經建立好了
        Iterator<Node> iter = result.readyNodes.iterator();
        long notReadyTimeout = Long.MAX_VALUE;
        while (iter.hasNext()) {
            Node node = iter.next();
            // 檢查客戶端與要傳送節點的網路是否已經建立好了
            if (!this.client.ready(node, now)) {
                // 移除對應節點
                iter.remove();
                notReadyTimeout = Math.min(notReadyTimeout, this.client.pollDelayMs(node, now));
            }
        }
        // create produce requests
        // 5. 獲取上面返回的已經準備好的節點上要傳送的 ProducerBatch 集合
        Map<Integer, List<ProducerBatch>> batches = this.accumulator.drain(cluster, result.readyNodes, this.maxRequestSize, now);
        // 6. 將從訊息累加器中讀取的資料集,放入正在執行傳送相關的訊息批次集合中
        addToInflightBatches(batches);
        // 7. 要保證訊息的順序性,即引數 max.in.flight.requests.per.connection=1
        if (guaranteeMessageOrder) {
            // Mute all the partitions drained
            for (List<ProducerBatch> batchList : batches.values()) {
                for (ProducerBatch batch : batchList)
                    // 對 tp 進行 mute,保證只有一個 batch 正在傳送
                    this.accumulator.mutePartition(batch.topicPartition);
            }
        }
        // 重置下一批次的過期時間
        accumulator.resetNextBatchExpiryTime();
        // 8. 從正在執行傳送資料集合 inflightBatches 中獲取過期集合
        List<ProducerBatch> expiredInflightBatches = getExpiredInflightBatches(now);
        // 9. 從 batches 中獲取過期集合
        List<ProducerBatch> expiredBatches = this.accumulator.expiredBatches(now);
        // 10. 從 inflightBatches 與 batches 中查詢已過期的訊息批次(ProducerBatch),判斷批次是否過      期是根據系統當前時間與 ProducerBatch 建立時間之差是否超過120s,過期時間可以透過引數 delivery.timeout.ms 設定。
        expiredBatches.addAll(expiredInflightBatches);

        // 如果過期批次不為空 則輸出對應日誌
        if (!expiredBatches.isEmpty())
            log.trace("Expired {} batches in accumulator", expiredBatches.size());
        // 11. 處理已過期的訊息批次,通知該批訊息傳送失敗並返回給客戶端
        for (ProducerBatch expiredBatch : expiredBatches) {
            // 處理當前過期ProducerBatch的回撥結果 ProduceRequestResult,並且設定超時異常 new TimeoutException(errorMessage)
            String errorMessage = "Expiring " + expiredBatch.recordCount + " record(s) for " + expiredBatch.topicPartition
                + ":" + (now - expiredBatch.createdMs) + " ms has passed since batch creation";
            // 通知該批訊息傳送失敗並返回給客戶端
            failBatch(expiredBatch, -1, NO_TIMESTAMP, new TimeoutException(errorMessage), false);
            // ... 事務管理器的處理忽略
        }
        // 收集統計指標,後續會專門對 Kafka 的 Metrics 進行分析
        sensors.updateProduceRequestMetrics(batches);

        // 設定下一次的傳送延時
        long pollTimeout = Math.min(result.nextReadyCheckDelayMs, notReadyTimeout);
        pollTimeout = Math.min(pollTimeout, this.accumulator.nextExpiryTimeMs() - now);
        pollTimeout = Math.max(pollTimeout, 0);
        if (!result.readyNodes.isEmpty()) {
            log.trace("Nodes with data ready to send: {}", result.readyNodes);
            pollTimeout = 0;
        }
        // 12. 傳送訊息暫存到 NetworkClient send 欄位裡。
        sendProduceRequests(batches, now);
        return pollTimeout;
}

該方法主要做了12件事情,逐一說明下:

  1. 首先獲取後設資料,這裡主要是根據後設資料的更新機制來保證資料的準確性。
  2. 獲取已經準備好的節點。accumulator#reay() 方法會透過傳送記錄對應的節點和後設資料進行比較,返回結果中包括兩個重要的集合:「準備好傳送的節點集合 readyNodes」、「找不到 Leader 分割槽對應節點的主題 unKnownLeaderTopic」。
  3. 如果主題 Leader 分割槽對應的節點不存在,則強制更新後設資料。
  4. 迴圈 readyNodes 並檢查客戶端與要傳送節點的網路是否已經建立好了。在 NetworkClient 中維護了客戶端與所有節點的連線,這樣就可以透過連線的狀態判斷是否連線正常。
  5. 獲取上面返回的已經準備好的節點上要傳送的 ProducerBatch 集合。accumulator#drain() 方法就是將 「TopicPartition」-> 「ProducerBatch 集合」的對映關係轉換成 「Node 節點」->「ProducerBatch 集合」的對映關係,如下圖所示,這樣的話按照節點方式只需要2次就完成,大大減少網路的開銷。
【生產者原始碼分析系列第八篇】圖解 Kafka 原始碼之 Sender 執行緒架構設計
  1. 將從訊息累加器中讀取的資料集,放入正在執行傳送相關的訊息批次集合中。
  2. 要保證訊息的順序性,即引數 max.in.flight.requests.per.connection=1,會新增到 muted 集合,保證只有一個 batch 在傳送。
  3. 從正在執行傳送資料集合 inflightBatches 中獲取過期集合。
  4. 從 accumulator 累加器的 batches 中獲取過期集合。
  5. 從 inflightBatches 與 batches 中查詢已過期的訊息批次(ProducerBatch),判斷批次是否過期是根據系統當前時間與 ProducerBatch 建立時間之差是否超過120s,過期時間可以透過引數 delivery.timeout.ms 設定。
  6. 處理已過期的訊息批次,通知該批訊息傳送失敗並返回給客戶端。
  7. 傳送訊息暫存到 NetworkClient send 欄位裡。

從上面原始碼可以看出,SendProducerData 方法中呼叫到了 Sender 執行緒類中多個方法,這裡就不一一講解了,感興趣的請移步到星球中檢視完整內容

03 Sender 傳送流程分析

透過前兩部分的原始碼解讀和剖析,我們可以得出 Sender 執行緒的處理流程可以分為兩大部分:「傳送請求」、「接收響應結果」。

03.1 傳送請求

從 runOnce 方法可以得出傳送請求也分兩步:「訊息預傳送」、「真正的網路傳送」。

  void runOnce() {
        // 1. 把訊息快取在 KafkaChannel 的 Send 欄位裡。
        long pollTimeout = sendProducerData(currentTimeMs);
        // 2. 讀取訊息實現真正的網路傳送
        client.poll(pollTimeout, currentTimeMs);
}

03.2 接收響應結果

等 Sender 執行緒收到 Broker 端的響應結果後,會根據響應結果分情況進行處理。

03.3 時序圖

原文完整版在星球裡面,如果感興趣可以掃文末二維碼加入

04 總結

這裡,我們一起來總結一下這篇文章的重點。

1、開篇總述訊息被暫存到 Deque<ProducerBatch> 的 batches 中,等「批次已滿」或者「有新批次被建立」後,喚醒 Sender 子執行緒將訊息批次傳送給 Kafka Broker 端,從而引出「Sender 執行緒」。

2、帶你深度剖析了「Sender 執行緒」 的傳送訊息以及響應處理的相關方法。

3、最後帶你串聯了整個訊息傳送的流程,讓你有個更好的整體認知。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70024420/viewspace-2936420/,如需轉載,請註明出處,否則將追究法律責任。

相關文章