kafka源码解析(2)生产者解析之元数据初始化和拉取全流程

元数据相关

文章结构如下

  • 1.整体流程图
  • 2.元数据初始化
  • 3.整体流程图每一步解释

参考

1.整体流程图

在这里插入图片描述

2.元数据初始化

用properties创建的生产者一开始的元数据是空,下面会分析元数据的初始化以及更新流程
在这里插入图片描述

2.1 Metadata类

生产者会在初始化后生成Metadata类,里面重要的属性和方法如下

属性意思
metadataExpireMs请求元数据时间间隔
version版本号,元数据每次更新都会自增1
clusterCluster类,存放元数据的真身,详见2.2
needUpdate判别是否需要更新的标识符
topicsMap结构,每个topic过期时间

2.2 Metadata 中用到的Cluster类

这个类封装了集群的各种元信息,比如节点,内置topic列表,集群Controller,以及各种Map,PartitionInfo存放了leader,follower,ISR等信息。搞了这么多花里胡哨的感觉较为冗余,目前还不知道有何用处,后面看到会返回来修改这段文字。

//分区(topic,分区号),分区详细信息
private final Map<TopicPartition, PartitionInfo> partitionsByTopicPartition;
//topic名,分区详细信息
private final Map<String, List<PartitionInfo>> partitionsByTopic;
//topic名,可用分区详细信息
private final Map<String, List<PartitionInfo>> availablePartitionsByTopic;
//节点,可用分区详细信息
private final Map<Integer, List<PartitionInfo>> partitionsByNode;

2.3 Metadata 的updata方法

可以看到如果构建Metadata实例时候传入的 topicExpiryEnabled(topic是否会过期)是true时候,要运行如下逻辑。如果TOPIC_EXPIRY_NEEDS_UPDATE设置了过期时间,就会从topic中删除。 只不过目前还没拉取元数据呢topic是空,这个循环不走。

for (Iterator<Map.Entry<String, Long>> it = topics.entrySet().iterator(); it.hasNext(); ) {
                Map.Entry<String, Long> entry = it.next();
                long expireMs = entry.getValue();
                if (expireMs == TOPIC_EXPIRY_NEEDS_UPDATE)
                    entry.setValue(now + TOPIC_EXPIRY_MS);
                else if (expireMs <= now) {
                    it.remove();
                    log.debug("Removing unused topic {} from the metadata list, expiryMs {} now {}", entry.getKey(), expireMs, now);
                }
            }
        }

最后,this.cluster = newCluster。相当于update把仅仅包含broker地址的崭新Cluster实例塞入 Metadata, Metadata初始化完成。

this.metadata.update(Cluster.bootstrap(addresses), Collections.emptySet(), time.milliseconds());

3. 整体流程图每一步解释

step① 主线程发送消息

生产者需要知道kafka集群元数据才能发送出去,元数据是生产者的send方法获取的。里面先要等待拉取元数据操作

clusterAndWaitTime = waitOnMetadata(record.topic(), record.partition(), maxBlockTimeMs);

生产者的doSend方法中,有一个ClusterAndWaitTime类,包含了Cluster类(真正的元数据)和等待时间

private static class ClusterAndWaitTime {
        final Cluster cluster;
        final long waitedOnMetadataMs;
        ClusterAndWaitTime(Cluster cluster, long waitedOnMetadataMs) {
            this.cluster = cluster;
            this.waitedOnMetadataMs = waitedOnMetadataMs;
        }
    }

clusterAndWaitTime实例是通过waitOnMetadata来拉取数据的,给主题,分区,预设最大等待时间

clusterAndWaitTime = waitOnMetadata(record.topic(), record.partition(), maxBlockTimeMs);

在深入看拉元数据的内部方法之前,将send方法后面流程走一下。后面一系列操作依次是Key,Value序列化,判断KV序列化后的大小,拦截器链,消息注入RecordAccumulator,当RecordAccumulator中的队列满了或者创建新批次就唤醒sender线程。

step② waitOnMetadata方法:看看缓存里面有没有元数据

Cluster cluster = metadata.fetch();

if (cluster.invalidTopics().contains(topic))
	throw new InvalidTopicException(topic);

metadata.add(topic);

