大家好,这是一个为了梦想而保持学习的博客。这个专题会记录我对于 KAFKA 的学习和实战经验,希望对大家有所帮助,目录形式依旧为问答的方式,相当于是模拟面试。

【概述】

在 kafka 集群中,还存在一个角色:Controller

这个角色和 kafka 集群中的各个 broker 是什么关系呢?其实就是任意一个 broker 都可以去扮演这个么一个 Controller 的角色,然后去履行这个角色所需要执行的动作。

 


一、为什么要设计 controller 角色呢?

我们可以思考一些分布式系统中可能存在的问题:
1、整个集群中的的元数据要如何维护呢?如果每一个 broker 都可以随意修改元数据,那么元数据的管理会变得非常复杂;如果单独创建一个组件单独来管理元数据,又需要单独为这个组件进行容灾部署;
2、集群中如果有某个 Broker 掉线了该如何保证服务可用呢?也就是如何做容灾处理呢?
3、集群中如果加入了一个新的 Broker 该如何调整集群状态呢?
以上这三个问题,在不同的分布式系统中有着不同的答案与对应的设计;但是在 kafka 中,对应的答案就是选举出一个 broker 来担任 Controller 的角色,来负责解决以上这些问题。
总结一下的话:kafka 中的 controller 角色,主要是为了集群容灾,以及集群管理而设计的。

整个集群完整的架构图现在看起来就是这个样子的:

kafka异地容灾 kafka容灾部署方案_kafka

 


二、controller 角色的职责是什么?

Controller 职责,其实翻译一下就是 Controller 角色要做哪些事情呢?
这个看代码最直接不过了,以下代码是某个 Broker 在成为 Controller 之后的回调函数:

private def onControllerFailover() {
  info("Reading controller epoch from ZooKeeper")
  readControllerEpochFromZooKeeper()
  info("Incrementing controller epoch in ZooKeeper")
  incrementControllerEpoch()
  info("Registering handlers")

  // before reading source of truth from zookeeper, register the listeners to get broker/topic callbacks
  // 注册一大堆必要的handlers
  val childChangeHandlers = Seq(brokerChangeHandler, topicChangeHandler, topicDeletionHandler, logDirEventNotificationHandler,
    isrChangeNotificationHandler)
  childChangeHandlers.foreach(zkClient.registerZNodeChildChangeHandler)
  // 注册最优副本选举/分区重分配的事件handler
  val nodeChangeHandlers = Seq(preferredReplicaElectionHandler, partitionReassignmentHandler)
  nodeChangeHandlers.foreach(zkClient.registerZNodeChangeHandlerAndCheckExistence)

  info("Deleting log dir event notifications")
  zkClient.deleteLogDirEventNotifications()
  info("Deleting isr change notifications")
  zkClient.deleteIsrChangeNotifications()
  info("Initializing controller context")
  initializeControllerContext()
  info("Fetching topic deletions in progress")
  val (topicsToBeDeleted, topicsIneligibleForDeletion) = fetchTopicDeletionsInProgress()
  info("Initializing topic deletion manager")
  topicDeletionManager.init(topicsToBeDeleted, topicsIneligibleForDeletion)

  // We need to send UpdateMetadataRequest after the controller context is initialized and before the state machines
  // are started. The is because brokers need to receive the list of live brokers from UpdateMetadataRequest before
  // they can process the LeaderAndIsrRequests that are generated by replicaStateMachine.startup() and
  // partitionStateMachine.startup().
  info("Sending update metadata request")
  sendUpdateMetadataRequest(controllerContext.liveOrShuttingDownBrokerIds.toSeq)

  replicaStateMachine.startup()
  partitionStateMachine.startup()

  info(s"Ready to serve as the new controller with epoch $epoch")
  maybeTriggerPartitionReassignment(controllerContext.partitionsBeingReassigned.keySet)
  topicDeletionManager.tryTopicDeletion()
  val pendingPreferredReplicaElections = fetchPendingPreferredReplicaElections()
  onPreferredReplicaElection(pendingPreferredReplicaElections)
  info("Starting the controller scheduler")
  kafkaScheduler.startup()
  if (config.autoLeaderRebalanceEnable) {
    scheduleAutoLeaderRebalanceTask(delay = 5, unit = TimeUnit.SECONDS)
  }

  if (config.tokenAuthEnabled) {
    info("starting the token expiry check scheduler")
    tokenCleanScheduler.startup()
    tokenCleanScheduler.schedule(name = "delete-expired-tokens",
      fun = tokenManager.expireTokens,
      period = config.delegationTokenExpiryCheckIntervalMs,
      unit = TimeUnit.MILLISECONDS)
  }
}

