#作者:猎人
文章目录
1. 分布式常用选举策略
1.1 为什么需要选举?
- 主节点:在一个分布式集群中负责对其他节点的协调和管理,有了它,就可以保证其他节点的有序运行,以及数据库集群中的写入数据在每个节点上的一致性。
- 一致性:数据在每个集群节点中都是一样的
如果主节点故障,就需要由从节点中依靠选举方法择优选举出下一个主节点
目前分布式系统常用的选举策略为bully,zab及raft。
1.2.Bully
选举原则是“长者”为大,即在所有活着的节点中:取 ID 最大的节点作为主节点。
节点的角色有两种:普通节点和主节点。初始化时,所有节点都是普通节点,并且都有成为主的权利。(当选主成功后,有且仅有一个节点成为主节点,其他所有节点都是普通节点)
执行选举条件:当且仅当主节点故障或与其他节点失去联系后,才会重新选主。
选举过程中,需要用到以下消息:
- Election 消息,用于发起选举;
- Alive 消息,对 Election 消息的应答;
- Victory 消息,竞选成功的主节点向其他节点发送的宣誓主权的消息。
假设条件:集群中每个节点均知道其他节点的 ID。
选举过程是:
- 集群中每个节点判断自己的 ID 是否为当前活着的节点中 ID 最大的,如果是,则直接向其他节点发送 Victory 消息,宣誓自己的主权;
- 如果自己不是当前活着的节点中 ID 最大的,则向比自己 ID 大的所有节点发送Election 消息,并等待其他节点的回复;
- 若在给定的时间范围内,本节点没有收到其他节点回复的 Alive 消息,则认为自己成为主节点,并向其他节点发送 Victory 消息,宣誓自己成为主节点;若接收到来自比自己ID 大的节点的 Alive 消息,则等待其他节点发送 Victory 消息;
- 若本节点收到比自己 ID 小的节点发送的 Election 消息,则回复一个 Alive 消息,告知其他节点,我比你大,重新选举。
优点:选举速度快、算法复杂度低、简单易实现。
缺点:
- 需要每个节点有全局的节点信息,因此额外信息存储较多;
- 任意一个比当前主节点 ID 大的新节点或节点故障后恢复加入集群的时候,都可能会触发重新选举,成为新的主节点,如果该节点频繁退出、加入集群,就会导致频繁切主。
1.3.Zab(zookeeper使用)
-
集群中每个节点拥有 3 种角色:
- Leader,主节点;
- Follower,跟随者节点;
- Observer,观察者,无投票权。
-
选举过程中,集群中的节点拥有 4 个状态:
- Looking 状态,即选举状态。当节点处于该状态时,它会认为当前集群中没有 Leader,因此自己进入选举状态。
- Leading 状态,即领导者状态,表示已经选出主,且当前节点为 Leader。
- Following 状态,即跟随者状态,集群中已经选出主后,其他非主节点状态更新为Following,表示对 Leader 的追随。
- 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。
流程如下:- 刚开始投推选自己为leader,并将信息广播出去
- 每个站点都会收到其它人的信息,统计一下,知道其中谁最符合条件
- 开始广播自己新的推选人出去(也就是自己统计后觉得最符合的那个节点)
- 大家都推送那么最大的结点,自然他就成为了leader
- 虽有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。
集群节点的角色有:
- Leader,即主节点,同一时刻只有一个 Leader,负责协调和管理其他节点;
- Candidate,即候选者,每一个节点都可以成为 Candidate,节点在该角色下才可以被
选为新的 Leader; - Follower,Leader 的跟随者,不可以发起选举。
选举流程:
- 初始化时,所有节点均为 Follower 状态。
- 开始选主时,所有节点的状态由 Follower 转化为 Candidate,并向其他节点发送选举请求。
- 其他节点根据接收到的选举请求的先后顺序,回复是否同意成为主。这里需要注意的是,在每一轮选举中,一个节点只能投出一张票。
- 若发起选举请求的节点获得超过一半的投票,则成为主节点,其状态转化为 Leader,其他节点的状态则由 Candidate 降为 Follower。Leader 节点与 Follower 节点之间会定期发送心跳包,以检测主节点是否活着。
- 当 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.触发选举条件
- 当某个分区的leader副本出现故障时,由控制器负责为该分区选举新的leader副本。
- 当检测到某个分区的ISR集合发生变化时,由控制器负责通知所有broker更新其元数据信息。
- 当使用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多一份职责,具体细节如下:
- 监听broker相关的变化。为Zookeeper中的/brokers/ids/节点添加BrokerChangeListener,用来处理broker增减的变化。
- 监听topic相关的变化。为Zookeeper中的/brokers/topics节点添加TopicChangeListener,用来处理topic增减的变化;为Zookeeper中的/admin/delete_topics节点添加TopicDeletionListener,用来处理删除topic的动作。
- 从Zookeeper中读取获取当前所有与topic、partition以及broker有关的信息并进行相应的管理。对于所有topic所对应的Zookeeper中的/brokers/topics/[topic]节点添加PartitionModificationsListener,用来监听topic中的分区分配变化。
- 更新集群的元数据信息,同步到其他普通的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
注意:
- kafka只会保证ISR集合中的所有副本保持完全同步。
- kafka一定会保证leader接收到消息然后完全同步给ISR中的所有副本
- ISR的机制保证了处于ISR内部的follower都可以和leader保持同步,一旦出现故障或者延迟(在一定时间内没有同步),就会被提出ISR
4.2.选举流程
- 去zookeeper节点 /broker/topics/{topic名称}/partitions/{分区号}/state 节点读取基本信息。
- 遍历从zk中获取的leaderIsrAndControllerEpoch信息,做一些简单的校验:zk中获取的数据的controllerEpoch必须<=当前的Controller的controller_epoch。最终得到 validLeaderAndIsrs, controller_epoch 就是用来防止脑裂的, 当有两个Controller当选的时候,他们的epoch肯定不一样, 那么最新的epoch才是真的Controller
- 如果没有获取到有效的validLeaderAndIsrs 信息 则直接返回
- 根据入参partitionLeaderElectionStrategy 来匹配不同的Leader选举策略。来选出合适的Leader和ISR信息
- 根据上面的选举策略选出的 LeaderAndIsr 信息进行遍历, 将它们一个个写入到zookeeper节点 /broker/topics/{topic名称}/partitions/{分区号}/state 中。 (当然如果上面没有选择出合适的leader,那么久不会有这个过程了)
- 遍历上面写入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,总体来说,这个选举是很随意的。