一 导读

Kafka是一个基于Scala开发的多分区、多副本且基于ZooKeeper协调的分布式消息系统,它以高吞吐、可水平扩展、支持流数据处理等多种特性而被广泛使用,本文带大家了解Kafka服务端核心模块Kafka Controller。

二 Kafka简介

Kafka起初由LinkedIn开发,后捐赠给Apache,主要用于日志收集、实时计算、MySQL Binlog日志分发等业务。Kafka集群由若干台Broker、若干台Producer、若干台Consumer组成,此外Kafka集群需要依赖ZooKeeper集群做协调,完成元数据管理、优先副本选举、管理Topic等工作。其中Producer和Consumer属于Kafka客户端部分,Broker和ZooKeeper属于Kafka服务端部分。


如何使用kafka集群创建主题 kafka集群工作原理_定时任务

Kafka集群

Kafka某个Topic的消息,会先写入页缓存,再异步顺序写入该Topic对应的分区日志文件,完成数据落盘。Kafka使用页缓存、顺序写、零拷贝方式,使得Kafka具有非常高的吞吐量。由于分区是支持多副本的,这是Kafka高可用的前提。如何集中化管理Kafka集群的Broker、Topic、分区,是Kafka集群的一大难题,也是Kafka集群极其重要的工作,事实上,Kafka是通过Controller来完成这些工作的。

三 Kafka Controller

Kafka Controller,其实就是一个Kafka集群中一台Broker,只不过拥有Controller身份的Broker,需要额外承担管理Kafka集群的Broker、Topic、分区等职责,Kafka通过抢注ZooKeeper临时节点/controller的方式指定Controller。同时,Controller支持Failover(失效转移),Failover使得Kafka集群不会因为Controller服务中断或者突然宕机而导致整个集群不可用,这也是Kafka高可用的另一个重要保障。

3.1 职责

拥有Controller身份的Broker,除了要像普通Broker那样对外提供消息的生产、消费、同步功能之外,还需多承担以下几份职责:

  • 监听分区的变化。对ZooKeeper中的/admin/reassign_partitions节点注册ReassignmentHandler,用来处理分区重分配;对ZooKeeper中的/isr_change_notification节点注册ChangeNotificetionHandler,用来处理ISR变更;对ZooKeeper中的/admin/preferred-relica-election节点注册PreferredReplicaElectionHandler,用来处理优先副本的选举。
  • 监听Topic的变化。对ZooKeeper中的/brokers/topics节点注册TopicChangeHandler,用来处理Topic的增减变化;对ZooKeeper中的/admin/delete_topics节点注册TopicDeleteHandler,用来处理删除Topic。
  • 监听Broker的变化。对ZooKeeper中的/brokers/ids节点注册BrokerChangeHandler,用来处理Broker增减的变化。
  • 从ZooKeeper中读取当前所有与Topic、分区以及Broker有关的信息并进行相应的管理。对所有Topic对应的ZooKeeper中的/brokers/topics/<topic>节点添加PartitionModificationsHandler,用来监听Topic中的分区分配变化。
  • 自动平衡优先副本。如果Broker启动参数auto.leader.rebalance.enable设置为true,Controller会开启定时任务,默认每五分钟(启动参数可配置)计算一次Kafka集群优先副本的不平衡率,如果超过10%(启动参数可配置)会自动平衡分区优先副本,避免因为分区优先副本不均衡造成流量不均匀,进而影响个别Broker性能。

总结起来,Controller就是注册一些监听器,监听事件做相应的处理,以及开启一些定时任务,做相应的处理。二者的区别在于,监听器能快速做出相应处理,比如Topic变更,这些事件的实时性要求很高,要求系统立即反应。优先副本不平衡,只是Broker读写流量不均衡,有一定程度的不平衡,对于Kafka集群来说是可以接受的,因此像分区优先副本自动平衡就可以用定时任务来处理。

3.2 选举

Kafka使用公平竞选的方式来确定Controller,最先在ZooKeeper成功创建临时节点/controller的Broker会成为Controller,因为Broker不会同时启动,一般而言,Kafka集群中第一台启动的Broker会成为Controller,并将版本、自身Broker编号、时间戳写入ZooKeeper临时节点/controller。


如何使用kafka集群创建主题 kafka集群工作原理_重启_02

Controller信息

其中version固定为1,brokerid表示成为Broker编号,timestamp表示成为Controller的时间戳。每个Broker启动的时候都会去尝试读取ZooKeeper临时节点/controller的brokerid,如果读取到brokerid不为-1,则表示已有Broker竞选成为Controller。此时,当前Broker会放弃竞选Controller,并对ZooKeeper临时节点/controller注册监听器,当Controller因为某些情况导致Kafka进程停止,甚至所在的机器宕机,并在规定的时间没有自行恢复服务,ZooKeeper通过Watch机制感知到并删除临时节点/controller,然后监听器会通知各个Broker进行Controller选举,第一个抢注ZooKeeper临时节点/controller成功的,成为新的Controller,这就是Controller Failover。


如何使用kafka集群创建主题 kafka集群工作原理_定时任务_03

Controller Failover