可以看到上面加粗的几行代码,是不是可以准确的看到 Controller 需要注册一大堆的监听器,然后绑定一大堆对应的 Handlers,这就是 Controller 负责的主要工作。
总结一下的话就是:
1、负责集群管理,包括 Broker 管理、Topic 管理、ISR 变更管理等等。其中 Broker 管理里面就有一项当某个 Broker 宕机以后该更新集群状态让剩余存活的 Broker 正确提供服务。
2、负责更新元数据,也就是一旦集群 Controller 感知到了集群的状态变化,都会把最新的元数据信息发送给还存活的 Broker,以更新整个集群的元数据。

而从上面源码中我们也能知道,Controller 是如何进行管理的呢?就是依赖于 zk 的临时节点,以及注册对应的监听器。一旦目标节点出现对应的事件,就去执行对应的 Handler 逻辑。
下面我们从源码中看一下,当某个 Broker 宕机后,Controller 是怎么实现容灾的呢?以下是 Broker 节点出现变化对应的 handler:

case object BrokerChange extends ControllerEvent {
  override def state: ControllerState = ControllerState.BrokerChange

  override def process(): Unit = {
    // 如果当前节点不是controller,不让执行
    if (!isActive) return
    // 从zk中获取最新的brokers
    val curBrokers = zkClient.getAllBrokersInCluster.toSet
    val curBrokerIds = curBrokers.map(_.id)
    // 原先本地缓存的brokers
    val liveOrShuttingDownBrokerIds = controllerContext.liveOrShuttingDownBrokerIds
    // new - old = 新增的brokers
    val newBrokerIds = curBrokerIds -- liveOrShuttingDownBrokerIds
    // old - new = 挂掉的brokers
    val deadBrokerIds = liveOrShuttingDownBrokerIds -- curBrokerIds
    val newBrokers = curBrokers.filter(broker => newBrokerIds(broker.id))
    // 更新本地的缓存信息
    controllerContext.liveBrokers = curBrokers
    val newBrokerIdsSorted = newBrokerIds.toSeq.sorted
    val deadBrokerIdsSorted = deadBrokerIds.toSeq.sorted
    val liveBrokerIdsSorted = curBrokerIds.toSeq.sorted
    info(s"Newly added brokers: ${newBrokerIdsSorted.mkString(",")}, " +
      s"deleted brokers: ${deadBrokerIdsSorted.mkString(",")}, all live brokers: ${liveBrokerIdsSorted.mkString(",")}")
    // 遍历新增的brokers,为每个新增的broker:
    // 1、初始化网络模块,networkClinet
    // 2、初始化对应的请求线程,requestThread
    // 最后将这些信息缓存在controller的map中
    newBrokers.foreach(controllerContext.controllerChannelManager.addBroker)
    // 清理掉GG的brokers对应的资源,并从map中移除
    deadBrokerIds.foreach(controllerContext.controllerChannelManager.removeBroker)
    if (newBrokerIds.nonEmpty)
      // 其实做两件事:
      // 1、给存活的所有broker,通过他们的sendRequestThread发送updateMetaDaraRequest
      // 2、给新增的broker节点,添加节点变更的监听器(这个监听器的内容,主要还是更新本地元数据以后,发送元数据更新请求)
      onBrokerStartup(newBrokerIdsSorted)
    if (deadBrokerIds.nonEmpty)
      // 三件事:
      // 1、移除本地该节点的缓存数据
      // 2、下线该节点上的副本
      // 3、取消掉对这些节点的变更监听器
      onBrokerFailure(deadBrokerIdsSorted)
  }
}
private def onBrokerFailure(deadBrokers: Seq[Int]) {
  info(s"Broker failure callback for ${deadBrokers.mkString(",")}")
  // 移除本地缓存信息
  deadBrokers.foreach(controllerContext.replicasOnOfflineDirs.remove)
  val deadBrokersThatWereShuttingDown =
    deadBrokers.filter(id => controllerContext.shuttingDownBrokerIds.remove(id))
  info(s"Removed $deadBrokersThatWereShuttingDown from list of shutting down brokers.")
  val allReplicasOnDeadBrokers = controllerContext.replicasOnBrokers(deadBrokers.toSet)
  // 下线对应broker上的副本
  onReplicasBecomeOffline(allReplicasOnDeadBrokers)
  // 取消这些节点的节点变更触发器
  unregisterBrokerModificationsHandler(deadBrokers)
}
private def onReplicasBecomeOffline(newOfflineReplicas: Set[PartitionAndReplica]): Unit = {
  val (newOfflineReplicasForDeletion, newOfflineReplicasNotForDeletion) =
    newOfflineReplicas.partition(p => topicDeletionManager.isTopicQueuedUpForDeletion(p.topic))

  val partitionsWithoutLeader = controllerContext.partitionLeadershipInfo.filter(partitionAndLeader =>
    !controllerContext.isReplicaOnline(partitionAndLeader._2.leaderAndIsr.leader, partitionAndLeader._1) &&
      !topicDeletionManager.isTopicQueuedUpForDeletion(partitionAndLeader._1.topic)).keySet

  // trigger OfflinePartition state for all partitions whose current leader is one amongst the newOfflineReplicas
  // 触发更新那些有leader在这些下线的副本之内的节点的元数据
  partitionStateMachine.handleStateChanges(partitionsWithoutLeader.toSeq, OfflinePartition)
  // trigger OnlinePartition state changes for offline or new partitions
  partitionStateMachine.triggerOnlinePartitionStateChange()
  // trigger OfflineReplica state change for those newly offline replicas
  replicaStateMachine.handleStateChanges(newOfflineReplicasNotForDeletion.toSeq, OfflineReplica)

  // fail deletion of topics that are affected by the offline replicas
  if (newOfflineReplicasForDeletion.nonEmpty) {
    // it is required to mark the respective replicas in TopicDeletionFailed state since the replica cannot be
    // deleted when its log directory is offline. This will prevent the replica from being in TopicDeletionStarted state indefinitely
    // since topic deletion cannot be retried until at least one replica is in TopicDeletionStarted state
    topicDeletionManager.failReplicaDeletion(newOfflineReplicasForDeletion)
  }

  // If replica failure did not require leader re-election, inform brokers of the offline replica
  // Note that during leader re-election, brokers update their metadata
  if (partitionsWithoutLeader.isEmpty) {
    sendUpdateMetadataRequest(controllerContext.liveOrShuttingDownBrokerIds.toSeq)
  }
}

