一、场景分析      在客户端初始化生产者对象时,需要配置 bootstrap.servers 参数,用于获取 Kafka 集群的元数据。这里只需要指定集群中 任意一台 节点即可,原因就是每个 Broker 均存储了集群的所有元数据信息。这篇主要分析 Controller 发送更新元数据请求的场景,以及 Broker更新集群元数据的流程。 二、图示说明

Controller 发送更新元数据请求,Broker 接收请求并更新元数据流程图解

kafka 获取 数据条数 从kafka读取数据更新数据库_数据请求


三、源码分析


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:请求批次的具体实现类

kafka 获取 数据条数 从kafka读取数据更新数据库_数据请求_02

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 方法处理更新集群元数据的请求