Kafka深度解析:揭秘选举机制下的分布式智慧与高效

#作者:猎人

1. 分布式常用选举策略

1.1 为什么需要选举?

  • 主节点:在一个分布式集群中负责对其他节点的协调和管理,有了它,就可以保证其他节点的有序运行,以及数据库集群中的写入数据在每个节点上的一致性。
  • 一致性:数据在每个集群节点中都是一样的

如果主节点故障,就需要由从节点中依靠选举方法择优选举出下一个主节点

目前分布式系统常用的选举策略为bully,zab及raft。

1.2.Bully

选举原则是“长者”为大,即在所有活着的节点中:取 ID 最大的节点作为主节点。
节点的角色有两种:普通节点和主节点。初始化时,所有节点都是普通节点,并且都有成为主的权利。(当选主成功后,有且仅有一个节点成为主节点,其他所有节点都是普通节点)
执行选举条件:当且仅当主节点故障或与其他节点失去联系后,才会重新选主。
选举过程中,需要用到以下消息:

  1. Election 消息,用于发起选举;
  2. Alive 消息,对 Election 消息的应答;
  3. Victory 消息,竞选成功的主节点向其他节点发送的宣誓主权的消息。
    假设条件:集群中每个节点均知道其他节点的 ID。

选举过程是:

  1. 集群中每个节点判断自己的 ID 是否为当前活着的节点中 ID 最大的,如果是,则直接向其他节点发送 Victory 消息,宣誓自己的主权;
  2. 如果自己不是当前活着的节点中 ID 最大的,则向比自己 ID 大的所有节点发送Election 消息,并等待其他节点的回复;
  3. 若在给定的时间范围内,本节点没有收到其他节点回复的 Alive 消息,则认为自己成为主节点,并向其他节点发送 Victory 消息,宣誓自己成为主节点;若接收到来自比自己ID 大的节点的 Alive 消息,则等待其他节点发送 Victory 消息;
  4. 若本节点收到比自己 ID 小的节点发送的 Election 消息,则回复一个 Alive 消息,告知其他节点,我比你大,重新选举。

优点:选举速度快、算法复杂度低、简单易实现。

缺点

  • 需要每个节点有全局的节点信息,因此额外信息存储较多;
  • 任意一个比当前主节点 ID 大的新节点或节点故障后恢复加入集群的时候,都可能会触发重新选举,成为新的主节点,如果该节点频繁退出、加入集群,就会导致频繁切主。

1.3.Zab(zookeeper使用)

  • 集群中每个节点拥有 3 种角色:

    1. Leader,主节点;
    2. Follower,跟随者节点;
    3. Observer,观察者,无投票权。
  • 选举过程中,集群中的节点拥有 4 个状态:

    1. Looking 状态,即选举状态。当节点处于该状态时,它会认为当前集群中没有 Leader,因此自己进入选举状态。
    2. Leading 状态,即领导者状态,表示已经选出主,且当前节点为 Leader。
    3. Following 状态,即跟随者状态,集群中已经选出主后,其他非主节点状态更新为Following,表示对 Leader 的追随。
    4. Observing 状态,即观察者状态,表示当前节点为 Observer,持观望态度,没有投票权和选举权。
  • 投票过程中,每个节点都有一个唯一的三元组 (server_id, server_zxID, epoch):

    • server_id 表示本节点的唯一 ID;
    • server_zxID 表示本节点存放的数据 ID,数据 ID 越大表示数据越新,选举权重越大;
    • epoch 表示当前选取轮数,一般用逻辑时钟表示*
  • 核心:“少数服从多数,ID 大的节点优先成为主”

  • 选举过程
    通过(vote_id, vote_zxID) 来表明投票给哪个节点,其中 vote_id 表示被投票节点的 ID,vote_zxID 表示被投票节点的服务器 zxID。
    流程如下:

    1. 刚开始投推选自己为leader,并将信息广播出去
    2. 每个站点都会收到其它人的信息,统计一下,知道其中谁最符合条件
    3. 开始广播自己新的推选人出去(也就是自己统计后觉得最符合的那个节点)
    4. 大家都推送那么最大的结点,自然他就成为了leader
    5. 虽有leader结点就会与其它结点建立心跳机制,维持监控
  • 选主的原则是:
    server_zxID 最大者成为 Leader;若 server_zxID 相同,则 server_id 最大者成为 Leader。