其实就是感知到节点变化之后,计算出下线的 Broker,然后通知分布在其他节点上的 follower 副本成为 leader,继续提供服务。

 


三、controller 角色是如何选举的呢?

同样的,源码出真知,我们直接看一下 controller 初始化的代码:

/* start kafka controller */
kafkaController = new KafkaController(config, zkClient, time, metrics, brokerInfo, tokenManager, threadNamePrefix)
kafkaController.startup()
/**
 * Invoked when the controller module of a Kafka server is started up. This does not assume that the current broker
 * is the controller. It merely registers the session expiration listener and starts the controller leader
 * elector
  *
  * 当kafka启动的时候,并不直到谁是controller
  * 因此他就在zk上注册了一个超时的listener
  * 并且开启选举*/
def startup() = {
  // 注册一个handler,在对应的事件触发的时候执行
  // handler的name为:controller-state-change-handler
  zkClient.registerStateChangeHandler(new StateChangeHandler {
    override val name: String = StateChangeHandlers.ControllerHandler
override def afterInitializingSession(): Unit = {
      eventManager.put(RegisterBrokerAndReelect)
    }
    override def beforeInitializingSession(): Unit = {
      val expireEvent = new Expire
      eventManager.clearAndPut(expireEvent)
      expireEvent.waitUntilProcessed()
    }
  })
  // 向 ControllerEventManager 中放入启动事件
  // 其实就是丢到了一个queue中,阻塞队列
  // 这里其实是一个生产者消费者模式 + 策略模式
  eventManager.put(Startup)
  // 启动内部的处理线程
  eventManager.start()
}
case object Startup extends ControllerEvent {

  def state = ControllerState.ControllerChange

  override def process(): Unit = {
    // 检查/controller这个znode是否存在
    // 然后注册该节点(创建/删除/数据改变)对应handler
    // 这里注册的handler其实就是注册对这个znode的watcher,然后一旦节点发生变化,
    // 就根据具体的事件执行具体的handler逻辑
    zkClient.registerZNodeChangeHandlerAndCheckExistence(controllerChangeHandler)
    // 然后执行选举
    elect()
  }
}
private def elect(): Unit = {
  val timestamp = time.milliseconds
  // 读取controllerId的值,没有的话,就返回-1
  activeControllerId = zkClient.getControllerId.getOrElse(-1)
  /*
   * We can get here during the initial startup and the handleDeleted ZK callback. Because of the potential race condition,
   * it's possible that the controller has already been elected when we get here. This check will prevent the following
   * createEphemeralPath method from getting into an infinite loop if this broker is already the controller.
   */
  // 如果已经有broker成为了controller,则直接中断操作
  if (activeControllerId != -1) {
    debug(s"Broker $activeControllerId has been elected as the controller, so stopping the election process.")
    return
  }

  try {
    // 尝试在zk上注册临时节点
    // 节点信息包括自己的brokerid和当前时间戳
    zkClient.checkedEphemeralCreate(ControllerZNode.path, ControllerZNode.encode(config.brokerId, timestamp))
    // 如果没有报错,则说明注册成功,直接打印信息
    info(s"${config.brokerId} successfully elected as the controller")
    // 更新当前的controllerId为自己的brokerId
    activeControllerId = config.brokerId
// 做一些controller应该做的事情
    // 更改controllerEpoch、增加一些监听器啥的
    onControllerFailover()
  } catch {
    case _: NodeExistsException =>
      // 如果注册失败了,得到这个节点已存在的异常
      // 那么此时就会更新自己本地的activeControllerId为那个节点里面的brokerId
      // If someone else has written the path, then
      activeControllerId = zkClient.getControllerId.getOrElse(-1)
      // 这里如果是拿到的controllerId依旧是-1,那么说明刚刚选上的broker又GG了
      // 所以重新开启新的一轮选举
      if (activeControllerId != -1)
        debug(s"Broker $activeControllerId was elected as controller instead of broker ${config.brokerId}")
      else
        warn("A controller has been elected but just resigned, this will result in another round of election")

    case e2: Throwable =>
      error(s"Error while electing or becoming controller on broker ${config.brokerId}", e2)
      triggerControllerMove()
  }
}