Integer partitionsCount = cluster.partitionCountForTopic(topic);

if (partitionsCount != null && (partition == null || partition < partitionsCount))
	return new ClusterAndWaitTime(cluster, 0);

本方法入参是 topic 主题名,partition分区。先获取元数据中的cluster属性,判断下是否是未授权topic,再调用metadata.add(topic),add方法里面判断如果没有topic就把metadata的needUpdate属性设置为true。partitionsCount是cluster中partitionsByTopic里面分区数量。现在刚刚初始化,那就是null。缓存中有,就把这个cluster封装ClusterAndWaitTime返回。

step③④ 更新标志位,唤醒sender线程

 do {
            log.trace("Requesting metadata update for topic {}.", topic);
            metadata.add(topic);
            int version = metadata.requestUpdate();
            sender.wakeup();
            try {
                metadata.awaitUpdate(version, remainingWaitMs);
            } catch (TimeoutException ex) {
                // Rethrow with original maxWaitMs to prevent logging exception with remainingWaitMs
                throw new TimeoutException("Failed to update metadata after " + maxWaitMs + " ms.");
            }
            cluster = metadata.fetch();
            elapsed = time.milliseconds() - begin;
            if (elapsed >= maxWaitMs)
                throw new TimeoutException("Failed to update metadata after " + maxWaitMs + " ms.");
            if (cluster.unauthorizedTopics().contains(topic))
                throw new TopicAuthorizationException(topic);
            if (cluster.invalidTopics().contains(topic))
                throw new InvalidTopicException(topic);
            remainingWaitMs = maxWaitMs - elapsed;
            partitionsCount = cluster.partitionCountForTopic(topic);
        } while (partitionsCount == null);

metadata.add(topic)这步里面将needUpdate标为true,然后唤醒sender线程。在下面这个循环中,一直尝试调用metadata.awaitUpdate方法,直到cluster中该topic存在或者超时。remainingWaitMs(还剩多少可用时间)从maxWaitMs开始每次拉取减去当此耗时elapsed。当elapsed大于maxWaitMs或者remainingWaitMs小于0就会抛出异常。当发现失效或者未授权topic也会抛异常。

step⑤ 请求topic元数据

awaitUpdate这一步乍一看没有元数据更新的环节,确实如此,metadata不干活,只是通过自身的version属性来判断sender线程是否更新了元数据。

        while ((this.version <= lastVersion) && !isClosed()) {
            AuthenticationException ex = getAndClearAuthenticationException();
            if (ex != null)
                throw ex;
            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;
        }

如果还有剩余时间,就阻塞线程,wait(remainingWaitMs),等待元数据。

------------------------------->>>-------------现在进入sender剧情线-------------------------------------------------

下面就是sender去拉取数据了。还记得生产者起的KafkaThread传入了sender嘛,Thread类的start方法就是在新线程运行传入的Runnale类的run方法。

this.ioThread = new KafkaThread(ioThreadName, this.sender, true);
this.ioThread.start();

再转入sender的run方法
先不去管事物代理部分,后面两句是关键

long pollTimeout = sendProducerData(now);
client.poll(pollTimeout, now);

------------------------------->>>-------------现在进入client.poll方法------------------------------------------------

sendProducerData方法主要是用来发送真正数据的,目前cluster对象里面还没进去信息,所以不会有什么作用。真正发挥作用的是poll方法中的maybeUpdate

long metadataTimeout = metadataUpdater.maybeUpdate(now); 

-------------------------------->>>-----------现在进入maybeUpdate方法------------------------------------------

metadataUpdater 封装topic =>metadataRequest

maybeUpdate 拿到其中一个node后,进入maybeUpdate(now, node),判断node网络连接可用后,将topic封装进metadataRequest请求

//metadataUpdater 封装topic =>metadataRequest
metadataRequest = new MetadataRequest.Builder(new ArrayList<>(metadata.topics()),
                            metadata.allowAutoTopicCreation());
sendInternalMetadataRequest(metadataRequest, nodeConnectionId, now);

然后
networkClient 封装sendInternalMetadataRequest => clientRequest

//networkClient封装sendInternalMetadataRequest => clientRequest
//builder : MetadataRequest.Builder
ClientRequest clientRequest = newClientRequest(nodeConnectionId, builder, now, true);