优点:算法性能高,对系统无特殊要求。稳定性比较好,当有新节点加入或节点故障恢复后,会触发选主,但不一定会真正切主,除非新节点或故障后恢复的节点数据 ID 和节点ID 最大,且获得投票数过半,才会导致切主。
缺点:采用广播方式发送信息,若节点中有 n个节点,每个节点同时广播,则集群中信息量为 n*(n-1)个消息,容易出现广播风暴;且除了投票,还增加了对比节点 ID 和数据 ID,这就意味着还需要知道所有节点的 ID 和数据ID,所以选举时间相对较长。

1.4.Raft

Raft算法性质:多数派投票选举算法,核心思想是“少数服从多数”,获得投票最多的节点成为主。启动时在集群中指定一些机器为Candidate,然后Candidate开始向其他机器(尤其是Follower)拉票,当某个Candidate的票数超过半数,它就成为leader。

集群节点的角色有:

  1. Leader,即主节点,同一时刻只有一个 Leader,负责协调和管理其他节点;
  2. Candidate,即候选者,每一个节点都可以成为 Candidate,节点在该角色下才可以被
    选为新的 Leader;
  3. Follower,Leader 的跟随者,不可以发起选举。

选举流程

  1. 初始化时,所有节点均为 Follower 状态。
  2. 开始选主时,所有节点的状态由 Follower 转化为 Candidate,并向其他节点发送选举请求。
  3. 其他节点根据接收到的选举请求的先后顺序,回复是否同意成为主。这里需要注意的是,在每一轮选举中,一个节点只能投出一张票。
  4. 若发起选举请求的节点获得超过一半的投票,则成为主节点,其状态转化为 Leader,其他节点的状态则由 Candidate 降为 Follower。Leader 节点与 Follower 节点之间会定期发送心跳包,以检测主节点是否活着。
  5. 当 Leader 节点的任期到了,即发现其他服务器开始下一轮选主周期时,Leader 节点状态由 Leader 降级为 Follower,进入新一轮选主。

请注意,每一轮选举,每个节点只能投一次票。这种选举就类似人大代表选举,正常情况下每个人大代表都有一定的任期,任期到后会触发重新选举,且投票者只能将自己手里唯一的票投给其中一个候选者。对应到 Raft 算法中,选主是周期进行的,包括选主和任值两个时间段,选主阶段对应投票阶段,任值阶段对应节点成为主之后的任期。但也有例外的时候,如果主节点故障,会立马发起选举,重新选出一个主节点。

优点:选举速度快、算法复杂度低、易于实现
缺点:它要求系统内每个节点都可以相互通信,且需要获得过半的投票数才能选主成功,因此通信量大。该算法选举稳定性比 Bully 算法好,这是因为当有新节点加入或节点故障恢复后,会触发选主,但不一定会真正切主,除非新节点或故障后恢复的节点获得投票数过半,才会导致切主

1.5.常用选举策略的问题

  • split-brain (脑裂)
    这是由ZooKeeper的特性引起的,虽然ZooKeeper能保证所有Watch按顺序触发,但是网络延迟,并不能保证同一时刻所有Replica“看”到的状态是一样的,这就可能造成不同Replica的响应不一致,可能选出多个领导“大脑”,导致“脑裂”。
  • herd effect (羊群效应)
    如果宕机的那个Broker上的Partition比较多, 会造成多个Watch被触发,造成集群内大量的调整,导致大量网络阻塞。
  • ZooKeeper负载过重
    每个Replica都要为此在ZooKeeper上注册一个Watch,当集群规模增加到几千个Partition时ZooKeeper负载会过重。