上面的代码是不是写的非常清晰明了?Kafka 不仅仅是架构优秀,内部代码设计和注释,简直也是业界良心,只不过是编写语言是 scala 从而劝退一些人,
不过其实我们了解一点语法,其实就能大概明白是这些代码是什么意思了,更何况还有很完善的注释信息。如果没有二次开发的需要,只需要了解一点点语法即可。

总结一下的话就是:broker 在启动的时候会去 zk 上注册一个 /controller 临时节点,写入自己的 brokerId 和时间戳,如果成功就说明自己选上了,如果失败则获取当前已经当选的 broker 的 brokerId。
所以通常一个集群中最开始的 controller 就是最先启动的那一个 broker。

 


四、controller 宕机了会发生什么

最后,我们在思考一个问题,一个 kafka 集群中只有一个 Broker 会成为 Controller,那么一旦挂掉的 Broker 是 Controller 该怎么办呢?是否也是需要多个 Controller 角色互为主备才可靠呢?
显然不是的,我们在最开始提到 Controller 角色的设计优雅之处,其中一点就是不需要单独部署,单独做容灾处理,直接可以复用 kafka 集群自身的容灾机制。
那么 kafka 是如何实现的呢?我们继续看对应部分的源码:

case object Startup extends ControllerEvent {

  def state = ControllerState.ControllerChange

  override def process(): Unit = {
    // 检查/controller这个znode是否存在
    // 然后注册该节点(创建/删除/数据改变)对应handler
    // 这里注册的handler其实就是注册对这个znode的watcher,然后一旦节点发生变化,
    // 就根据具体的事件执行具体的handler逻辑
    zkClient.registerZNodeChangeHandlerAndCheckExistence(controllerChangeHandler)
    // 然后执行选举
    elect()
  }
}
class ControllerChangeHandler(controller: KafkaController, eventManager: ControllerEventManager) extends ZNodeChangeHandler {
  override val path: String = ControllerZNode.path

override def handleCreation(): Unit = eventManager.put(controller.ControllerChange)
  override def handleDeletion(): Unit = eventManager.put(controller.Reelect)
  override def handleDataChange(): Unit = eventManager.put(controller.ControllerChange)
}
case object Reelect extends ControllerEvent {
  override def state = ControllerState.ControllerChange

  override def process(): Unit = {
    val wasActiveBeforeChange = isActive
    // 再次注册
    zkClient.registerZNodeChangeHandlerAndCheckExistence(controllerChangeHandler)
    activeControllerId = zkClient.getControllerId.getOrElse(-1)
    // 如果之前是controller,但是现在不是了,就需要清除一些状态
    if (wasActiveBeforeChange && !isActive) {
      onControllerResignation()
    }
    // 执行选举
    elect()
  }
}

