上一篇博文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
指Kafka集羣中所有node中負載最小的那一個node,它是由每個node再InFlightRequests
中還未確定的請求數決定的,未確定的請求越少則負載越小。如上圖所示,node1即爲LeastLoadedNode
。
更新metadata
當客戶端中沒有需要使用的元數據信息時,比如沒有指定的主題信息或者超過了rnetadata .rnax.age.rns
配置的時間還沒有更新元數據就會進行元數據的強制更新。
元數據的更新操作是在客戶端內部進行的,對客戶端的外部使用者不可見。當需要更新元數據時,會先挑選出LeastLoadedNode
,然後向這個node發送MetadataRequest
來獲取具體的元數據信息。
創建完成MetadataRequest
後,該請求也會放入InFlightRequests
中,因此更新元數據與發送消息一樣都是由Sender線程負責的,但是主線程也會讀取元數據信息,因此這些操作都會通過synchronized
和final
來保證數據一致性。
源碼分析
上一篇博文中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的信息,該循環中主要做了一下事情:
- 調用
metadata.requestUpdate()
方法來獲取updateVersion,即上一次更新成功時的version,並將needUpdate設爲true,強制更新; - 調用
sender.wakeup()
方法來喚醒Sender線程,Sender線程中又會喚醒NetworkClient線程,在NetworkClient中會對UpdateMetadataRequest
請求進行操作,待會下面會詳細介紹; - 調用
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()
方法,該方法主要做了一下幾件事:
- 判斷當前節點是否還可以發送請求,如果可以則構建
MetadataRequest
對象, 更新metadata中所有的topic; - 如果不能發送請求,則判斷是否有節點正在創建或者當前節點是否還可以創建連接,這兩種情況都會返回創建連接所需的時間;
該博文的源碼是基於Kafka 2.3.0