Kafka之Controller(Broker的领导者)

什么是Controller

在Kafka集群中,某个Broker将被选举出来担任一种特殊的角色,其用于管理和协调Kafka集群,即管理集群中的所有分区的状态并执行相应的管理操作。

每个Kafka集群任意时刻都只能有一个Controller。

当集群启动时,所有Broker都参与Controller的竞选,最终有一个胜出,一旦Controller在某个时刻崩溃,集群中的其他的Broker会收到通知,

然后开启新一轮的Controller选举,新选举出来的Controller将承担起之前Controller的所有工作。

状态管理

Controller维护的状态分为两类:

  1. 每台Broker上的分区副本信息
  2. 每个分区的Leader副本信息

1. 副本状态机 (Replica State Machine)

Kafka为副本定义了7种状态以及每个状态之间的流转规则,状态列表如下:

  1. NewReplica

    controller创建副本时的最初状态,当处于这个状态时,副本只能成为follower副本。

  2. OnlineReplica

    副本启动后,会变成该状态,在该状态下,副本可以成为follower或leader副本。

  3. OfflineReplica

    副本所在的Broker崩溃,将变为该状态。

  4. ReplicaDeletionStarted

    开启topic删除操作后,topic下的所有分区副本将变为该状态。

  5. ReplicaDeletionSuccessful

    若副本成功响应了删除副本请求,进入该状态

  6. ReplicaDeletionIneligible

    若副本删除失败,进入该状态

  7. NonExistentReplica

    若副本删除成功,进入该状态

副本状态流转过程

  1. 某topic新建时,该topic下的所有分区的所有副本都是 NonExistentReplica 状态。

  2. controller加载Zookeeper中该topic每个分区的所有副本信息到内存中,同时将副本状态变为 NewReplica

    随后controller选择该分区副本列表中的第一个副本作为分区的leader副本被设置所有副本进入ISR,然后在Zookeeper中持久化该决定。

  3. 当确定了分区的Leader和ISR后,controller将这些信息以请求的形式发送给所有副本,同时将副本状态同步到集群的所有broker

    最后controller将分区的所有副本状态设置为OnlineReplica。

  4. 当开启topic删除操作时,controller尝试停止所有副本,此时副本停止向leader获取数据

    如果停止的是leader副本本身,则 controller 会设置该分区的 leader 为 NO_LEADER,之后副本进入 Offline 状态。

    当所有的副本都进入Offline状态时,Controller将副本的状态变更为 ReplicaDeletionStarted 状态表示删除 topic 任务开启,在这一步的过程中,Controller向这些副本的Broker发送请求,让它们删除本机上的副本数据,

    一旦删除成功后,副本状态将变更为 ReplicaDeletionSuccessful 状态;失败的副本状态将进入 ReplicaDeletoinIneligible 表示暂时无法删除该副本,等到Controller重试,

    进入 ReplicaDeletionSuccessful 状态的副本后续会自动变更为 NonExistent 终止状态,同时Controller 的上下文缓存会清除这些副本信息。

以上就是副本状态流转的典型场景。

2. 分区状态机 (Partition State Machine)

分区状态列表如下:

  1. NonExistent

    表示不存在或者已删除的分区。

  2. NewPartition

    分区被创建时,处于这种状态,此时Kafka为分区确定了副本列表,但还未选举出leader和ISR。

  3. OnlinePartition

    一旦分区Leader被选出,分区进入此状态,正常工作的分区都处于这种状态。

  4. OfflinePartition

    分区处于正常工作过程中,leader所在的Broker宕机,则分区进入此状态,表示无法正常工作。

分区状态流转过程

  1. 新创建一个topic时,Controller负责创建分区对象,并设置分区状态为NonExistent,随后从Controller的上下文信息中读取副本分配方案,最后设置分区状态为 NewPartition

  2. 处于NewPartition状态的分区尚未有 leader 和 ISR,因此 Controller 会初始化 leader 和 ISR 信息并设置分区状态为 OnlinePartition,此时分区开始正常工作

  3. 若用户发起删除Topic操作或关闭Broker操作,那么Controller会令受影响的分区进入到Offline状态

    如果是删除Topic操作,Controller将开启分区下副本的删除操作,并最终将分区设置为NonExistent。

    如果是关闭Broker操作,则Controller判断该Broker是否是分区的leader,如果是则需要开启新一轮的leader选举并调整分区状态至 OnlinePartition。

Controller职责

