文章目录
Kafka 核心技术与实战
深入Kafka内核
26 | 你一定不能错过的Kafka控制器
控制器组件(Controller),是 Apache Kafka 的核心组件。它的主要作用是在 Apache ZooKeeper 的帮助下管理和协调整个 Kafka 集群。 集群中任意一台 Broker 都能充当控制器的角色,但是,在运行过程中,只能有一个 Broker 成为控制器,行使其管理和协调的职责。
Apache ZooKeeper 框架
控制器是重度依赖 ZooKeeper 的。
Apache ZooKeeper 是一个提供高可靠性的分布式协调服务框架。 它使用的数据模型类似于文件系统的树形结构,根目录也是以“/”开始。该结构上的每个节点被称为 znode,用来保存一些元数据协调信息。
如果以 znode 持久性来划分,znode 可分为持久性 znode 和临时 znode。持久性 znode 不会因为 ZooKeeper 集群重启而消失,而临时 znode 则与创建该 znode 的 ZooKeeper 会话绑定,一旦会话结束,该节点会被自动删除。
ZooKeeper 赋予客户端监控 znode 变更的能力,即所谓的 Watch 通知功能。 一旦 znode 节点被创建、删除,子节点数量发生变化,抑或是 znode 所存的数据本身变更,ZooKeeper 会通过节点变更监听器(ChangeHandler)的方式显式通知客户端。
ZooKeeper 常被用来实现集群成员管理、分布式锁、领导者选举等功能。Kafka 控制器大量使用 Watch 通知功能实现对集群的协调管理。
控制器是如何被选出来的?
Broker 在启动时,会尝试去 ZooKeeper 中创建 /controller 节点。Kafka 当前选举控制器的规则是:第一个成功创建 /controller 节点的 Broker 会被指定为控制器。
控制器是做什么的?
1.主题管理(创建、删除、增加分区)
主题管理,就是指控制器对 Kafka 主题的创建、删除以及分区增加的操作。执行 kafka-topics 脚本时,大部分的后台工作都是控制器来完成的。
2.分区重分配
分区重分配,主要是指 kafka-reassign-partitions 脚本提供的对已有主题分区进行细粒度的分配功能。
3.Preferred 领导者选举
Preferred 领导者选举,主要是 Kafka 为了避免部分 Broker 负载过重而提供的一种换 Leader 的方案。
4.集群成员管理(新增 Broker、Broker 主动关闭、Broker 宕机)
集群成员管理,包括自动检测新增 Broker、Broker 主动关闭及被动宕机。这种自动检测是依赖于 Watch 功能和 ZooKeeper 临时节点组合实现的。
比如,控制器组件会利用 Watch 机制检查 ZooKeeper 的 /brokers/ids 节点下的子节点数量变更。目前,当有新 Broker 启动后,它会在 /brokers/ids 下创建专属的 znode 节点。一旦创建完毕,ZooKeeper 会通过 Watch 机制将消息通知推送给控制器,这样,控制器就能自动地感知到这个变化,进而开启后续的新增 Broker 作业。
侦测 Broker 存活性则是依赖于刚刚提到的另一个机制:临时节点。每个 Broker 启动后,会在 /brokers/ids 下创建一个临时 znode。 当 Broker 宕机或主动关闭后,该 Broker 与 ZooKeeper 的会话结束,这个 znode 会被自动删除。同理,ZooKeeper 的 Watch 机制将这一变更推送给控制器,这样控制器就能知道有 Broker 关闭或宕机了,从而进行“善后”。
5.数据服务
数据服务,就是向其他 Broker 提供数据服务。控制器上保存了最全的集群元数据信息,其他所有 Broker 会定期接收控制器发来的元数据更新请求,从而更新其内存中的缓存数据。
控制器保存了什么数据?
上图中展示的数据几乎把所有 Kafka 集群的数据都囊括进来了。比较重要的数据有:
- 所有主题信息。 包括具体的分区信息,比如领导者副本是谁,ISR 集合中有哪些副本等。
- 所有 Broker 信息。 包括当前都有哪些运行中的 Broker,哪些正在关闭中的 Broker 等。
- 所有涉及运维任务的分区。 包括当前正在进行 Preferred 领导者选举以及分区重分配的分区列表。
这些数据其实在 ZooKeeper 中也保存了一份。每当控制器初始化时,它都会从 ZooKeeper 上读取对应的元数据并填充到自己的缓存中。 有了这些数据,控制器就能为其他 Broker 提供数据服务了,控制器通过向这些 Broker 发送请求的方式将这些数据同步到其他 Broker 上。
控制器故障转移(Failover)
在 Kafka 集群运行过程中,只能有一台 Broker 充当控制器的角色,那么这就存在单点失效(Single Point of Failure)的风险,Kafka 为控制器提供故障转移功能(Failover) 来应对单点失效。
故障转移是指,当运行中的控制器突然宕机或意外终止时,Kafka 能够快速地感知到,并立即启用备用控制器来代替之前失败的控制器。 这个过程就被称为 Failover,该过程是自动完成的,无需手动干预。
最开始时,Broker 0 是控制器。当 Broker 0 宕机后,ZooKeeper 通过 Watch 机制感知到并删除了 /controller 临时节点。之后,所有存活的 Broker 开始竞选新的控制器身份。Broker 3 最终赢得了选举,成功地在 ZooKeeper 上重建了 /controller 节点。之后,Broker 3 会从 ZooKeeper 中读取集群元数据信息,并初始化到自己的缓存中。至此,控制器的 Failover 完成,可以行使正常的工作职责了。
控制器内部设计原理
在 Kafka 0.11 版本之前,控制器设计的问题:
- 多线程设计。 控制器需要为每个 Broker 都创建一个对应的 Socket 连接,然后再创建一个专属的线程,用于向这些 Broker 发送特定请求。控制器连接 ZooKeeper 的会话,也会创建单独的线程来处理 Watch 机制的通知回调。除了以上这些线程,控制器还会为主题删除创建额外的 I/O 线程。
- 多线程访问共享控制器缓存数据。 为了保护数据安全性,控制器不得不在代码中大量使用 ReentrantLock 同步机制,这就进一步拖慢了整个控制器的处理速度。
最大的改进:
社区于 0.11 版本重构了控制器的底层设计,最大的改进就是,把多线程的方案改成了单线程加事件队列的方案。
社区引入了一个事件处理线程,统一处理各种控制器事件,然后控制器将原来执行的操作全部建模成一个个独立的事件,发送到专属的事件队列中,供此线程消费。这就是所谓的单线程 + 队列的实现方式。
值得注意的是,这里的单线程不代表之前提到的所有线程都被“干掉”了,控制器只是把缓存状态变更方面的工作委托给了这个线程而已。
这个方案的最大好处在于,控制器缓存中保存的状态只被一个线程处理,因此不再需要重量级的线程同步机制来维护线程安全,Kafka 不用再担心多线程并发访问的问题,非常利于社区定位和诊断控制器的各种问题(主要针对第二点问题)。
第二个改进:
针对控制器的第二个改进就是,将之前同步操作 ZooKeeper 全部改为异步操作。ZooKeeper 本身的 API 提供了同步写和异步写两种方式。之前控制器操作 ZooKeeper 使用的是同步的 API,性能很差,集中表现为,当有大量主题分区发生变更时,ZooKeeper 容易成为系统的瓶颈。0.11 版本 Kafka 修改了这部分设计,完全摒弃了之前的同步 API 调用,转而采用异步 API 写入 ZooKeeper,性能有了很大的提升。根据社区的测试,改成异步之后,ZooKeeper 写入提升了 10 倍(主要针对第一个问题)!