集群元数据是生产者的一个重要字段,这些信息记录了某个topic有哪几个分区,每个分区的Leader副本在哪个节点上,Follower副本在哪些节点上,ISR集合,以及这些节点的ip地址、端口。
public final class Metadata {
// metadata 更新失败时,为避免频繁更新 meta,最小的间隔时间,默认 100ms
private final long refreshBackoffMs;
// metadata 的过期时间, 默认 60,000ms,即每隔多久更新一次
private final long metadataExpireMs;
// 每更新成功1次,version自增1,主要是用于判断 metadata 是否更新
private int version;
// 最近一次更新时的时间(包含更新失败的情况)
private long lastRefreshMs;
// 最近一次成功更新的时间(如果每次都成功的话,与前面的值相等, 否则,lastSuccessulRefreshMs < lastRefreshMs)
private long lastSuccessfulRefreshMs;
// 集群中一些 topic 的信息,这些信息记录了某个topic有哪几个分区,每个分区的Leader副本在哪个节点上
// Follower副本在哪些节点上,ISR集合,以及这些节点的ip地址、端口
private Cluster cluster;
// 是否需要强制更新 metadata,这个是出发Sender线程更新集群元数据的条件之一
private boolean needUpdate;
// 监听Metadata更新的监听器集合,实现Metadata.Listener.onMetadataUpdate()方法,在更新Metadata中的cluster字段之前,统治Listener集合中全部的Listener对象。
private final List<Listener> listeners;
// 是否需要更新全部Topic的元数据,一般情况下,KafkaProducer只维护它用到的topic的元数据。
private boolean needMetadataForAllTopics;
//requestUpdate()的方法把needUpdate字段修改为true,这样Sender线程就会更新元数据信息,然后返回version的值。
public synchronized int requestUpdate() {
this.needUpdate = true;
return this.version;
}
/**
* Wait for metadata update until the current version is larger than the last version we know of
*/
//Metadata字段可以由主线程去读,Sender线程来更新,所以一定要是线程安全的,加上synchronized同步。
public synchronized void awaitUpdate(final int lastVersion, final long maxWaitMs) throws InterruptedException {
if (maxWaitMs < 0) {
throw new IllegalArgumentException("Max time to wait for metadata updates should not be < 0 milli seconds");
}
long begin = System.currentTimeMillis();
long remainingWaitMs = maxWaitMs;
while (this.version <= lastVersion) {
//与Sender通过wait/notify同步,更新元数据的操作,给Sender线程完成。
if (remainingWaitMs != 0)
wait(remainingWaitMs);
long elapsed = System.currentTimeMillis() - begin;
if (elapsed >= maxWaitMs)
throw new TimeoutException("Failed to update metadata after " + maxWaitMs + " ms.");
remainingWaitMs = maxWaitMs - elapsed;
}
}
}
Producer 在调用 dosend()
方法时,第一步就是通过 waitOnMetadata
方法获取该 topic 的 metadata 信息。
public class KafkaProducer<K, V> implements Producer<K, V> {
private long waitOnMetadata(String topic, long maxWaitMs) throws InterruptedException {
// 检查metadata集合中是否已经包含了制定的topic
if (!this.metadata.containsTopic(topic))
this.metadata.add(topic);
// 若从Cluster中获得分区的详细信息,则直接返回。
if (metadata.fetch().partitionsForTopic(topic) != null)
return 0;
long begin = time.milliseconds();
long remainingWaitMs = maxWaitMs;
while (metadata.fetch().partitionsForTopic(topic) == null) {
log.trace("Requesting metadata update for topic {}.", topic);
//设置needupdata,获取当前元数据的版本号。
int version = metadata.requestUpdate();
//唤醒 sender 线程,间接唤醒 NetworkClient 线程,NetworkClient 线程来负责发送 Metadata 请求,并处理 Server 端的响应。
sender.wakeup();
//阻塞等待元数据更新完成
metadata.awaitUpdate(version, remainingWaitMs);
long elapsed = time.milliseconds() - begin;
//监测超时时间
if (elapsed >= maxWaitMs)
throw new TimeoutException("Failed to update metadata after " + maxWaitMs + " ms.");
//检查权限,若认证失败,对当前 topic 没有 Write 权限
if (metadata.fetch().unauthorizedTopics().contains(topic))
throw new TopicAuthorizationException(topic);
remainingWaitMs = maxWaitMs - elapsed;
}
return time.milliseconds() - begin;
}
}
实际发送metadata请求是在NetworkClient中实现的:
public class NetworkClient implements KafkaClient {
public long maybeUpdate(long now) {
// 根据refreshBackoffMs和metadataExpireMs判断是否需要更新
long timeToNextMetadataUpdate = metadata.timeToNextUpdate(now);
long timeToNextReconnectAttempt = Math.max(this.lastNoNodeAvailableMs + metadata.refreshBackoff() - now, 0);
long waitForMetadataFetch = this.metadataFetchInProgress ? Integer.MAX_VALUE : 0;
// if there is no node available to connect, back off refreshing metadata
long metadataTimeout = Math.max(Math.max(timeToNextMetadataUpdate, timeToNextReconnectAttempt),
waitForMetadataFetch);
// 时间已到时,进行更新操作
if (metadataTimeout == 0) {
// 选择一个请求最少,并且链接状态可用的host,作为获取metadata的host
//主要从各个inFlightRequests字段(该字段记录已经发送出去但是没有收到响应的请求),找到最少该请求的对应节点。
Node node = leastLoadedNode(now);
//
maybeUpdate(now, node);
}
// 时间未到时,直接返回下次应该更新的时间
return metadataTimeout;
}
private void maybeUpdate(long now, Node node) {
if (node == null) {
log.debug("Give up sending metadata request since no node is available");
// mark the timestamp for no node available to connect
this.lastNoNodeAvailableMs = now;
return;
}
String nodeConnectionId = node.idString();
// 通道已经 ready 并且支持发送更多的请求:
// return connectionStates.isConnected(node) && selector.isChannelReady(node) && inFlightRequests.canSendMore(node);
if (canSendRequest(nodeConnectionId)) {
// 准备开始发送数据,将 metadataFetchInProgress 置为 true
this.metadataFetchInProgress = true;
// 创建 metadata 请求,
MetadataRequest metadataRequest;
if (metadata.needMetadataForAllTopics())
// 强制更新所有 topic 的 metadata(虽然默认不会更新所有 topic 的 metadata 信息,但是每个 Broker 会保存所有 topic 的 meta 信息)
metadataRequest = MetadataRequest.allTopics();
else
// 只更新 metadata 中的 topics 列表(列表中的 topics 由 metadata.add() 得到)
metadataRequest = new MetadataRequest(new ArrayList<>(metadata.topics()));
ClientRequest clientRequest = request(now, nodeConnectionId, metadataRequest);
log.debug("Sending metadata request {} to node {}", metadataRequest, node.id());
doSend(clientRequest, now);
} else if (connectionStates.canConnect(nodeConnectionId, now)) {
// 如果没有连接这个 node,且这个node是可连接的,那就初始化连接
log.debug("Initialize connection to node {} for sending metadata request", node.id());
initiateConnect(node, now);
// 如果连接失败,允许马上连接另一个node
} else { // 已经连接,但是不能发送请求,如inFlightRequests.canSendMore(node) == false则等待。
this.lastNoNodeAvailableMs = now;
}
}
}
在KafkaProducer.send中,会进行metadata元数据的更新。然后通过handleCompletedReceives(responses, updatedNow) 方法,它会处理 Server 端返回的 Metadata 结果。
class DefaultMetadataUpdater implements MetadataUpdater {
public boolean maybeHandleCompletedReceive(ClientRequest req, long now, Struct body) {
short apiKey = req.request().header().apiKey();
if (apiKey == ApiKeys.METADATA.id && req.isInitiatedByNetworkClient()) {
handleResponse(req.request().header(), body, now);
return true;
}
return false;
}
private void handleResponse(RequestHeader header, Struct body, long now) {
this.metadataFetchInProgress = false;
MetadataResponse response = new MetadataResponse(body);
Cluster cluster = response.cluster();
// check if any topics metadata failed to get updated
Map<String, Errors> errors = response.errors();
if (!errors.isEmpty())
log.warn("Error while fetching metadata with correlation id {} : {}", header.correlationId(), errors);
// 当没有节点存活时,则放弃更新metadata。
if (cluster.nodes().size() > 0) {
this.metadata.update(cluster, now);
} else {
log.trace("Ignoring empty metadata response with correlation id {}.", header.correlationId());
this.metadata.failedUpdate(now);
}
}
/**
* Update the cluster metadata
*/
public synchronized void update(Cluster cluster, long now) {
//设置更新时间,version
this.needUpdate = false;
this.lastRefreshMs = now;
this.lastSuccessfulRefreshMs = now;
this.version += 1;
//回调自定义的onMetadataUpdate函数。
for (Listener listener: listeners)
listener.onMetadataUpdate(cluster);
// 如果是
this.cluster = this.needMetadataForAllTopics ? getClusterForCurrentTopics(cluster) : cluster;
notifyAll();
log.debug("Updated cluster metadata version {} to {}", this.version, this.cluster);
}
}
Producer Metadata 的更新策略
Metadata 会在下面两种情况下进行更新
- KafkaProducer 第一次发送消息时强制更新,其他时间周期性更新,它会通过 Metadata 的 lastRefreshMs, lastSuccessfulRefreshMs 这2个字段来实现;
- 强制更新: 调用 Metadata.requestUpdate() 将 needUpdate 置成了 true 来强制更新。
在 NetworkClient 的 poll() 方法调用时,就会去检查这两种更新机制,只要达到其中一种,就行触发更新操作。
Metadata 的强制更新会在以下几种情况下进行:
- initConnect 方法调用时,初始化连接;
- poll() 方法中对 handleDisconnections() 方法调用来处理连接断开的情况,这时会触发强制更新;
- poll() 方法中对 handleTimedOutRequests() 来处理请求超时时;
- 发送消息时,如果无法找到 partition 的 leader;
- 处理 Producer 响应(handleProduceResponse),如果返回关于 Metadata 过期的异常,比如:没有 topic-partition 的相关 meta 或者 client 没有权限获取其 metadata。