集群状态的维护是Controller保持运行状态一致性的基本要素,如果要持续稳定的对外提供服务,除了集群状态还有其他的工作需要Controller负责,如下:

  1. 更新集群元数据信息

    一个client可以向集群中任意一台Broker发送 METADATA 请求来查询 topic 的分区信息 (例如topic有多少个分区、每个分区的leader在哪台Broker上以及分区的副本列表),

    随着集群的运行,元数据信息可能会发生变化,因此Controller必须提供一种机制,用于随时随地将变更后的分区信息广播出去,同步给集群中所有的Broker,

    具体做法是,当有分区信息发生变更时,Controller将变更后的信息封装进 UpdateMetaRequests 请求中,然后发送给集群中的每个Broker,后续 client 在请求数据时就可以获取最新、最及时的分区信息。

  2. 创建topic

    Controller启动时会创建一个Zookeeper的监听器,该监听器的唯一任务就是监控Zookeeper节点/brokers/topics下节点的变更情况。

    通过client或admin进行topic创建的方式主要有下面3种:

    • 通过kafka-topics脚本的--create创建
    • 构造CreateTopicsRequest请求创建
    • 配置 broker 端参数 auto.create.topics.enabletrue,然后发送MetadataRequest请求

    无论是上面哪种方式,最终都是在Zookeeper的/brokers/topics下创建一个对应的znode,然后将该topic分区以及对应的副本列表写入到这个znode中,

    Controller监听器一旦监控到该目录下有新增znode,立即触发topic的创建逻辑,即为新建Topic的每个分区确定leader和ISR,然后更新集群的元数据信息,

    之后Controller将创建一个新的监听器用于监听Zookeeper的/brokers/topics/<新增 topic>节点内容的变更,之后该topic分区发生变化时,Controller可立即收到通知。

  3. 删除topic

    标准的Kafka删除Topic方式有两种

    • 通过 kafka-topics 脚本的 --delete 来删除topic

    • 构造 DeleteTopicsRequest

    这两种方式都是向Zookeeper的/admin/delete_topics下新建一个znode,Controller启动时会创建一个监听器专门监听该路径下的子节点变更情况,

    当发现有新增节点,则Controller开启删除Topic的逻辑,该逻辑主要分为两个阶段:

    1. 停止所有副本运行

    2. 删除所有副本的日志数据

    一旦完成这些操作,Controller将移除 /admin/delete_topics/<待删除topic> 节点,表示Topic删除操作正式完成。

  4. 分区重分配

    分区重分配操作一般由Kafka集群的管理员发起,用于对Topic的所有分区重新分配副本所在Broker的位置,达到更均匀的分配效果,

    该操作中,管理员需要手动制定分配方案并按照执行的格式写入Zookeeper的/admin/reassign_partitions 节点下。

    分区副本重分配的过程实际上是先扩展再收缩的过程,Controller首先将分区副本集合进行扩展,等待它们全部与leader保持同步之后,从新分配方案中的副本选举leader,最后执行收缩阶段,将分区副本集合缩减成分配方案中的副本集合。

  5. preferred leader选举

    为了避免分区副本分配不均匀,Kafka引入了 preferred 副本的概念,例如一个分区的副本列表是 [1,2,3],那么broker 1就被称为该分区的preferred leader,因为它位于副本列表的第一位。

    在集群运行过程中,分区的leader会因为各种各样的原因发生变更,从而使得leader不再是preferred leader,此时用户可以自行通过命令将分区的leader重新调整为 preferred leader,方法有如下两种:

    • 设置broker端参数 auto.leader.rebalance.enable 为true,Controller将定时自动调整 preferred leader

    • 通过 kafka-preferred-replica-election 脚本手动触发

    上面两种方式都向 Zookeeper 的 /admin/preferred_replica_election 节点写入数据,同时,Controller将注册该目录的监听器,一旦触发,Controller将对应分区的 leader 调整回副本列表中的第一个副本,并将此变更广播出去。

  6. topic分区拓展

    在Kafka集群运行过程中,用户可能会发现某Topic的现有分区不足以支撑 client 的业务量,因此需要新增分区,

    目前增加分区主要使用kafka-topics脚本的--alter选项来完成,和创建topic一样,它会向Zookeeper的/brokers/topics/<topic>节点下写入新的分区目录,

    因为topic在创建的时候,Controller已经注册过一个监听器用于监听分区目录数据的变化,因此一旦新增了topic分区,该监听器就会被触发,执行对应的分区创建任务(例如选举leader和ISR),之后更新集群的元数据。

  7. broker加入集群

    每个Broker成功启动之后都会在Zookeeper的/broker/ids下创建一个znode,并写入broker的信息,为了动态维护Broker列表,

    Kafka注册一个Zookeeper监听器用于时刻监控该目录下的数据变化,每当有新Broker加入集群时,该监听器会感知到变化,执行对应的Broker启动任务,之后更新集群元数据信息并广而告之。

  8. broker崩溃

    Broker加入集群时,注册的是临时节点znode,因此一旦Broker崩溃,与Zookeeper的会话失效,临时节点就会被立即删除,

    因此上面的监听器也可以监视因为崩溃而退出集群的Broker列表,如果发现Broker子目录消失,Controller可立即知道该Broker退出集群,从而开启Broker退出逻辑,最后更新集群元数据并同步到其他Broker上。

  9. 受控关闭

    受控关闭指通过kafka-server-stop脚本、kill -15的方式关闭Broker,Broker崩溃一般是机器突然断电或kill -9导致的,

    受控关闭可以最大程度的降低Broker的不一致性,由即将关闭的Broker向Controller发送 ControlledShutdownRequest,发送完毕后,待关闭Broker将处于阻塞状态,直到收到 ControlledShutdownResponse 表示关闭成功,或用完所有重试机会后强制退出,

    Controller在完成必要的leader重选举和ISR收缩调整之后,会向待关闭Broker发送ControlledShutdownResponse表示该Broker现在可以正常退出。

    受控关闭是由待关闭Broker发起RPC请求给Controller来实现的,并没有依赖Zookeeper,之前的功能都依赖了Zookeeper来做。

  10. controller leader选举

    如果Kafka当前集群中的Controller发生故障或显式关闭,Kafka必须保证可以及时选出新的leader,即故障转移(fail-over),一般会导致Controller leader选举的场景有如下几种:

    1. 关闭Controller所在Broker
    2. 当前Controller所在的Broker宕机或崩溃
    3. 手动删除Zookeeper的/controller节点
    4. 手动向Zookeeper的/controller节点写入新的broker id

    上面4种操作都是修改了/controller节点内容,因此Controller只需要做一件事情:创建一个监听该目录的监听器。

    /controller本质上是一个临时节点,节点保存了当前Controller所在的broker id,集群首次启动时所有broker都会争抢该节点,但Zookeeper保证最终只有一个Broker胜出并成为Controller,

    一旦成为Controller,它就会增加Controller的版本号,即更新/controller/_epoch的节点值,然后履行上面的所有职责,

    对于没有成为Controller的broker而言,它们将继续监听/controller节点的存活情况,并随时准备竞选为新的controller