1.6.Kafka的选举策略

Kafka的Leader Election方案解决了上述选举策略面临的问题,它在所有broker中选出一个controller,所有Partition的Leader选举都由controller决定。
controller会将Leader的改变直接通过RPC的方式(比ZooKeeper Queue的方式更高效)通知需为此作为响应的Broker。

没有使用 zk,所以无负载过重问题;也没有注册 watch无羊群效应问题

leader 失败了,就通过 controller 继续重新选举即可,所以克服所有问题

2.Kafka基础架构

在这里插入图片描述

  • Producer:Producer即生产者,消息的产生者,是消息的入口。
  • Broker:Broker是kafka实例,每个服务器上有一个或多个kafka的实例,我们姑且认为每个broker对应一台服务器。每个kafka集群内的broker都有一个不重复的编号,如图中的broker-0、broker-1等……
  • Topic:消息的主题,可以理解为消息的分类,kafka的数据就保存在topic。在每个broker上都可以创建多个topic。
  • Partition:Topic的分区,每个topic可以有多个分区,分区的作用是做负载,提高kafka的吞吐量。同一个topic在不同的分区的数据是不重复的,partition的表现形式就是一个一个的文件夹
  • Replication:每一个分区都有多个副本,副本的作用是做备。当主分区(Leader)故障的时候会选择一个从(Follower)上位,成为Leader。在kafka中默认副本的最大数量是10个,且副本的数量不能大于Broker的数量,follower和leader绝对是在不同的机器,同一机器对同一个分区也只可能存放一个副本(包括自己)。
  • Message:每一条发送的消息主体。
  • Consumer:消费者,即消息的消费方,是消息的出口。
  • Consumer Group:我们可以将多个消费组组成一个消费者组,在kafka的设计中同一个分区的数据只能被消费者组中的某一个消费者消费。同一个消费者组的消费者可以消费同一个topic的不同分区的数据,这也是为了提高kafka的吞吐量
  • Zookeeper:kafka集群依赖zookeeper来保存集群的的元信息,来保证系统的可用性
  • Controller:在Kafka集群中会有一个或者多个broker,其中有一个broker会被选举为控制器(Kafka Controller),它负责管理整个集群中所有分区和副本的状态

3.Kafka controller选举

3.1.controller作用

  • 主题管理(创建、删除、增加分区)
    这里的主题管理,就是指控制器帮助我们完成对 Kafka 主题的创建、删除以及分区增加的操作。当我们执行kafka-topics 脚本时,大部分的后台工作都是控制器来完成的。
  • 分区重分配
    分区重分配主要是指,kafka-reassign-partitions 脚本提供的对已有主题分区进行细粒度的分配功能。这部分功能也是控制器实现的。
  • 集群成员管理
    自动检测新增 Broker、Broker 主动关闭及被动宕机。这种自动检测是依赖于ZooKeeper 的 Watch 功能和 ZooKeeper 的临时节点组合实现的。比如控制器组件会利用Watch 机制检查 ZooKeeper 的 /brokers/ids 节点下的子节点变化情况。
    当有新 Broker 启动后,它会在 /brokers 下创建专属的 znode 节点。一旦创建完毕,ZooKeeper 会通过 Watch 机制将消息通知推送给 Broker 控制器,这样,控制器就能自动地感知到这个变化,进而开启后续的新增 Broker 作业。
    侦测 Broker 存活性还是依赖于 ZooKeeper的临时节点。每个 Broker 启动后,会在 /brokers/ids 下创建一个临时 znode。当 Broker 宕机或主动关闭后,该 Broker 与 ZooKeeper 的会话结束,这个 znode 会被自动删除。同理,ZooKeeper 的 Watch 机制将这一变更推送给Broker 控制器,这样控制器就能知道有 Broker 关闭或宕机了,从而进行后续处理。
  • 数据服务
    控制器的最后一大类工作,就是向其他 Broker 提供数据服务,控制器上保存了最全的集群元数据信息,其他所有 Broker 会定期接收控制器发来的元数据更新请求,从而更新其内存中的缓存数据。
    当控制器发现一个 broker 离开集群,便会检测该broker 下是否有leader 分区,如果存在,控制器会依次遍历每个分区,根据 ISR 确定新的 leader,然后向所有 broker 发送消息,该请求消息包含谁是新的 leader 以及谁是 follower 。随后,新的 Leader 开始处理来自生产者和消费者的请求,Follower 用于从新的 Leader 那里进行复制。

