Kafka源碼閱讀(二):Producer Metadata概述及源碼分析

上一篇博文Kafka源碼閱讀(一):Kafka Producer整體架構概述和源碼分析(一)介紹了Kafka 生產者發送消息的主要流程和計算分區等機制,接下來這篇博文將對Kafka更新Producer Metadata的機制進行講解說明。

Metadata

什麼是metadata

metadata指Kafka集羣的元數據,包含了Kafka集羣的各種信息,例如如:

  • 集羣中有哪些節點;
  • 集羣中有哪些topic,這些topic有哪些partition;
  • 每個partition的leader副本分配在哪個節點上,follower副本分配在哪些節點上;
  • 每個partition的AR有哪些副本,ISR有哪些副本;

metadata應用場景

metadata在Kafka中無疑是非常重要的,很多場景中都需要從metadata中獲取數據或更新數據,例如:

  • KafkaProducer發送一條消息到指定的topic中,需要知道分區的數量,要發送的目標分區,目標分區的leader,leader所在的節點地址等,這些信息都要從metadata中獲取。
  • 當Kafka集羣中發生了leader選舉,節點中partition或副本發生了變化等,這些場景都需要更新metadata中的數據。

LeastLoadedNode

判定leastLoadedNode
LeastLoadedNode指Kafka集羣中所有node中負載最小的那一個node,它是由每個node再InFlightRequests中還未確定的請求數決定的,未確定的請求越少則負載越小。如上圖所示,node1即爲LeastLoadedNode

更新metadata

       當客戶端中沒有需要使用的元數據信息時,比如沒有指定的主題信息或者超過了rnetadata .rnax.age.rns配置的時間還沒有更新元數據就會進行元數據的強制更新。
        元數據的更新操作是在客戶端內部進行的,對客戶端的外部使用者不可見。當需要更新元數據時,會先挑選出LeastLoadedNode,然後向這個node發送MetadataRequest來獲取具體的元數據信息。
        創建完成MetadataRequest後,該請求也會放入InFlightRequests中,因此更新元數據與發送消息一樣都是由Sender線程負責的,但是主線程也會讀取元數據信息,因此這些操作都會通過synchronizedfinal來保證數據一致性。


源碼分析

       上一篇博文中KafkaProducer發送消息的doSend()方法中調用了waitOnMetadata()方法來等待更新元數據,那麼Kafka是如何等待更新元數據的呢?接下來就讓我們通過閱讀源碼來分析一下這其中的一些細節。在開始分析源碼之前我們先看下Cluster對象和Metadata對象中的主要屬性,以便更好的理解代碼。

Metadata.java

// 該Metadata對象會被主線程和Sender線程共享, 當metadata不包含我們所需要的數據時會發送``MetadataRequest``來同步數據。
// ProducerMetadata繼承了Metadata類
public class Metadata implements Closeable {
    private final Logger log;
    private final Map<String, Long> topics = new HashMap<>(); // topic和過期時間的對應關係
    private final long refreshBackoffMs;// retry.backoff.ms: 默認值爲100ms,它用來設定兩次重試之間的時間間隔,避免無效的頻繁重試.
    private final long metadataExpireMs;// metadata.max.age.ms: 默認值爲300000,如果在這個時間內元數據沒有更新的話會被 強制更新.
    private int updateVersion;  // 更新版本號,每更新成功1次,version自增1,主要是用於判斷metadata是否更新
    private int requestVersion; // 請求版本號,沒發送一次請求,version自增1
    private long lastRefreshMs; // 上一次更新的時間(包含更新失敗)
    private long lastSuccessfulRefreshMs; // 上一次更新成功的時間
    private KafkaException fatalException;
    private Set<String> invalidTopics; // 非法的topics
    private Set<String> unauthorizedTopics; // 未認證的topics
    private MetadataCache cache = MetadataCache.empty();
    private boolean needUpdate; 
    private final ClusterResourceListeners clusterResourceListeners; // 會收到metadata updates的Listener列表
    private boolean isClosed;
    private final Map<TopicPartition, Integer> lastSeenLeaderEpochs; // 存儲Partition最近一次的leaderEpoch
}

Cluster.java

// 保存了Kafka集羣中部分nodes、topics和partitions的信息
public final class Cluster {
    private final boolean isBootstrapConfigured;
    private final List<Node> nodes;
    private final Set<String> unauthorizedTopics; // 未認證的topics
    private final Set<String> invalidTopics; // 非法的topics
    private final Set<String> internalTopics; // kafka內置的topics
    private final Node controller;
    private final Map<TopicPartition, PartitionInfo> partitionsByTopicPartition; // partition對應的信息,如:leader所在節點、所有的副本、ISR中的副本、offline的副本
    private final Map<String, List<PartitionInfo>> partitionsByTopic; // topic和partition信息的對應關係
    private final Map<String, List<PartitionInfo>> availablePartitionsByTopic; // topic和可用partition(leader不爲null)的對應關係
    private final Map<Integer, List<PartitionInfo>> partitionsByNode; // node和partition信息的對應關係
    private final Map<Integer, Node> nodesById; //節點id與節點的對應關係
    private final ClusterResource clusterResource; //集羣信息,裏面只有一個clusterId
}