Controller与Broker间的通信

Controller启动时会为集群中所有Broker创建一个专属的Socket连接(也包括Controller所在的Broker),如果集群中有100台Broker机器,那么Controller也会创建100个Socket连接,

Controller还会为每个TCP连接创建一个RequestSendThread线程,这些连接被用于让Controller给集群Broker发送请求,目前的请求有如下几种:

  1. UpdateMetadataRequest:更新集群元数据请求,该请求携带了集群当前最新的元数据信息,收到该请求后,Broker将更新本地内存中的缓存信息,从而保证返回给client的信息总是最新、最及时的。

  2. LeaderAndIsrRequest:用于创建分区、副本,同时完成作为leader和作为follower角色各自的逻辑。

  3. StopReplicaRequest:停止指定副本的数据请求操作,另外还负责删除副本数据的功能。

通常情况下,Controller一般主动发送请求给Broker,不过受控关闭是个例外,由Broker主动发送ControlledShutdownRequest 请求给Controller,

并且,Controller大部分功能是依赖Zookeeper触发完成,而受控关闭则是Broker与Controller之间直接完成。

Controller组件

Controller由下图中的组件组成,看似很多,不过可以分为数据类、基础功能类、状态机类、选举器和监听器等。

1. 数据类 ControllerContext

ControllerContext 被称为 Controller上下文 或 Controller缓存,其汇总了Zookeeper中关于Kafka集群的所有元数据信息,是Controller能正确提供服务的基础,其几乎囊括了Kafka集群的一切信息。

2. 基础功能类

  1. ZkClient:封装与Zookeeper的各种交互API,Controller与Zookeeper的所有操作都交由该组件完成。

  2. ControllerChannelManager:Controller需要向其他Broker发送请求,通过该组件完成。

  3. ControllerBrokerRequestBatch:Controller将发往同一Broker的各种请求按照类型分组,之后统一发送提升效率,通过该组件完成管理请求batch。

  4. RequestSendThread:负责给其他Broker发送请求的I/O线程。

  5. ZookeeperLeaderElector:结合Zookeeper负责Controller的leader选举。

3. 状态机类

  1. ReplicaStateMachine:副本状态机,负责定义副本状态以及合法的副本状态流转。

  2. PartitionStateMachine:分区状态机,负责定义分区状态以及合法的分区状态流转。

  3. TopicDeletionManager:topic删除状态机,处理删除topic的各种状态流转以及相应的状态变更。

4. 选举器类