从源码中我们可以看到,依旧是通过 zk 来实现的。

总结一下:
在启动的时候如果 /controller 节点存在则会对该节点增加一个监听器,并且绑定对应的 handler;
一旦承担 controller 角色的 broker 宕机了,对应的临时节点就会被删除,因此就会触发集群中剩余的所有 broker 的监听事件,再次执行选举动作,也就是竞争创建 /controller 节点;
最终谁创建成功就成为新的 controller;最后新的 controller 会重新更新集群状态,集群继续稳定提供服务。
整个 controller 切换过程的耗时,正常情况下在秒级以内。

 

补充:

1、zk 中还存在一个持久节点 /controller_epoch,用于记录 Controller 的变更次数,一旦 controller 发生切换这个节点值就会 + 1;
每个和 controller 交互的请求,都必须要携带 controller_epoch 值,一旦这个值和 broker 中缓存的值不一样,那么就说明可能是过期请求 (小于内存中的 controller_epoch),
或者 controller 发生了切换 (大于内存中的 controller_epoch),那么此时这个请求就会被视为无效请求。
由此可见,kakfa 通过 controller_epoch 来保证控制器的唯一性,进而保证相关操作的一致性。

2、controller 的设计,还减少了集群中所有 broker 对 zk 的依赖,因为整个集群中只有 controller 需要注册大量的监听器,而其他的 broker 只需要注册很少的监听器即可。
这样可以很好地避免集群中所有的 broker 注册大量的监听器,从而重度依赖 zk 的设计,带来的脑裂问题,羊群效应等。