概述
本文描述了kafka的controller的实现原理,并对其源代码的实现进行了讲解。
controller运行原理
在Kafka集群中,controller多个broker中的一个(也只有一个controller),它除了实现正常的broker的功能外,还负责选取分区(partition)的leader。
第一个启动的broker会成为一个controller,它会在Zookeeper上创建一个临时节点(ephemeral):/controller。其他后启动的broker也尝试去创建这样一个临时节点,但会报错,此时这些broker会在该zookeeper的/controller节点上创建一个监控(Watch),这样当该节点状态发生变化(比如:被删除)时,这些broker就会得到通知。此时,这些broker就可以在得到通知时,继续创建该节点。保证该集群一直都有一个controller节点。
当controller所在的broker节点宕机或断开和Zookeeper的连接,它在Zookeeper上创建的临时节点就会被自动删除。其他在该节点上都安装了监控的broker节点都会得到通知,此时,这些broker都会尝试去创建这样一个临时的/controller节点,但它们当中只有一个broker(最先创建的那个)能够创建成功,其他的broker会报错:node already exists,接收到该错误的broker节点会再次在该临时节点上安装一个watch来监控该节点状态的变化。每次当一个broker被选举时,将会赋予一个更大的数字(通过zookeeper的条件递增实现),这样其他节点就知道controller目前的数字。
当一个broker宕机而不在当前Kafka集群中时,controller将会得到通知(通过监控zookeeper的路径实现),若有些topic的主分区恰好在该broker上,此时controller将重新选择这些主分区。controller将会检查所有没有leader的分区,并决定新的leader是谁(简单的方法是:选择该分区的下一个副本分区),并给所有的broker发送请求。
每个分区的新leader指导,它将接收来自客户端的生产者和消费者的请求。同时follower也指导,应该从这个新的leader开始复制消息。
当一个新的broker节点加入集群时,controller将会检查,在该broker上是否存在分区副本。若存在,controller通知新的和存在的broker这个变化,该broker开始从leader处复制消息。
总的来说,Kafka会通过在Zookeeper上创建临时节点的方式来选举一个controller,但Kafka集群中有节点加入或退出时,该controller将会得到通知。Controller还负责在多个分区中选择主分区,负责当有节点加入集群时进行副本的复制。Controller通过递增数字(epoch number)来防止脑裂(split brain)的问题(脑裂是指:多个节点都选自己为Controller)。
实现分析
Controller的实现原理
Controller是通过事件处理机制来实现的。把broker的节点的变化,分区的变化,都封装成事件,发生事件时把事件放入到事件队列中,此时阻塞在事件队列的处理者即可开始处理这些事件。
这些事件类,都必须实现同一个接口。
启动
类KafkaServer中的startup()函数中启动Controller,代码如下:
def startup() {
...
/* start kafka controller */
kafkaController = new KafkaController(config, zkClient, time, metrics, brokerInfo, tokenManager, threadNamePrefix)
// 启动kafka server的控制模块
kafkaController.startup()
...
}
初始化工作
Kafka服务节点启动时Controller模块就会启动。但当Controller启动时,不会假设自己是controller,而是先注册回话超时的监听者(listener),然后开始controller的leader选举过程。
启动时,会把Startup事件控制实体,放入到事件队列中。在eventManager线程启动时,会在队列取出ControllerEvent类型的事件。并进行处理,此时取出的当然是刚刚放入的Startup事件,所以,开始执行Startup类的process函数。代码实现如下:
def startup() = {
... ...
eventManager.put(Startup)
eventManager.start()
}
- Startup事件控制实体
在Controller模块启动时,该事件就被放入到事件队列中,所以,最开始处理该事件。执行的处理函数是下面定义的process()。代码的实现如下:
case object Startup extends ControllerEvent {
def state = ControllerState.ControllerChange
override def process(): Unit = {
zkClient.registerZNodeChangeHandlerAndCheckExistence(controllerChangeHandler)
elect()
}
}
Startup事件处理
private def elect(): Unit = {
val timestamp = time.milliseconds
activeControllerId = zkClient.getControllerId.getOrElse(-1)
// 这里要判断一下controller是否已经选出来了,若是选出来了,就不需要再继续选举
if (activeControllerId != -1) {
debug(s"Broker $activeControllerId has been elected as the controller, so stopping the election process.")
return
}
// 若controller还没有选出来,则进行选举
try {
// 若成功的选举成controller,继续进行后面的注册,否则抛出异常
zkClient.checkedEphemeralCreate(ControllerZNode.path, ControllerZNode.encode(config.brokerId, timestamp))
info(s"${config.brokerId} successfully elected as the controller")
// 自己被选举成controller,进入onControllerFailover函数
activeControllerId = config.brokerId
onControllerFailover()
} catch {
case _: NodeExistsException =>
// If someone else has written the path, then
activeControllerId = zkClient.getControllerId.getOrElse(-1)
... ...
}
}
被选举成controller之后
当broker被选举成为controller后,继续执行后面的代码,此时进入onControllerFailover()函数。
该函数主要完成以下几件事:
* 注册controller epoch(controller id)变化的监听器
* 增加controller的id
* 初始化controller的context,该context保存了每个topic信息,和所有分区的leader信息。
* 开启controller的channel管理模块
* 开启副本状态机
* 开启分区状态机
* 当该发生任何的异常,会从新选择目前的controller的,这样让其他的broker节点也有可能成为controller。
* 注册zk上的broker状态变化,topic状态变化,topic删除,分区的zk目录等状态变化时的处理函数。
总结
本文总结了controller的运行原理,通过本文可以理解,controller的功能和选举过程。