KafkaProducer.java

        瞭解Cluster對象和 Metadata對象的基本信息之後,接下來將正式進入分析代碼階段。

waitOnMetadata()

	// 等待更新集羣的元數據
    private ClusterAndWaitTime waitOnMetadata(String topic, Integer partition, long maxWaitMs) throws InterruptedException {
        // 獲取緩存中的cluster信息
        Cluster cluster = metadata.fetch();

		// 判斷給定的topic在當前集羣中是不是非法的(若果topic的partition沒有leader,則認爲該topic是invalid)
        if (cluster.invalidTopics().contains(topic))
            throw new InvalidTopicException(topic);
		// 將topic添加到metadata的topics列表,並將過期時間重置爲-1; 如果topics列表中不存在當前topic,
		// 則強制更新metadata並將requestVersion加1,同時將lastRefreshMs設爲0,將needUpdate設爲true
        metadata.add(topic);

		// 獲取給定topic的分區數
        Integer partitionsCount = cluster.partitionCountForTopic(topic);
        // 如果從緩存中獲取的cluster中有partition,並且ProducerRecord中沒有指定partition或者ProducerRecord中指定的partition在已知的partition範圍內,則返回緩存中的cluster信息
        if (partitionsCount != null && (partition == null || partition < partitionsCount))
            return new ClusterAndWaitTime(cluster, 0);

        long begin = time.milliseconds();
        long remainingWaitMs = maxWaitMs; // maxWaitMs: 等待更新metadata的最長時間
        long elapsed; // 更新過程中已經消耗的時間
     
        // 一直等待metadata更新,除非metadata中含有我們所需要的topic和partition信息,或者超過最大的等待時間
        do {
            if (partition != null) {
                log.trace("Requesting metadata update for partition {} of topic {}.", partition, topic);
            } else {
                log.trace("Requesting metadata update for topic {}.", topic);
            }
            // 參考上面介紹
            metadata.add(topic);
            // 獲取上一次更新的version,並將needUpdate設爲true,強制更新
            int version = metadata.requestUpdate();
            // 喚醒Sender線程,Sender線程又會喚醒NetworkClient線程,併發送updateMetadataRequest請求
            sender.wakeup(); 
            try {
            	// 一直等待更新metadata,直到當前的updateVersion大於上一次的updateVersion或者timeout(方法內部會不斷的獲取最新的updateVersion)
                metadata.awaitUpdate(version, remainingWaitMs);
            } catch (TimeoutException ex) {
                // Rethrow with original maxWaitMs to prevent logging exception with remainingWaitMs
                throw new TimeoutException(
                        String.format("Topic %s not present in metadata after %d ms.",
                                topic, maxWaitMs));
            }
            // 從緩存中獲取最新的cluster信息
            cluster = metadata.fetch();
            elapsed = time.milliseconds() - begin;
            // 如果等待時間超過設定的最大等待時長,則拋出異常結束等待
            if (elapsed >= maxWaitMs) {
                throw new TimeoutException(partitionsCount == null ?
                        String.format("Topic %s not present in metadata after %d ms.",
                                topic, maxWaitMs) :
                        String.format("Partition %d of topic %s with partition count %d is not present in metadata after %d ms.",
                                partition, topic, partitionsCount, maxWaitMs));
            }
            metadata.maybeThrowExceptionForTopic(topic);
            remainingWaitMs = maxWaitMs - elapsed; // 計算可以等待的剩餘時間
            partitionsCount = cluster.partitionCountForTopic(topic); // 重新獲取partition數
        } while (partitionsCount == null || (partition != null && partition >= partitionsCount));

        return new ClusterAndWaitTime(cluster, elapsed);
    }

總結一下上面這段代碼:

  • 首先會從緩存中獲取cluster信息,並從中獲取partition信息,如果可以取到則返回當前的cluster信息,如果不含有所需要的partition信息時就會更新metadata;
  • 更新metadata的操作會在一個do ....while循環中進行,直到metadata中含有所需partition的信息,該循環中主要做了一下事情:
  1. 調用metadata.requestUpdate()方法來獲取updateVersion,即上一次更新成功時的version,並將needUpdate設爲true,強制更新;
  2. 調用sender.wakeup()方法來喚醒Sender線程,Sender線程中又會喚醒NetworkClient線程,在NetworkClient中會對UpdateMetadataRequest請求進行操作,待會下面會詳細介紹;
  3. 調用metadata.awaitUpdate(version, remainingWaitMs)方法來等待metadata的更新,通過比較當前的updateVersion與步驟1中獲取的updateVersion來判斷是否更新成功;

NetworkClient.java

上面提到過需要更新metadata時會調用sender.wakeup()方法來喚醒Sender線程,Sender線程中又會喚醒NetworkClient線程,在NetworkClient中會對UpdateMetadataRequest請求進行操作,在NetworkClient中真正處理請求的是NetworkClient.poll()方法,接下來讓我們通過分析源碼來看下NetworkClient是如何處理請求的。

poll()

