【kafka】kafka Producer Metadata概述及源码分析

394 篇文章 628 订阅 ¥99.90 ¥99.00

在这里插入图片描述

1.概述

转载:Kafka源码阅读(二):Producer Metadata概述及源码分析

2.Metadata

什么是metadata?

metadata指Kafka集群的元数据,包含了Kafka集群的各种信息,例如如:

  1. 集群中有哪些节点;
  2. 集群中有哪些topic,这些topic有哪些partition;
  3. 每个partition的leader副本分配在哪个节点上,follower副本分配在哪些节点上;
  4. 每个partition的AR有哪些副本,ISR有哪些副本;

metadata应用场景

metadata在Kafka中无疑是非常重要的,很多场景中都需要从metadata中获取数据或更新数据,例如:

  1. KafkaProducer发送一条消息到指定的topic中,需要知道分区的数量,要发送的目标分区,目标分区的leader,leader所在的节点地址等,这些信息都要从metadata中获取。
  2. 当Kafka集群中发生了leader选举,节点中partition或副本发生了变化等,这些场景都需要更新metadata中的数据。

2.1 LeastLoadedNode

在这里插入图片描述

LeastLoadedNode指Kafka集群中所有node中负载最小的那一个node,它是由每个node再InFlightRequests中还未确定的请求数决定的,未确定的请求越少则负载越小。如上图所示,node1即为LeastLoadedNode。

3.更新metadata

当客户端中没有需要使用的元数据信息时,比如没有指定的主题信息或者超过了rnetadata .rnax.age.rns配置的时间还没有更新元数据就会进行元数据的强制更新。

元数据的更新操作是在客户端内部进行的,对客户端的外部使用者不可见。当需要更新元数据时,会先挑选出LeastLoadedNode,然后向这个node发送MetadataRequest来获取具体的元数据信息。

创建完成MetadataRequest后,该请求也会放入InFlightRequests中,因此更新元数据与发送消息一样都是由Sender线程负责的,但是主线程也会读取元数据信息,因此这些操作都会通过synchronized和final来保证数据一致性。

4.源码分析

上一篇博文中KafkaProducer发送消息的doSend()方法中调用了waitOnMetadata()方法来等待更新元数据,那么Kafka是如何等待更新元数据的呢?接下来就让我们通过阅读源码来分析一下这其中的一些细节。在开始分析源码之前我们先看下Cluster对象和Metadata对象中的主要属性,以便更好的理解代码。

4.1 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
}

4.2 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
}

4.3 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来判断是否更新成功;

4.4 NetworkClient.java

上面提到过需要更新metadata时会调用sender.wakeup()方法来唤醒Sender线程,Sender线程中又会唤醒NetworkClient线程,在NetworkClient中会对UpdateMetadataRequest请求进行操作,在NetworkClient中真正处理请求的是NetworkClient.poll()方法,接下来让我们通过分析源码来看下NetworkClient是如何处理请求的。

4.4.1 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;
    }

4.4.2 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;
            }

总结一下上面几个方法所做的事情:

  1. 首先计算下次更新metadata的时间,如果大于0说明需要等待,否则继续执行更新操作;
  2. 获取最小负载节点,如果没有则返回等待创建连接所需时间;
  3. 调用重载的mayUpdate()方法,该方法主要做了一下几件事:
    • 判断当前节点是否还可以发送请求,如果可以则构建MetadataRequest对象, 更新metadata中所有的topic;
    • 如果不能发送请求,则判断是否有节点正在创建或者当前节点是否还可以创建连接,这两种情况都会返回创建连接所需的时间;

该博文的源码是基于Kafka 2.3.0

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值