大家好,这是一个为了梦想而保持学习的博客。这个专题会记录我对于 KAFKA 的学习和实战经验,希望对大家有所帮助,目录形式依旧为问答的方式,相当于是模拟面试。
【概述】
在 kafka 集群中,还存在一个角色:Controller
这个角色和 kafka 集群中的各个 broker 是什么关系呢?其实就是任意一个 broker 都可以去扮演这个么一个 Controller 的角色,然后去履行这个角色所需要执行的动作。
一、为什么要设计 controller 角色呢?
我们可以思考一些分布式系统中可能存在的问题:
1、整个集群中的的元数据要如何维护呢?如果每一个 broker 都可以随意修改元数据,那么元数据的管理会变得非常复杂;如果单独创建一个组件单独来管理元数据,又需要单独为这个组件进行容灾部署;
2、集群中如果有某个 Broker 掉线了该如何保证服务可用呢?也就是如何做容灾处理呢?
3、集群中如果加入了一个新的 Broker 该如何调整集群状态呢?
以上这三个问题,在不同的分布式系统中有着不同的答案与对应的设计;但是在 kafka 中,对应的答案就是选举出一个 broker 来担任 Controller 的角色,来负责解决以上这些问题。
总结一下的话:kafka 中的 controller 角色,主要是为了集群容灾,以及集群管理而设计的。
整个集群完整的架构图现在看起来就是这个样子的:
二、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 的设计,带来的脑裂问题,羊群效应等。