public List<ClientResponse> poll(long timeout, long now) {
		// 判斷當前NetworkClient是否是處於active狀態
        ensureActive();
		// 判斷是否有打斷的響應(比如UnsupportedVersionException),如果有的話立即處理
        if (!abortedSends.isEmpty()) {
            // If there are aborted sends because of unsupported version exceptions or disconnects,
            // handle them immediately without waiting for Selector#poll.
            List<ClientResponse> responses = new ArrayList<>();
            handleAbortedSends(responses);
            completeResponses(responses);
            return responses;
        }
		// 判斷是否需要更新metadata,如果需要則更新,返回值爲可以等待更新的時間,待會下面會詳細介紹
        long metadataTimeout = metadataUpdater.maybeUpdate(now);
        try {
        	// 進行I/O的讀寫操作,這裏先不展開,有機會再詳細介紹
            this.selector.poll(Utils.min(timeout, metadataTimeout, defaultRequestTimeoutMs));
        } catch (IOException e) {
            log.error("Unexpected error during I/O", e);
        }

        // process completed actions
        long updatedNow = this.time.milliseconds();
        List<ClientResponse> responses = new ArrayList<>();
        // 處理已經發送完成的request,如果請求不需要response則將response設爲null
        handleCompletedSends(responses, updatedNow);
        // 處理已經接收完成的response,並根據接收的response更新responses列表,包括metadata的更新
        // 待會下面會詳細介紹
        handleCompletedReceives(responses, updatedNow);
        handleDisconnections(responses, updatedNow); // 內部會觸發強制更新metadata
        handleConnections();
        handleInitiateApiVersionRequests(updatedNow);
        handleTimedOutRequests(responses, updatedNow); // 內部會觸發強制更新metadata
        completeResponses(responses);

        return responses;
    }

mayUpdate()

接下來看一下metadata是如何更新的

public long maybeUpdate(long now) {
            // 獲取下一次更新的時間,如果needUpdate=true,則返回0,即馬上更新;否則返回剩餘的過期時間
            long timeToNextMetadataUpdate = metadata.timeToNextUpdate(now);
            // 計算需要等待的時間,如果有正在處理的請求,則返回默認的請求間隔時間,否則返回0
            long waitForMetadataFetch = hasFetchInProgress() ? defaultRequestTimeoutMs : 0;

            long metadataTimeout = Math.max(timeToNextMetadataUpdate, waitForMetadataFetch);
			// 大於0說明還需等待一段時間才能更新
            if (metadataTimeout > 0) {
                return metadataTimeout;
            }

            //獲取最小負載節點,概述裏已經講的很清楚了,這裏就不在細看.
            Node node = leastLoadedNode(now);
            if (node == null) {
                log.debug("Give up sending metadata request since no node is available");
                return reconnectBackoffMs; // 返回等待創建連接所需時間
            }

            return maybeUpdate(now, node);
        }
        private long maybeUpdate(long now, Node node) {
            String nodeConnectionId = node.idString();
			// 判斷當前node節點是否已經ready,並且支持發送更多請求(即inFlightRequests是否有未處理的request或者給隊列是否達到最大size)
            if (canSendRequest(nodeConnectionId, now)) {
            	// 該請求會更新當前metadata中包含的所有topic
                Metadata.MetadataRequestAndVersion requestAndVersion = metadata.newMetadataRequestAndVersion();
                this.inProgressRequestVersion = requestAndVersion.requestVersion;
                MetadataRequest.Builder metadataRequest = requestAndVersion.requestBuilder;
                log.debug("Sending metadata request {} to node {}", metadataRequest, node);
                // 調用NetworkClient的doSend方法,發送更新metadata請求
                sendInternalMetadataRequest(metadataRequest, nodeConnectionId, now);
                return defaultRequestTimeoutMs;
            }

            // If there's any connection establishment underway, wait until it completes. This prevents
            // the client from unnecessarily connecting to additional nodes while a previous connection
            // attempt has not been completed.
            if (isAnyNodeConnecting()) {
                // Strictly the timeout we should return here is "connect timeout", but as we don't
                // have such application level configuration, using reconnect backoff instead.
                return reconnectBackoffMs;
            }

            if (connectionStates.canConnect(nodeConnectionId, now)) {
                // We don't have a connection to this node right now, make one
                log.debug("Initialize connection to node {} for sending metadata request", node);
                initiateConnect(node, now);
                return reconnectBackoffMs;
            }

總結一下上面幾個方法所做的事情:

  • 首先計算下次更新metadata的時間,如果大於0說明需要等待,否則繼續執行更新操作;
  • 獲取最小負載節點,如果沒有則返回等待創建連接所需時間;
  • 調用重載的mayUpdate()方法,該方法主要做了一下幾件事:
  1. 判斷當前節點是否還可以發送請求,如果可以則構建MetadataRequest對象, 更新metadata中所有的topic;
  2. 如果不能發送請求,則判斷是否有節點正在創建或者當前節點是否還可以創建連接,這兩種情況都會返回創建連接所需的時間;

該博文的源碼是基於Kafka 2.3.0

發佈了117 篇原創文章 · 獲贊 192 · 訪問量 10萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章