然后
networkClient 封装clientRequest =>InFlightRequest
networkClient里面,把inFlightRequest(包含了metadataRequest,node封装成的请求头信息等等)加进inFlightRequest队列(下图7号)

//networkClient 封装clientRequest =>InFlightRequest
InFlightRequest inFlightRequest = new InFlightRequest(
                clientRequest,
                header,
                isInternalRequest,
                request,
                send,
                now);
this.inFlightRequests.add(inFlightRequest);
selector.send(send);

省略若干

channel.setSend(send);

在这里插入图片描述

至此,networkClient客户端缓存了拉取该topic元数据的请求,另外将地址等信息封装成的send存在KafkaChannel里面,具体就不再追进去了,后面有机会再看。

-----------------------------------------------终于跳出 maybeUpdate--------------------->>>-----------------------

networkClient的poll方法继续运行,selector的poll方法通过复杂网络操作将请求发给集群。具体发送机制会在后面网络解析进行讲解。

this.selector.poll(Utils.min(timeout, metadataTimeout, defaultRequestTimeoutMs));

【这里是一个链接占位符:kafka网络传输专题,解释信息怎么传回来的】

step⑥ 服务端返回响应

后续就是对selector的completedReceives(服务端返回的数据)进行处理

可以看到遍历服务端返回的数据,得到一个个的receive被封装成AbstractResponse,然后判断是不是元数据

private void handleCompletedReceives(List<ClientResponse> responses, long now) {
        for (NetworkReceive receive : this.selector.completedReceives()) {
            String source = receive.source();
            InFlightRequest req = inFlightRequests.completeNext(source);
            Struct responseStruct = parseStructMaybeUpdateThrottleTimeMetrics(receive.payload(), req.header,
                throttleTimeSensor, now);
            if (log.isTraceEnabled()) {
                log.trace("Completed receive from node {} for {} with correlation id {}, received {}", req.destination,
                    req.header.apiKey(), req.header.correlationId(), responseStruct);
            }
            // If the received response includes a throttle delay, throttle the connection.
            AbstractResponse body = AbstractResponse.parseResponse(req.header.apiKey(), responseStruct);
            maybeThrottle(body, req.header.apiVersion(), req.destination, now);
            if (req.isInternalRequest && body instanceof MetadataResponse)
                metadataUpdater.handleCompletedMetadataResponse(req.header, now, (MetadataResponse) body);
            else if (req.isInternalRequest && body instanceof ApiVersionsResponse)
                handleApiVersionsResponse(responses, req, now, (ApiVersionsResponse) body);
            else
                responses.add(req.completed(body, now));
        }
    }

这里面是元数据,因此走这个分支

metadataUpdater.handleCompletedMetadataResponse(req.header, now, (MetadataResponse) body);

step⑦ ⑧ 更新本地元数据缓存,唤醒主线程

handleCompletedMetadataResponse如下,response.cluster()已经包含了cluster的所有信息,如果返回的node节点数量大于0,说明服务端在工作,继续调用metadata.update方法。

public void handleCompletedMetadataResponse(RequestHeader requestHeader, long now, MetadataResponse response) {
            this.metadataFetchInProgress = false;
            Cluster cluster = response.cluster();
            //中间省略无关代码
            if (cluster.nodes().size() > 0) {
                this.metadata.update(cluster, response.unavailableTopics(), now);
            } else {
                log.trace("Ignoring empty metadata response with correlation id {}.", requestHeader.correlationId());
                this.metadata.failedUpdate(now, null);
            }
        }

还记得metadata类吗?前面在阻塞wait中了。所以update方法一上来version += 1让metadata从那个判断版本号的循环解放出来。updata里面的这步就是把response里面的cluster传给metadata进行更新,并叫醒metadata主线程(如果他还没来得及去判断version版本)

this.cluster = newCluster;
notifyAll();

--------------------------------------------终于跳出client.poll方法---------------------------->>>------------------
-------------------------------------------终于跳出sender剧情线------------------------------>>>-------------------

step⑨ awaitUpdate方法继续

被叫醒了就继续运行,算出时间

remainingWaitMs = maxWaitMs - elapsed;

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值