
图解 Kafka 生产者元数据拉取管理全流程(下)
02.3 初探 cluster
github 源码地址如下:
https://github.com/apache/kafka/blob/2.7/clients/src/main/java/org/apache/kafka/common/Cluster.java
https://github.com/apache/kafka/blob/2.7/clients/src/main/java/org/apache/kafka/common/Node.java
https://github.com/apache/kafka/blob/2.7/clients/src/main/java/org/apache/kafka/common/PartitionInfo.java
https://github.com/apache/kafka/blob/2.7/clients/src/main/java/org/apache/kafka/common/TopicPartition.java
Cluster类基本保存了所有的kafka集群相关的信息。
Node broker节点的相关的信息
PartitionInfo 分区信息
TopicPartition
每个topic和每个分区组成的唯一索引,代表一个分区标识。
综上映射关系可以看出,kafka 是以「分区」为最小管理单元,然后分区中的 Leader 负责交互。「Cluster」代表整个kafka集群的实体类,「MetaData」的角色相当于「Cluster」的在客户端的一个维护者。
02.4 主线程加载元数据
我们先来看加载元数据大体过程。
接下来分析详细源码,首先客户端可以直接调用「 producer.send() 」进行发送,底层调用 doSend() 方法。
从上述发送的源码中,可以看出来主线程在发送消息前需要先获取元数据,这样才能知道消息要发送到哪些节点。
中间通过调用 waitOnMetadata() 获取元数据的相关信息,为后续发送消息提供支持,接下来我们重点分析下 waitOnMetadata() 这个方法。
该方法用来获取元数据以及元数据消耗时间,我们具体分析下主流程的逻辑:
1. 从元数据缓存中获取元数据,如果metadata不存在当前topic的元数据,会触发一次强制刷新,metaData中的needUpdate置为true。
2. 判断该主题是否是无效的主题, 如果是抛异常。
3. 将该主题放入元数据的主题列表中,通过 Sender 线程定时更新这些主题的元数据。
4. 从元数据缓存中获取主题对应的分区数。
5. 判断元数据缓存是否能满足需要,就是说要能够找到要发送消息的主题分区,条件是:partitionsCount != null && (partition == null || partition < partitionsCount) 即「主题对应的分区数不能为空且发送的分区ID要小于主题的分区数」。
6. 不停的轮询唤醒 sender 线程更新元数据,这里有两个条件要满足其一才可以。
● partitionsCount == null 「从元数据缓存中获取主题对应的分区数为空」,这里要发送的主题连Leader分区都没有,可能主题分区根本不存在也可能没拉取到最新分区信息,如果真的不存在就没必要继续直接抛异常。
● partition != null && partition >= partitionsCount「分区不为空且要发送的分区ID大于主题分区数」,这里说明了主题分区的数量增加了,需要重新拉取下获取最新分区信息。
7. 将主题 topic及过期时间添加到元数据主题列表中
8. 标记元数据更新标识,并获取当前元数据版本号,提醒 Sender 线程更新元数据。
9. 唤醒 sender 子线程进行元数据更新以及消息发送。
10. 阻塞主线程并等待Sender线程更新元数据成功。
11. 待 「Sender 子线程更新元数据成功」或者「阻塞超时解除阻塞」,再次获取元数据。
12. 计算等待更新完成元数据消耗时间。
13. 如果超时,抛超时异常。maxWaitMs 最大1分钟。
14. 获取元数据分区数。
15. 返回元数据以及消耗时间。
聪明的读者可能发现在生产者中获取元数据都是基于topic的,主要原因就是对于生产者来说,没必要拉取全部的元数据,只拉取自己需要的主题元数据就可以了。
至此,元数据加载已经分析完了, 接下来我们看下 Sender 子线程是如何拉取元数据的。
03 Sender 线程拉取元数据
由于 Sender 线程的深度分析不是本文的重点,这里先简单的给大家带一带,后续会有专门篇章去分析。
github 源码地址如下:
https://github.com/apache/kafka/blob/2.7/clients/src/main/java/org/apache/kafka/clients/producer/internals/Sender.java
https://github.com/apache/kafka/blob/2.7/clients/src/main/java/org/apache/kafka/clients/NetworkClient.java
我们来看看 Sender 线程拉取元数据的大体过程。
接下来我们详细分析具体的源码。
03.1 Sender 周期执行
通过源码可以得知 Sender 是一个 Runnable 对象,那整个 Sender 线程执行的核心逻辑就在 run() 方法中,run() 方法中的第一段代码就是循环调用 runOnce() 方法:
上述源码中的 runOnce() 方法是 Sender 线程一个执行的周期,在这个周期中会进行一次批量的请求发送,并进行一次响应的处理。
接下来我们看里面2个涉及到元数据的重要方法:
03.1.1 sendProducerData()
该方法用来进行 Sender 线程创建请求的核心,这里只分析下元数据涉及的部分:
1. 从元数据缓存中获取元数据。
2. 通过元数据cluster获取要发送的节点 Leader 分区信息。
3. 如果主题的 Leader 分区对应的节点不存在就强制更新元数据。
● 循环无 Leader 分区的主题,将主题加入元数据主题列表。
● 强制标记元数据更新标识。
● 对于无 Leader 分区的主题,可能分区正在选主中,也可能 Leader 分区所在节点 Crash,所以要强制更新元数据保证元数据一致性。
03.1.2 client.poll()
该方法用来发送网络IO请求,在请求之前先执行尝试更新元数据的请求。
03.2 元数据更新触发时机
03.2.1 metadataUpdater.maybeUpdate()
从初始化函数中可以得出元数据更新组件是调用 NetworkClient 内部类即 DefaultMetadataUpdater,前面已多次提到更新集群元数据的场景,而这些更新操作都是在标记集群元数据是否需要更新,而真正执行更新的操作是这里。接下来我们看下这个类的 maybeUpdate() 方法。
03.2.2 public maybeUpdate()
从这段源码中可以看出上来先计算下次要更新元数据的时间,我们看下这个计算过程。
1. 元数据是否过期 timeToExpire,计算方式:
● 首先会通过 updateRequested() 方法检查 Metadata 中的 「needFullUpdate」「needPartialUpdate」,如果这两个标识位为 true,表示 Metadata 需要立即更新,
● 否则计算上次更新成功的时间距离当前时间是否已经超过了指定的元数据过期时间阈值metadataExpireMs「默认5分钟」。
2. 允许更新的时间点 timeToAllowUpdate,计算方式:
● 上次更新时间 + 退避时间 - 当前时间的间隔 「要求上次更新时间与当前时间的间隔不能大于退避时间,如果大于则需要等待」。
3. 最后计算这俩值的最大值作为下次更新元数据的时间。
分析完元数据更新时机,最后调用 maybeUpdate(now, node) 发送更新元数据的请求。
03.2.3 private maybeUpdate()
该方法用来发送更新元数据的请求,具体的实现逻辑如下:
1. canSendRequest() 判断当前node的状态是否可以发送Request请求, 如果可以发送则构建元数据请求,调用 sendInternalMetadataRequest() 向 nodeConnectionId 发送元数据请求。
2. isAnyNodeConnecting() 如果该 node 正在建立连接,则直接返回重新连接超时时间,等待更新成功。
3. connectionStates.canConnect() 如果存在可用的Node,则尝试初始化连接,返回重新连接超时时间,等待更新成功。
4. 阻塞等待有新的节点可用。
从上述源码可以看出:更新流程一直在重试,直到元数据更新成功为止。
1. Sender 子线程第一次调用 poll() 方法时,尝试初始化与 node 的连接。
2. Sender 子线程第二次调用 poll() 方法时,发送 Metadata 请求。
3. Sender 子线程会阻塞等待一定时间,当有响应返回时则获取 metadataResponse,并更新 metadata。
如果元数据更新成功以后,KafkaProducer 主线程就不会被阻塞,当 NetworkClient 接收到服务端对 Metadata 请求的响应后,就会更新 Metadata 信息, 即 poll() 方法后续的操作。
03.2.4 handleCompletedReceives()
该方法用来判断成功接收服务端返回的响应,根据不同的响应返回做不同的操作,这里只看下跟元数据更新有关的逻辑。
03.2.5 handleSuccessfulResponse()
该方法用来对成功返回信息进行处理,主要是对元数据信息的更新。
1. 查看 response 返回信息的 error。
2. 判断broker信息是否为空
● 如果为空表示没有获得元数据即更新失败了,然后调用 failedUpdate(now)方法记录 lastRefreshMs 为当前时间,不允许立即更新元数据。
● 如果不为空表示获取元数据成功,则调用 MetaData.update() 更新元数据。
至此,Sender 子线程拉取并更新元数据分析完毕。
最后通过一张图来描述整个元数据拉取的全过程:
05 总结
这里,我们一起来总结一下这篇文章的重点。
1、通过「场景驱动」的方式从元数据的使用场景出发,抛出主线程加载元数据和子线程拉取元数据的过程是怎样的?
2、带你梳理了「主线程如何加载元数据源码全貌」,包括 ProducerMetadata 类、Metadata 元数据基类、cluster类的几个重要方法源码分析,最后分析主线程加载元数据过程。
3、又带你梳理了「Sender 线程拉取元数据」,包括 Sender 周期执行、元数据更新触发时机的几个重要方法源码分析。
3、最后通过一张元数据流程图来勾勒出元数据拉取和更新的全貌。
下一篇我们来深度剖析「Kafka NIO实现」,大家期待,我们下期见。
本文转载自公众号捉虫大师