Controller提供了4个选举器用于各种情况下的leader选举,此处的leader选举是为分区进行leader选举而非选举Controller:

  1. OfflinePartitionLeaderSelector: 负责常规性的分区Leader选举。

  2. ReassignPartitionLeaderSelector: 负责用户发起分区重分配时的Leader选举。

  3. PerferredReplicaPartitionLeaderSelector: 负责用户发起 perferred leader 选举时的 Leader 选举。

  4. ControlledShutdownLeaderSelector: 负责Broker在受控关闭后的Leader选举。

5. Zookeeper监听器

Controller自己维护的监听器有3个:

  1. PartitionsReassignedListener:监听Zookeeper下分区重分配路径的数据变更情况。

  2. PreferredReplicaElectionListener:监听Zookeeper下的preferred leader选举路径的数据变更。

  3. IsrChangeNotificationListener:监听Zookeeper下的ISR列表变更通知路径下的数据变化。

    这个监听器的意思是,Kafka一旦发现topic分区的ISR发生了变化,就会在Zookeeper的/isr_change_notification 节点下写入一个新的数据节点,

    里面封装了集群中哪些Topic的哪些分区对应的ISR发生了变更,该监听器监控到节点变化后会发起 更新元数据 的请求给集群中的所有Broker。

Controller定义的监听器不止上面的3个,只是上面的3个是由Controller自己维护的,其他的交给各个状态机分别维护。

新老版本Controller设计对比

0.11之前的版本被称为老版本。

0.11之前的版本

老版本的设计有如下缺陷:

  1. 多线程共享状态

    Controller中创建了许多线程用于对上下文信息的访问,例如RequestSendThread线程,ZkClient创建的Zookeeper回调逻辑处理线程,以及TopicDeletionManager创建的线程和其他I/O线程等,

    由于很多线程对上线文进行访问,加锁是必要的,当前版本的Controller使用 monitor 锁实现,因此没有并行度可言。

  2. 代码组织混乱

    KafkaController、PartitionStateMachine、ReplicaStateMachine 每个都是500多行的大类且彼此调用混乱,

    比如KafkaController的stopOldReplicasOfReassignedPartition调用ReplicaStateMachine的handleStateChanges方法,二者又调用KafkaController的remoteReplicaFromIsr方法,

    类似的情况还发生在 KafkaControllerControllerChannelManager 之间。

  3. 管理类请求与数据类请求未分离

    Broker对入站请求类型不做任何优先级处理,不论是 PRODUCE 请求、FETCH 请求还是 Controller类的请求,

    这样会造成一个问题,即client发送的数据类请求积压导致Broker推迟了管理类请求的处理,假设有这么一个场景,

    Controller向Broker广播了Leader发生变更,于是新Leader开始接收Client端的请求,而同时老Leader所在的Broker由于出现了数据类型请求的积压,使得它一直忙于处理这些请求而无法处理Controller发来的LeaderAndIsrRequest请求,

    因此就会出现双主的情况,即脑裂,如果此时client发送的一个PRODUCE请求未指定acks=-1,那么由于日志水印截断的缘故,这个请求包含的信息可能丢失。

  4. Controller同步写Zookeeper是一个分区一个分区的写

    Controller操作Zookeeper通过ZkClient完成,ZkClient目前是同步写入Zookeeper,同步意味着性能不高,更严重的是,Controller是一个分区一个分区的进行写入,对于分区很多的集群来说,这是一个巨大的性能瓶颈。

    PartitionStateMachineelectLeaderForPartition就是一个分区一个分区的进行选举的。

  5. Controller一个分区一个分区的发送请求

    Controller发送请求按照分区级别发送,没有batch或并行,效率比较低。

  6. Controller给Broker的请求无版本号信息

    这里的版本号类似于Java版本中的Consumer中的generation,即需要一种机制告诉 Controller Broker 的版本信息,

    有些情况下Broker可能会处理已经失效的过期请求,从而导致Broker状态不一致,比如,一个Broker正常关闭过程中宕机了,那么重启之后这个Broker有可能处理之前Controller发送过来的 StopReplicaRequest,导致某些副本被设置为Offline,进而无法使用。

  7. ZkClient阻碍状态管理

    Controller目前使用ZkClient和Zookeeper打交道,其可以自动重建会话并使用特有的线程顺序处理所有的Zookeeper监听消息,因为是顺序处理,可能无法及时响应最新的状态变更,导致Kafka集群状态不一致。

0.11版本

在0.11版本中,对Controller的改进略微有限,仅改造了Controller的多线程事件处理模型,改为使用单线程基于事件队列的模型,

切换到单线程事件队列模型可以极大降低并发同步的开销,使用JavaLinkedBlockingQueue可以很容易的在多线程之间维护线程安全,因此不需要在多个线程上使用锁机制保护Controller的状态。

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值