3.2.触发选举条件

  1. 当某个分区的leader副本出现故障时,由控制器负责为该分区选举新的leader副本。
  2. 当检测到某个分区的ISR集合发生变化时,由控制器负责通知所有broker更新其元数据信息。
  3. 当使用kafka-topics.sh脚本为某个topic增加分区数量时,同样还是由控制器负责让新分区被其他节点感知到。

3.3.选举机制及流程

在这里插入图片描述

  • 启动时选举
    集群中第一个启动的broker会通过在zookeeper中创建临时节点/controller来让自己成为控制器,其他broker启动时会去尝试读取/controller节点的brokerid的值,读取到的brokerid的值不为-1知道已经有其他broker节点成功竞选为控制器,就会在zookeeper中创建watch对象,便于它们收到控制器变更的通知。

  • leader异常选举
    那么如果broker由于网络原因与zookeeper断开连接或者异常退出,那么其他broker通过watch收到控制器变更的通知,就会去尝试创建临时节点/controller,如果有一个broker创建成功,那么其他broker就会收到创建异常通知,也就意味着集群中已经有了控制器,其他broker只需创建watch对象即可。

  • follower异常
    如果集群中有一个broker发生异常退出了,那么控制器就会检查这个broker是否有分区的副本leader,如果有那么这个分区就需要一个新的leader,此时控制器就会去遍历其他副本,决定哪一个成为新的leader,同时更新分区的ISR集合。

  • broker加入
    如果有一个broker加入集群中,那么控制器就会通过brokerid去判断新加入的broker中是否含有现有分区的副本,如果有,就会从分区副本中去同步数据。

  • epoch防止脑裂
    Kafka通过controller_epoch来保证控制器的唯一性,进而保证相关操作的一致性。
    controller_epoch是一个整型值,存放在Zookeeper的/controller_epoch这个持久节点中;
    controller_epoch值用于记录控制器发生变更的次数,即记录当前的控制器是第几代控制器;controller_epoch的初始值为1,当控制器发生变更时,就将该字段值加1。每个和控制器交互的请求都会携带controller_epoch字段:
    如果请求的controller_epoch值小于内存中的controller_epoch值,则认为这个请求是向已经过期的控制器发送的请求,那么这个请求会被认定为无效的请求。
    如果请求的controller_epoch值大于内存中的controller_epoch值,那么说明已经有新的控制器当选了。

具备控制器身份的broker需要比其他普通的broker多一份职责,具体细节如下:

  1. 监听broker相关的变化。为Zookeeper中的/brokers/ids/节点添加BrokerChangeListener,用来处理broker增减的变化。
  2. 监听topic相关的变化。为Zookeeper中的/brokers/topics节点添加TopicChangeListener,用来处理topic增减的变化;为Zookeeper中的/admin/delete_topics节点添加TopicDeletionListener,用来处理删除topic的动作。
  3. 从Zookeeper中读取获取当前所有与topic、partition以及broker有关的信息并进行相应的管理。对于所有topic所对应的Zookeeper中的/brokers/topics/[topic]节点添加PartitionModificationsListener,用来监听topic中的分区分配变化。
  4. 更新集群的元数据信息,同步到其他普通的broker节点中。

4.Kafka partition选举

