一、场景分析 在客户端初始化生产者对象时,需要配置 bootstrap.servers 参数,用于获取 Kafka 集群的元数据。这里只需要指定集群中 任意一台 节点即可,原因就是每个 Broker 均存储了集群的所有元数据信息。这篇主要分析 Controller 发送更新元数据请求的场景,以及 Broker更新集群元数据的流程。 二、图示说明
Controller 发送更新元数据请求,Broker 接收请求并更新元数据流程图解
三、源码分析
1.Controller 发送更新元数据请求的场景
在前面几篇关于 Controller 功能的介绍中,多次提到了 Controller 向集群的 Broker 发送更新元数据的请求,如:
①调用 onControllerFailover 方法执行成为 Controller 的逻辑时,向集群中所有运行中的 Broker 发送更新元数据的请求
//向集群中所有运行中的Broker发送更新元数据的请求sendUpdateMetadataRequest(controllerContext.liveOrShuttingDownBrokerIds.toSeq, Set.empty)
②调用 KafkaController.processBrokerChange 方法处理节点数量变更时:
if (newBrokerIds.nonEmpty) { controllerContext.addLiveBrokersAndEpochs(newBrokerAndEpochs) //执行新加入节点启动的逻辑 onBrokerStartup(newBrokerIdsSorted)}
在 onBrokerStartup 方法中,分别向集群现有的所有 Broker 和新加入的 Broker 发送更新元数据的请求
// 第2步:给集群现有Broker发送元数据更新请求,令它们感知到新增加了BrokersendUpdateMetadataRequest(existingBrokers.toSeq, Set.empty)// 第3步:给新增Broker发送元数据更新请求,令它们同步集群当前的所有分区的元数据sendUpdateMetadataRequest(newBrokers, controllerContext.partitionLeadershipInfo.keySet)
③调用 KafkaController.processBrokerModification 方法处理节点信息变更时:
// 第3步:如果从zk获取到的和元数据中的不相等,说明Broker数据发生了变更// 那么,更新元数据缓存,以及执行onBrokerUpdate方法处理Broker更新的逻辑if (newMetadata.endPoints != oldMetadata.endPoints) { info(s"Updated broker metadata: $oldMetadata -> $newMetadata") controllerContext.updateBrokerMetadata(oldMetadata, newMetadata) onBrokerUpdate(brokerId)}
在 onBrokerUpdate 方法中,向集群的所有 Broker 发送更新元数据的请求
// 给集群所有Broker发送UpdateMetadataRequest,让它们去更新元数据sendUpdateMetadataRequest(controllerContext.liveOrShuttingDownBrokerIds.toSeq, Set.empty)
④调用 KafkaController.processTopicChange 方法处理新增主题时:
// 第9步:调整新增主题所有分区以及所属所有副本的运行状态为“在线”状态if (addedPartitionReplicaAssignment.nonEmpty) onNewPartitionCreation(addedPartitionReplicaAssignment.keySet)
在 onNewPartitionCreation 方法中,分别使用分区状态机和副本状态机进行分区及副本状态的转换,调用的 handleStateChanges 方法中,都执行了如下代码:
//Controller控制类请求给对应的Broker,通知其状态变化controllerBrokerRequestBatch.sendRequestsToBrokers(controllerContext.epoch)
sendRequestsToBrokers:发送各种控制类请求
def sendRequestsToBrokers(controllerEpoch: Int): Unit = { try { val stateChangeLog = stateChangeLogger.withControllerEpoch(controllerEpoch) //发送更新Leader副本和ISR列表的请求 sendLeaderAndIsrRequest(controllerEpoch, stateChangeLog) //发送更新元数据的请求 sendUpdateMetadataRequests(controllerEpoch, stateChangeLog) //发送副本下线的请求 sendStopReplicaRequests(controllerEpoch) } catch { ...}
⑤调用 KafkaController.processTopicDeletion 方法处理删除主题时,待删除主题最后交给了 TopicDeletionManager 管理,其重启主题删除的方法 resumeDeletions 中,无论调用的 completeDeleteTopic 方法还是调用的 onTopicDeletion 方法,都会向集群的 Broker 发送更新元数据的请求:
completeDeleteTopic:该方法通过调用副本状态机的 handleStateChanges 方法,向 Broker 发送更新元数据的请求,原理同上
//第3步:利用副本状态机将这些副本对象转换成NonExistentReplica状态。replicaStateMachine.handleStateChanges(replicasForDeletedTopic.toSeq, NonExistentReplica)
onTopicDeletion:
//第四步:向集群Broker发送指定分区的元数据更新请求client.sendMetadataUpdate(partitions)
综上:可以认为只要 Controller 修改了集群的元数据,就会向 Broker 发送更新元数据的请求。
2. Controller 发送更新元数据请求的流程
在《
深入理解Kafka服务端之Controller基于事件队列的处理流程
》中,分析过 ControllerChannelManager 组件,其管理了 Controller 和集群所有 Broker 之间的网络连接,并为每个 Broker 创建了一个请求队列和一个专属的发送线程,用来向 Broker 发送请求并接收响应。
其实,在 Controller 和 ControllerChannelManager 之前还有一层,可以理解为请求批次对象。相关的类如下:
- AbstractControllerBrokerRequestBatch:请求批次的抽象基类
- ControllerBrokerRequestBatch:请求批次的具体实现类
AbstractControllerBrokerRequestBatch 的定义:
abstract class AbstractControllerBrokerRequestBatch(config: KafkaConfig,//配置类 controllerContext: ControllerContext,//元数据类 stateChangeLogger: StateChangeLogger//日志对象 ) extends Logging { //Controller 所在节点id val controllerId: Int = config.brokerId //存储LeaderAndIsrRequest请求的集合,key是目标Broker,value是一个map,value.key是主题分区,value.value是待更新的分区信息 val leaderAndIsrRequestMap = mutable.Map.empty[Int, mutable.Map[TopicPartition, LeaderAndIsrRequest.PartitionState]] //存储StopReplicaRequest请求的集合,key是目标Broker,value是待停止副本的信息 val stopReplicaRequestMap = mutable.Map.empty[Int, ListBuffer[StopReplicaRequestInfo]] //存储UpdateMetadataRequest请求的目标节点 val updateMetadataRequestBrokerSet = mutable.Set.empty[Int] //存储UpdateMetadataRequest请求的信息,key是主题分区,value是待更新的分区信息 val updateMetadataRequestPartitionInfoMap = mutable.Map.empty[TopicPartition, UpdateMetadataRequest.PartitionState] ...}
其定义了几个重要的变量,含义如下:
- controllerId:Controller 所在节点的 id
- leaderAndIsrRequestMap:存储 LeaderAndIsrRequest 请求的集合,key是目标Broker,value是一个map,value.key是主题分区对象,value.value是待更新的分区信息
- stopReplicaRequestMap:存储 StopReplicaRequest 请求的集合,key是目标Broker,value是待停止副本的信息
- updateMetadataRequestBrokerSet:存储 UpdateMetadataRequest 请求目标节点的集合
- updateMetadataRequestPartitionInfoMap:存储 UpdateMetadataRequest 请求的信息,key是主题分区,value是待更新的分区信息
该抽象类定义了添加各类请求和发送各类请求的方法:(这里具体分析 UpdateMetadataRequest 请求的相关方法,另外两种请求类似)
添加请求相关方法:
本质是将请求添加到上面定义的各种数据结构中
- addLeaderAndIsrRequestForBrokers:将 LeaderAndIsrRequest 请求添加到存储该请求的集合
- addStopReplicaRequestForBrokers:将 StopReplicaRequest 请求添加到存储该请求的集合
- addUpdateMetadataRequestForBrokers:将 UpdateMetadataRequest 请求添加到存储该请求的集合
def addUpdateMetadataRequestForBrokers(brokerIds: Seq[Int], partitions: collection.Set[TopicPartition]): Unit = { //定义了一个内部方法,用于将待更新的分区信息保持到请求集合中 def updateMetadataRequestPartitionInfo(partition: TopicPartition, beingDeleted: Boolean) { val leaderIsrAndControllerEpochOpt = controllerContext.partitionLeadershipInfo.get(partition) leaderIsrAndControllerEpochOpt match { case Some(l @ LeaderIsrAndControllerEpoch(leaderAndIsr, controllerEpoch)) => val replicas = controllerContext.partitionReplicaAssignment(partition) val offlineReplicas = replicas.filter(!controllerContext.isReplicaOnline(_, partition)) val leaderIsrAndControllerEpoch = if (beingDeleted) { val leaderDuringDelete = LeaderAndIsr.duringDelete(leaderAndIsr.isr) LeaderIsrAndControllerEpoch(leaderDuringDelete, controllerEpoch) } else { l } val partitionStateInfo = new UpdateMetadataRequest.PartitionState(leaderIsrAndControllerEpoch.controllerEpoch, leaderIsrAndControllerEpoch.leaderAndIsr.leader, leaderIsrAndControllerEpoch.leaderAndIsr.leaderEpoch, leaderIsrAndControllerEpoch.leaderAndIsr.isr.map(Integer.valueOf).asJava, leaderIsrAndControllerEpoch.leaderAndIsr.zkVersion, replicas.map(Integer.valueOf).asJava, offlineReplicas.map(Integer.valueOf).asJava) updateMetadataRequestPartitionInfoMap.put(partition, partitionStateInfo) case None => info(s"Leader not yet assigned for partition $partition. Skip sending UpdateMetadataRequest.") } } //将待发送请求的目标 Broker 放入集合 updateMetadataRequestBrokerSet ++= brokerIds.filter(_ >= 0) //遍历分区对象,封装更新元数据请求的分区信息,将分区信息放入更新元数据请求的集合 partitions.foreach(partition => updateMetadataRequestPartitionInfo(partition, beingDeleted = controllerContext.topicsToBeDeleted.contains(partition.topic)))}
发送请求相关方法:
本质是将请求添加到 ControllerChannelManager 中管理的对应 Broker 的请求队列
- sendLeaderAndIsrRequest:发送 LeaderAndIsrRequest 请求
- sendStopReplicaRequests:发送 StopReplicaRequest 请求
- sendUpdateMetadataRequests:发送 UpdateMetadataRequest 请求
private def sendUpdateMetadataRequests(controllerEpoch: Int, stateChangeLog: StateChangeLogger): Unit = { updateMetadataRequestPartitionInfoMap.foreach { case (tp, partitionState) => stateChangeLog.trace(s"Sending UpdateMetadata request $partitionState to brokers $updateMetadataRequestBrokerSet " + s"for partition $tp") } //复制一份待发送元数据更新请求的分区信息 val partitionStates = Map.empty ++ updateMetadataRequestPartitionInfoMap //请求版本 val updateMetadataRequestVersion: Short = if (config.interBrokerProtocolVersion >= KAFKA_2_2_IV0) 5 else if (config.interBrokerProtocolVersion >= KAFKA_1_0_IV0) 4 else if (config.interBrokerProtocolVersion >= KAFKA_0_10_2_IV0) 3 else if (config.interBrokerProtocolVersion >= KAFKA_0_10_0_IV1) 2 else if (config.interBrokerProtocolVersion >= KAFKA_0_9_0) 1 else 0 //待发送请求的目标节点 val liveBrokers = if (updateMetadataRequestVersion == 0) { // Version 0 of UpdateMetadataRequest only supports PLAINTEXT. controllerContext.liveOrShuttingDownBrokers.map { broker => val securityProtocol = SecurityProtocol.PLAINTEXT val listenerName = ListenerName.forSecurityProtocol(securityProtocol) val node = broker.node(listenerName) val endPoints = Seq(new EndPoint(node.host, node.port, securityProtocol, listenerName)) new UpdateMetadataRequest.Broker(broker.id, endPoints.asJava, broker.rack.orNull) } } else { controllerContext.liveOrShuttingDownBrokers.map { broker => val endPoints = broker.endPoints.map { endPoint => new UpdateMetadataRequest.EndPoint(endPoint.host, endPoint.port, endPoint.securityProtocol, endPoint.listenerName) } new UpdateMetadataRequest.Broker(broker.id, endPoints.asJava, broker.rack.orNull) } } updateMetadataRequestBrokerSet.intersect(controllerContext.liveOrShuttingDownBrokerIds).foreach { broker => val brokerEpoch = controllerContext.liveBrokerIdAndEpochs(broker) val updateMetadataRequest = new UpdateMetadataRequest.Builder(updateMetadataRequestVersion, controllerId, controllerEpoch, brokerEpoch, partitionStates.asJava, liveBrokers.asJava) //向指定节点发送请求,本质就是将请求添加到目标节点的请求队列 sendRequest(broker, updateMetadataRequest) } //清空更新元数据的目标Broker列表和分区信息集合 updateMetadataRequestBrokerSet.clear() updateMetadataRequestPartitionInfoMap.clear()}
该方法的逻辑是:
- 第一步:将待发送的元数据更新请求的分区信息保存到一个新的集合
- 第二步:获取待发送请求的目标Broker
- 第三步:将待发送的请求添加到 ControllerChannelManager 管理的每个 Broker 对应的请求队列
- 第四步:清空存储元数据更新请求的相关集合
sendRequestsToBrokers:发送各种控制类请求,内部调用了上面三个发送请求的方法
除此之外,还定义了几个操作存储请求集合的方法:
newBach:判断各个存储请求的集合是否为空,不为空则抛异常。调用上面发送请求的相关方法时,最后都会清空对应的请求集合
def newBatch() { // raise error if the previous batch is not empty if (leaderAndIsrRequestMap.nonEmpty) throw new IllegalStateException("Controller to broker state change requests batch is not empty while creating " + s"a new one. Some LeaderAndIsr state changes $leaderAndIsrRequestMap might be lost ") if (stopReplicaRequestMap.nonEmpty) throw new IllegalStateException("Controller to broker state change requests batch is not empty while creating a " + s"new one. Some StopReplica state changes $stopReplicaRequestMap might be lost ") if (updateMetadataRequestBrokerSet.nonEmpty) throw new IllegalStateException("Controller to broker state change requests batch is not empty while creating a " + s"new one. Some UpdateMetadata state changes to brokers $updateMetadataRequestBrokerSet with partition info " + s"$updateMetadataRequestPartitionInfoMap might be lost ")}
clear:清空各个存储请求的集合
def clear() { leaderAndIsrRequestMap.clear() stopReplicaRequestMap.clear() updateMetadataRequestBrokerSet.clear() updateMetadataRequestPartitionInfoMap.clear()}
ControllerBrokerRequestBatch:
请求批次的实现类,主要实现了两个方法:
sendEvent:
def sendEvent(event: ControllerEvent): Unit = { //将事件添加到事件管理器 controllerEventManager.put(event)}
sendRequest:
def sendRequest(brokerId: Int, request: AbstractControlRequest.Builder[_ <: abstractcontrolrequest> callback: AbstractResponse => Unit = null): Unit = { //向指定的Broker发送控制类请求,本质是添加到Broker对应的请求队列 controllerChannelManager.sendRequest(brokerId, request, callback)}
在 KafkaController 类处理各类事件时,发送更新元数据请求的方法为:sendUpdateMetadataRequest:
private[controller] def sendUpdateMetadataRequest(brokers: Seq[Int], partitions: Set[TopicPartition]) { try { //判断三个待发送请求集合是否为空 brokerRequestBatch.newBatch() //将请求添加到ControllerBrokerRequestBatch管理请求的数据结构中 brokerRequestBatch.addUpdateMetadataRequestForBrokers(brokers, partitions) //向Broker发送请求,本质是将请求添加到目标节点对应的请求队列中 //ControllerChannelManager管理了每个Broker和对应的请求队列 brokerRequestBatch.sendRequestsToBrokers(epoch) } catch { case e: IllegalStateException => handleIllegalState(e) }}
这个方法的逻辑比较清晰:首先将待发送的请求添加到保存请求的集合中,然后将集合中的请求提交到 ControllerChannelManager 的请求队列
3. Broker 处理元数据请求的流程
MetadataCache:Broker 端存储元数据的类
class MetadataCache(brokerId: Int) extends Logging { //锁对象 private val partitionMetadataLock = new ReentrantReadWriteLock() //保存实际的元数据信息,volatile修饰,说明是易变的 @volatile private var metadataSnapshot: MetadataSnapshot = MetadataSnapshot(partitionStates = mutable.AnyRefMap.empty, controllerId = None, aliveBrokers = mutable.LongMap.empty, aliveNodes = mutable.LongMap.empty) //用于日志输出的属性 this.logIdent = s"[MetadataCache brokerId=$brokerId] " private val stateChangeLogger = new StateChangeLogger(brokerId, inControllerContext = false, None) ...}
创建 MetaDataCache 实例时,只需要传入当前节点 id 即可。定义的变量如下:
- partitionMetadataLock:可重入的读写锁对象,用于保护元数据的写入
- logIdent 和 stateChangeLogger:用于日志输出
- metadataSnapshot:保存集群元数据信息的对象,用 volatile 关键字修饰,说明是易变的。
MetadataSnapshot 类:是 MetaDataCache 中定义的一个样例类,定义如下:
case class MetadataSnapshot(partitionStates: mutable.AnyRefMap[String, mutable.LongMap[UpdateMetadataRequest.PartitionState]],//key是主题名称,value是一个Map,value.key 是分区号,value.value是PartitionState controllerId: Option[Int],//Controller 所在 Broker 的 ID aliveBrokers: mutable.LongMap[Broker],//当前集群中所有存活着的 Broker 对象列表 aliveNodes: mutable.LongMap[collection.Map[ListenerName, Node]])//key是BrokerId,value是一个Map,value.key是ListenerName即监听器类型,value是Node节点类型
其属性含义如下:
- partitionStates:key 表示主题名称,value是一个Map类型,value.key 表示分区号,value.value 是 UpdateMetadataRequest.PartitionState 类型,保存分区信息
- controllerId:Controller 所在节点 id
- aliveBrokers:当前集群中所有存活的 Broker 对象列表
- aliveNodes:key 是 Broker Id,value 是一个Map,value.key 是ListenerName,即监听器类型,value.value 是Node节点类型
UpdateMetadataRequest.PartitionState
类型:
public static final class PartitionState { //分区基础信息 public final BasePartitionState basePartitionState; //离线副本集合 public final List offlineReplicas; ...}
该类定义了两个属性:
- basePartitionState:BasePartitionState 类型,存储了分区的基础信息
public class BasePartitionState { //Controller Epoch信息 public final int controllerEpoch; //Leader副本所在节点 public final int leader; //Leader副本的Epoch public final int leaderEpoch; //ISR列表 public final List isr; //ZooKeeper节点Stat统计信息中的版本号 public final int zkVersion; //副本对象集合,即AR集合 public final List replicas; ...}
- offlineReplicas:离线副本集合
MetadataCache 对象的初始化:
在 KafkaServer.startup 方法中进行了 MetadataCache 对象的初始化:
metadataCache = new MetadataCache(config.brokerId)
MetadataCache 主要方法:
这里主要分析更新元数据的方法:updateMetadata。该方法的返回值是已删除主题分区的列表
def updateMetadata(correlationId: Int, updateMetadataRequest: UpdateMetadataRequest): Seq[TopicPartition] = { //加锁 inWriteLock(partitionMetadataLock) { //第一步:定义两个容器: //保存存活Broker对象。Key是BrokerID,Value是Broker对象 val aliveBrokers = new mutable.LongMap[Broker](metadataSnapshot.aliveBrokers.size) //保存存活节点对象。Key是BrokerID,Value是监听器->Node节点对象 val aliveNodes = new mutable.LongMap[collection.Map[ListenerName, Node]](metadataSnapshot.aliveNodes.size) //第二步:获取请求中携带的Controller所在的节点id val controllerId = updateMetadataRequest.controllerId match { case id if id < 0 => None case id => Some(id) } //第三步:遍历请求中的所有存活的Broker对象,对其进行处理后将节点信息放入步骤一定义的两个容器中 updateMetadataRequest.liveBrokers.asScala.foreach { broker => // `aliveNodes` is a hot path for metadata requests for large clusters, so we use java.util.HashMap which // is a bit faster than scala.collection.mutable.HashMap. When we drop support for Scala 2.10, we could // move to `AnyRefMap`, which has comparable performance. //定义两个数据结构,用于存放监听器 val nodes = new java.util.HashMap[ListenerName, Node] val endPoints = new mutable.ArrayBuffer[EndPoint] //遍历Broker中所有的Endpoint类型,即Broker配置的监听器 broker.endPoints.asScala.foreach { ep => //将监听器封装成EndPoint类型并放入endPoints集合 endPoints += EndPoint(ep.host, ep.port, ep.listenerName, ep.securityProtocol) // 将对应关系保存起来 nodes.put(ep.listenerName, new Node(broker.id, ep.host, ep.port)) } // 将Broker加入到存活Broker对象集合 aliveBrokers(broker.id) = Broker(broker.id, endPoints, Option(broker.rack)) // 将Broker节点加入到存活Node节点对象集合 aliveNodes(broker.id) = nodes.asScala } // 第四步:使用上面获取的存活Node节点对象集合,获取当前Broker所有的对 // 检查当前节点配置的ListenerName是否和其他节点不一致,如果是则记录错误日志 aliveNodes.get(brokerId).foreach { listenerMap => val listeners = listenerMap.keySet // 如果发现当前Broker配置的监听器与其他Broker有不同之处,记录错误日志 if (!aliveNodes.values.forall(_.keySet == listeners)) error(s"Listeners are not identical across brokers: $aliveNodes") } //第四步:构造已删除分区数组,将其作为方法返回的结果集 val deletedPartitions = new mutable.ArrayBuffer[TopicPartition] //第五步:如果UpdateMetadataRequest请求没有携带任何分区信息,说明只是更新Broker节点的信息 if (updateMetadataRequest.partitionStates().isEmpty) { // 构造新的MetadataSnapshot对象,使用之前的分区信息和新的Broker列表信息 metadataSnapshot = MetadataSnapshot(metadataSnapshot.partitionStates, controllerId, aliveBrokers, aliveNodes) //第六步:如果携带了分区信息,说明需要更新分区信息,那么先将之前的分区信息进行备份 } else { //since kafka may do partial metadata updates, we start by copying the previous state val partitionStates = new mutable.AnyRefMap[String, mutable.LongMap[UpdateMetadataRequest.PartitionState]](metadataSnapshot.partitionStates.size) // 备份现有元数据缓存中的分区数据 metadataSnapshot.partitionStates.foreach { case (topic, oldPartitionStates) => val copy = new mutable.LongMap[UpdateMetadataRequest.PartitionState](oldPartitionStates.size) copy ++= oldPartitionStates partitionStates += (topic -> copy) } //遍历请求中的分区信息 updateMetadataRequest.partitionStates.asScala.foreach { case (tp, info) => val controllerId = updateMetadataRequest.controllerId val controllerEpoch = updateMetadataRequest.controllerEpoch // 如果分区处于被删除过程中 if (info.basePartitionState.leader == LeaderAndIsr.LeaderDuringDelete) { // 将分区从元数据缓存中移除 removePartitionInfo(partitionStates, tp.topic, tp.partition) stateChangeLogger.trace(s"Deleted partition $tp from metadata cache in response to UpdateMetadata " + s"request sent by controller $controllerId epoch $controllerEpoch with correlation id $correlationId") // 将分区加入到结果集 deletedPartitions += tp } else { // 将分区加入到元数据缓存 addOrUpdatePartitionInfo(partitionStates, tp.topic, tp.partition, info) stateChangeLogger.trace(s"Cached leader info $info for partition $tp in response to " + s"UpdateMetadata request sent by controller $controllerId epoch $controllerEpoch with correlation id $correlationId") } } // 第七步:使用更新过的分区元数据,和第一部分计算的存活Broker列表及节点列表,构建最新的元数据缓存 metadataSnapshot = MetadataSnapshot(partitionStates, controllerId, aliveBrokers, aliveNodes) } // 第八步:返回已删除分区列表数组 deletedPartitions }}
该方法的逻辑相对复杂,总结有以下的 8 个步骤:
- 第一步:定义两个容器。
- aliveBrokers 用于存储存活的 Broker 对象:key是BrokerId,value 是 Broker 对象
- aliveNodes 用于存储节点上的监听器和Node对象的对应关系:key 是 BrokerId,value 是一个Map,value.key 是监听器,value.value 是 Node 对象
- 第二步:获取 UpdateMetadataRequest 请求中携带的 Controller 所在节点的 id
- 第三步:遍历请求中的所有存活的 Broker 对象,对其进行处理后将节点信息放入第一步定义的两个容器中
- 第四步:构造已删除分区数组,将其作为方法返回的结果集
- 第五步:如果 UpdateMetadataRequest 请求没有携带任何分区信息,说明只是更新Broker节点信息的请求,使用之前节点存储的分区信息和新的Broker列表信息构建新的 MetadataSnapshot 对象
- 第六步:如果 UpdateMetadataRequest 请求携带了分区信息,说明需要更新分区信息,那么先将之前的分区信息进行备份。然后遍历请求中的分区信息,如果分区处于正在删除状态,则移除元数据缓存中该分区的信息,然后将其放入第四步构建的分区数组;否则直接更新元数据缓存中该分区的信息
- 第七步:使用更新过的分区元数据,和第一部分计算的存活Broker列表及节点列表,构建最新的元数据缓存 MetadataSnapshot 对象。并将元数据缓存的引用指向新创建的对象
- 第八步:返回已删除分区数组
分析完了 MetaDataCache 类,再来看一下 Broker 是如何处理 UpdateMetadataRequest 请求的:
Kafka 对于各种类型的请求,都是通过 KafkaApis 类的 handle 方法处理的,处理 UpdateMetadataRequest 请求的逻辑如下:
def handle(request: RequestChannel.Request) { try { trace(s"Handling request:${request.requestDesc(true)} from connection ${request.context.connectionId};" + s"securityProtocol:${request.context.securityProtocol},principal:${request.context.principal}") request.header.apiKey match { ... //处理更新元数据的请求 case ApiKeys.UPDATE_METADATA => handleUpdateMetadataRequest(request) ... } } catch { case e: FatalExitError => throw e case e: Throwable => handleError(request, e) } finally { request.apiLocalCompleteTimeNanos = time.nanoseconds }}
具体的处理方法是:handleUpdateMetadataRequest
def handleUpdateMetadataRequest(request: RequestChannel.Request) { val correlationId = request.header.correlationId //获取请求对象 val updateMetadataRequest = request.body[UpdateMetadataRequest] //验证请求的合法性 authorizeClusterAction(request) //验证Broker Epoch是否过期 if (isBrokerEpochStale(updateMetadataRequest.brokerEpoch())) { info("Received update metadata request with broker epoch " + s"${updateMetadataRequest.brokerEpoch()} smaller than the current broker epoch ${controller.brokerEpoch}") sendResponseExemptThrottle(request, new UpdateMetadataResponse(Errors.STALE_BROKER_EPOCH)) //如果Broker Epoch合法 } else { //调用副本管理器的 maybeUpdateMetadataCache 方法尝试更新元数据缓存,返回已删除分区集合 val deletedPartitions = replicaManager.maybeUpdateMetadataCache(correlationId, updateMetadataRequest) //如果删除了分区 if (deletedPartitions.nonEmpty) //移除组协调器中已删除分区的信息 groupCoordinator.handleDeletedPartitions(deletedPartitions) if (adminManager.hasDelayedTopicOperations) { updateMetadataRequest.partitionStates.keySet.asScala.map(_.topic).foreach { topic => //尝试执行延时操作 adminManager.tryCompleteDelayedTopicOperations(topic) } } quotas.clientQuotaCallback.foreach { callback => if (callback.updateClusterMetadata(metadataCache.getClusterMetadata(clusterId, request.context.listenerName))) { quotas.fetch.updateQuotaMetricConfigs() quotas.produce.updateQuotaMetricConfigs() quotas.request.updateQuotaMetricConfigs() } } if (replicaManager.hasDelayedElectionOperations) { updateMetadataRequest.partitionStates.asScala.foreach { case (tp, ps) => //尝试执行延时选举 replicaManager.tryCompleteElection(new TopicPartitionOperationKey(tp.topic(), tp.partition())) } } //返回响应 sendResponseExemptThrottle(request, new UpdateMetadataResponse(Errors.NONE)) }}
该方法的主要逻辑是:
- 第一步:验证请求合法性
- 第二步:调用副本管理器的 maybeUpdateMetadataCache 方法尝试更新元数据缓存,返回已删除分区集合
- 第三步:如果删除了分区,则需要移除组协调器中这些已删除分区的信息
- 第四步:尝试执行各种延时操作
- 第五步:封装响应并返回
在第二步的 ReplicaManager.maybeUpdateMetadataCache 方法中,就调用了上面提到的 MetaDataCache.updateMetadata 方法来更新 Broker 端的元数据。
至此,Controller 端发送更新元数据的请求,Broker 端接收请求并更新元数据的流程就分析完了。
总结:
1. Controller 向 Broker 发送更新元数据请求的场景:只要 Controller 端更新了元数据,就会向 Broker 发送更新元数据的请求。如新加入节点、删除节点、节点信息变更、新增主题、删除主题等。
2. Controller 会将待发送的请求缓存到 ControllerBrokerRequestBatch 的数据结构中,然后交给 ControllerChannelManager 管理的各个 Broker 对应的请求队列,由专属发送线程发送给目标 Broker。
3. Broker 端的元数据是由 MetadataCache 类管理的
4. Broker 端通过 KafkaApis.handleUpdateMetadataRequest 方法处理更新集群元数据的请求