元数据相关
文章结构如下
- 1.整体流程图
- 2.元数据初始化
- 3.整体流程图每一步解释
参考
1.整体流程图
2.元数据初始化
用properties创建的生产者一开始的元数据是空,下面会分析元数据的初始化以及更新流程
2.1 Metadata类
生产者会在初始化后生成Metadata类,里面重要的属性和方法如下
属性 | 意思 |
---|---|
metadataExpireMs | 请求元数据时间间隔 |
version | 版本号,元数据每次更新都会自增1 |
cluster | Cluster类,存放元数据的真身,详见2.2 |
needUpdate | 判别是否需要更新的标识符 |
topics | Map结构,每个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;