controller感知到分区leader所在的broker挂了(controller可以监听zookeeper /brokers/ids),controller会从ISR列表里挑第一个broker作为leader(第一个broker最先放进ISR列表,可能是同步数据最多的副本),如果参数unclean.leader.election.enable为true,代表在ISR列表里所有副本都挂了的时候可以在ISR列表以外的副本中选leader,这种设置,可以提高可用性,但是选出的新leader有可能数据少很多。

4.1.相关概念

AR(Assigned Repllicas): 一个分区里面所有的副本(不区分leader和follower)
ISR(In-Sync Replicas): 能够和leader保持同步的follower+leader本身组成的集合
OSR(Out-Sync Replicas): 不能和leader保持同步的follower集合

公式: AR=ISR+OSR

注意:

  1. kafka只会保证ISR集合中的所有副本保持完全同步。
  2. kafka一定会保证leader接收到消息然后完全同步给ISR中的所有副本
  3. ISR的机制保证了处于ISR内部的follower都可以和leader保持同步,一旦出现故障或者延迟(在一定时间内没有同步),就会被提出ISR

4.2.选举流程

在这里插入图片描述

  1. 去zookeeper节点 /broker/topics/{topic名称}/partitions/{分区号}/state 节点读取基本信息。
  2. 遍历从zk中获取的leaderIsrAndControllerEpoch信息,做一些简单的校验:zk中获取的数据的controllerEpoch必须<=当前的Controller的controller_epoch。最终得到 validLeaderAndIsrs, controller_epoch 就是用来防止脑裂的, 当有两个Controller当选的时候,他们的epoch肯定不一样, 那么最新的epoch才是真的Controller
  3. 如果没有获取到有效的validLeaderAndIsrs 信息 则直接返回
  4. 根据入参partitionLeaderElectionStrategy 来匹配不同的Leader选举策略。来选出合适的Leader和ISR信息
  5. 根据上面的选举策略选出的 LeaderAndIsr 信息进行遍历, 将它们一个个写入到zookeeper节点 /broker/topics/{topic名称}/partitions/{分区号}/state 中。 (当然如果上面没有选择出合适的leader,那么久不会有这个过程了)
  6. 遍历上面写入zk成功的分区, 然后更新Controller里面的分区leader和isr的内存信息 并发送LeaderAndISR请求,通知对应的Broker Leader更新了。

4.3.unclean.leader.election.enable参数

unclean.leader.election.enable参数从Kafka 0.11.0.0版本开始其默认值由原来的true改为false,当leader挂掉以后,需要从ISR中选择一个副本出来当leader,但是ISR中不一定有,所以就会根据unclean.leader.election.enable的值有不同处理办法:

  • 值为true,如果ISR中有,我就选择一个,如果没有,就从OSR队列中选择一个(就是不完全同步的队列)
  • 值为false,我必须要从ISR中找到一个,如果没有,当前服务不可用,就是一直等到原来挂掉的leader重启起来,加入ISR中,重新成为leader。

影响
如果unclean.leader.election.enable参数设置为true,就有可能发生数据丢失和数据不一致的情况,Kafka的可靠性就会降低;
如果unclean.leader.election.enable参数设置为false,Kafka的可用性就会降低。
具体怎么选择需要读者更具实际的业务逻辑进行权衡,可靠性优先还是可用性优先。从Kafka 0.11.0.0版本开始将此参数从true设置为false,可以看出Kafka的设计者偏向于可靠性,如果能够容忍uncleanLeaderElection场景带来的消息丢失和不一致,可以将此参数设置为之前的老值true

5.消费组Leader的选举

GroupCoordinator需要为消费组内的消费者选举出一个消费组的leader,这个选举的算法很简单,当消费组内还没有leader,那么第一个加入消费组的消费者即为消费组的leader,如果当前leader退出消费组,则会挑选以HashMap结构保存的消费者节点数据中,第一个键值对来作为leader,总体来说,这个选举是很随意的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值