Kafka生产者介绍(二):Metadata

         集群元数据是生产者的一个重要字段,这些信息记录了某个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 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 implements Producer {
    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 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 会在下面两种情况下进行更新

  1. KafkaProducer 第一次发送消息时强制更新,其他时间周期性更新,它会通过 Metadata 的 lastRefreshMs, lastSuccessfulRefreshMs 这2个字段来实现;
  2. 强制更新: 调用 Metadata.requestUpdate() 将 needUpdate 置成了 true 来强制更新。

在 NetworkClient 的 poll() 方法调用时,就会去检查这两种更新机制,只要达到其中一种,就行触发更新操作。

Metadata 的强制更新会在以下几种情况下进行:

  1. initConnect 方法调用时,初始化连接;
  2. poll() 方法中对 handleDisconnections() 方法调用来处理连接断开的情况,这时会触发强制更新;
  3. poll() 方法中对 handleTimedOutRequests() 来处理请求超时时;
  4. 发送消息时,如果无法找到 partition 的 leader;
  5. 处理 Producer 响应(handleProduceResponse),如果返回关于 Metadata 过期的异常,比如:没有 topic-partition 的相关 meta 或者 client 没有权限获取其 metadata。

你可能感兴趣的:(Kafka)