Controller在选举成功后会拉取ZooKeeper中各个节点的信息来初始化ControllerContext,并管理这些ControllerContext,比如某个Topic增加若干个分区,Controller在负责创建这些分区的同时,还要更新ControllerContext,并且需要将这些变更信息同步到普通Broker。不管是监听器触发的事件,还是定时任务触发的事件,或者是其他事件都会更新Controller中的ControllerContext,那么这样就会涉及多线程同步。如果单纯使用锁机制来控制,那么Kafka集群整体性能会大打折扣,实际上,Kafka在0.10之前,都是通过锁机制的方式来处理ControllerContext的多线程同步问题。在0.11后改用单线程基于事件队列的模型,将每个事件都做一层封装,然后按事件发生的先后顺序暂存到LinkedBlockingQueue中,最后使用ControllerEventThread线程按照FIFO的原则来处理各个事件,这样就可以不用锁机制解决多线程同步问题。


如何使用kafka集群创建主题 kafka集群工作原理_定时任务_04

Controller原理

变更Controller,通常有以下几种方式,但是Controller转移,需要付出的代价也比较大,需要关闭原Controller各种监听器、定时任务和线程,并且新Controller上线同样开启以上资源。一般而言,除非Controller所在的机器IO、CPU、内存等资源不足,又或者频繁发生长时间Full GC,否则不应该人为变更Controller。

  • 手动更改ZooKeeper临时/controller中brokerid,每个Broker会更新内存中的ActiveControllerId。
  • 手动删除ZooKeeper临时节点/controller,触发一轮新的选举。
  • 停止当前Controller Kafka进程,此时ZooKeeper会认为Controller所在的Broker宕机,ZooKeeper同样会删除临时节点/controller,触发一轮新的选举。

3.3 脑裂

随着服务的长时间运行,加上Controller对比普通Broker多承担一部分额外的工作,Controller不可避免发生Full GC,导致服务停止,如果Full GC时间很长,甚至超过Broker与ZooKeeper设置的会话时间,此时ZooKeeper会认为Controller下线,进行Controller Failover。


如何使用kafka集群创建主题 kafka集群工作原理_如何使用kafka集群创建主题_05

Controller假死

选举产生新的Controller之后,ZooKeeper会通知到各个Broker更新内存中的ActiveControllerId,原先发生Full GC的Controller因为停止服务收不到Controller更新通知,在Full GC过后,原先的Controller认为自身服务正常,只是假死了一段时间,甚至还认为自己是Controller,此时Kafka集群就会有两个Controller,这就是Controller脑裂问题。


如何使用kafka集群创建主题 kafka集群工作原理_监听器_06

Controller脑裂

为了解决Controller脑裂问题,ZooKeeper中还有一个与Controller有关的持久节点/controller_epoch,存放的是一个整形值,用于记录Controller发生变更的次数,即记录当前是第几代Controller,也称为纪元编号。Controller和普通Broker的请求都会携带controller_epoch,如果请求的controller_epoch小于ActiveControllerId,则认为这个请求是已经过期的Controller发送的无效请求,此时普通Broker。如果请求的controller_epoch大于内存中ActiveControllerId,那么说明已经有新的Controller产生,当前Broker需更新内存中的ActiveControllerId。Kafka通过controller_epoch来保证Controller的唯一性,同时防止Controller脑裂。

3.4 监控

监控Controller个数尤为重要,需要为Controller建立监控指标,个数不为1的情况都需要特别留意,个数为0,意味着没有Broker成功注册ZooKeeper临时节点/controller,意味着Broker均与ZooKeeper断开连接,需要及时检查Broker和ZooKeeper状况;如果个数大于1,意味发生Controller脑裂,如果Broker版本在0.10之前(包含0.10),需要及时找到失效的Controller,通过运维的手段踢出Kafka集群,再重新加入,成为普通的Broker。由于Kafka会在内存里面维护一些监控指标,并以JMX的方式对外提供,因此我们只需要在启动Kafka时开放JMX端口,再定时获取Controller对应的指标即可。


如何使用kafka集群创建主题 kafka集群工作原理_重启_07

Controller监控

四 最佳实践

由于Controller支持Failover,且集群中任意一台Broker均可能成为Controller,因此我们无须对Controller做额外的配置或者调优。唯一需要注意的是,对于消息敏感程度非常高的集群(例如用于下单、结算、清算等业务的集群)需要重启Broker(比如Broker升级,需要依次重启),需要在重启Broker之前确认身份,假如当前Broker是Controller,需要手动更改ZooKeeper临时/controller中brokerid,将Controller转移到其他Broker上再进行重启。因为ZooKeeper感知Controller下线,到新的Controller上线这个过程,少则几秒,多则十几秒,在这段时间内,集群没有Controller,会存在消息的生产、失败情况,提前转移Controller,能在最大程度保证集群高可用。


如何使用kafka集群创建主题 kafka集群工作原理_监听器_08

更改ZooKeeper临时节点/controller为Broker7,让当前Broker5放弃Controller

五 结语

本文通过讲解Controller的职责、选举、脑裂和监控,加深对Controller的认识,对于生产、消费客户端而言,它们并不关心Controller,甚至不知道有Controller的存在,但是对于服务端而言,Controller却是非常重要的组成部分,Controller除了要像普通Broker完成消息的生产、消费、同步等功能之外,还需承担管理分区、副本状态以及分区重分配等一些额外的工作,相当于Kafka集群的控制器, 并具备失效转移功能。对于运维层面,我们需要为Controller个数建立监控曲线,防止出现脑裂。