Kafka 进阶学习(十三)—— 控制器

183 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第12天,点击查看活动详情

前言

今天是我 Kafka 学习的第 12 天,今天学习的内容是 Kafka 的控制器。

在 Kafka 集群中会有一个或多个 broker,其中有一个 broker 会被选举为控制器(Kafka Controller),它负责管理整个集群中所有分区和副本的状态。比如当某个分区的 leader 副本出现故障时,由控制器负责为该分区选举新的 leader 副本。当检测到某个分区的 ISR 集合发生变化时,由控制器负责通知所有 broker 更新其元数据信息。当使用 kafka-topics.sh 脚本为某个 topic 增加分区数量时,同样还是由控制器负责分区的重新分配。

下面就让我们开始今天的学习之旅!

控制器选举

Kafka 中的控制器选举工作依赖于 ZooKeeper,成功竞选为控制器的 broker 会在 ZooKeeper 中创建 /controller 这个临时(EPHEMERAL)节点,此临时节点的内容参考如下:

{"version":1, "brokerid":0, "timestamp": "123123213123"}

其中 version 在目前版本中固定为 1,brokerid 表示成为控制器的 broker 的 id 编号,timestamp 表示竞选成为控制器时的时间戳。

注意:在任意时刻,集群中有且仅有一个控制器

每个 broker 启动的时候会去尝试读取 /controller 节点的 brokerid 的值,如果读取到 brokerid 的值不为 -1,则表示已经有其他 broker 节点成功竞选为控制器,所以当前 broker 就会放弃竞选;如果 ZooKeeper 中不存在 /controller 节点,或者这个节点中的数据异常,那么就会尝试去创建 /controller 节点。当前 broker 去创建节点的时候,也有可能其他 broker 同时去尝试创建这个节点,只有创建成功的那个 broker 才会成为控制器,而创建失败的 broker 竞选失败。每个 broker 都会在内存中保存当前控制器的 brokerid 值,这个值可以标识为 activeControllerId。

操作一致性

ZooKeeper 中还有一个与控制器有关的 /controller_epoch 节点,这个节点是持久(PERSISTENT)节点,节点中存放的是一个整型的 controller_epoch 值。controller_epoch 用于记录控制器发生变更的次数,即记录当前的控制器是第几代控制器,我们也可以称之为“控制器的纪元”。

controller_epoch的初始值为1,即集群中第一个控制器的纪元为 1,当控制器发生变更时,每选出一个新的控制器就将该字段值加 1。每个和控制器交互的请求都会携带 controller_epoch 这个字段:

  • 如果请求的 controller_epoch 值小于内存中的 controller_epoch 值,则认为这个请求 是向已经过期的控制器所发送的请求,那么这个请求会被认定为无效的请求
  • 如果请求的 controller_epoch 值 大于 内存中的 controller_epoch 值,那么 说明已经有新的控制器当选了

由此可见,Kafka 通过 controller_epoch 来保证控制器的唯一性,进而保证相关操作的一致性。

控制器作用

下面我们再来看一看,具备控制器身份的 broker 比其他普通的 broker 多了哪些作用。

  1. 监听分区相关的变化,包括:

    1. 为 ZooKeeper 中的 /admin/reassign_partitions 节点注册 PartitionReassignmentHandler,用来处理 分区重分配 的动作。
    2. 为 ZooKeeper 中的 /isr_change_notification 节点注册 IsrChangeNotificetionHandler,用来处理 ISR 集合变更 的动作。
    3. 为 ZooKeeper中的 /admin/preferred-replica-election 节点添加 PreferredReplicaElectionHandler,用来处理 优先副本选举 动作。
  2. 监听主题相关的变化,包括:

    1. 为 ZooKeeper 中的 /brokers/topics 节点添加 TopicChangeHandler,用来处理 主题增减 的变化;
    2. 为 ZooKeeper 中的 /admin/delete_topics 节点添加 TopicDeletionHandler,用来处理 删除主题 的动作。
  3. 监听 broker 相关的变化

    1. 为 ZooKeeper 中的 /brokers/ids 节点添加 BrokerChangeHandler,用来处理 broker 增减 的变化。
  4. 从 ZooKeeper 中读取获取当前所有与主题、分区及 broker 有关的信息并进行相应的管理

    1. 对所有主题对应的 ZooKeeper 中的 /brokers/topics/<topic> 节点添加 PartitionModificationsHandler,用来 监听主题中的分区分配变化
  5. 启动并管理分区状态机和副本状态机

  6. 更新集群的元数据信息

  7. 如果参数 auto.leader.rebalance.enable 设置为 true,则还会开启一个名为“auto-leader-rebalance-task”的定时任务来负责 维护分区的优先副本的均衡

线程安全

控制器在选举成功之后会读取 ZooKeeper 中各个节点的数据来 初始化上下文信息,并且需要管理这些上下文信息。比如为某个主题增加了若干分区,控制器在负责创建这些分区的同时要更新上下文信息,并且需要将这些变更信息同步到其他普通的 broker 节点中。

不管是监听器触发的事件,还是定时任务触发的事件,或者是其他事件都会读取或更新控制器中的上下文信息,那么这样就会涉及多线程间的同步。如果单纯使用锁机制来实现,那么整体的性能会大打折扣。

针对这一现象,Kafka 的控制器使用 单线程基于事件队列的模型,将每个事件都做一层封装,然后按照事件发生的先后顺序暂存到 LinkedBlockingQueue 中,最后使用一个专用的线程按照FIFO(First Input First Output,先入先出)的原则顺序处理各个事件,这样不需要锁机制就可以在多线程间维护线程安全

参考文档

  • 《深入理解 Kafka:核心设计与实践原理》—— 朱忠华

往期文章