ZooKeeper:11---内部原理(群首选举原理)

一、前言

  • 群⾸为集群中的服务器选择出来的⼀个服务器,并会⼀直被集群所认可。设置群⾸的⽬的是为了对客户端所发起的ZooKeeper状态变更请求进⾏排序,包括:create、setData和delete操作。群⾸将每⼀个请求转换为⼀个事务,如前⼀文中所介绍的(https://blog.****.net/qq_41453285/article/details/107217067),将这些事务发送给追随者,确保集群按照群⾸确定的顺序接受并处理这些事务
  • 为了了解管理权的原理,⼀个服务器必须被仲裁的法定数量的服务器所认可。在前面我们已经讨论过(https://blog.****.net/qq_41453285/article/details/107146459),法定数量必须集群数量是能够交错在 ⼀起,以避免我们所说的脑裂问题(split brain):即两个集合的服务器分 别独⽴的运⾏,形成了两个集群。这种情况将导致整个系统状态的不⼀致 性,最终客户端也将根据其连接的服务器⽽获得不同的结果,在前面链接的文章我们已经通过具体例⼦说明了这⼀情况

“仲裁”的概念

  • 选举并⽀持⼀个群⾸的集群服务器数量必须⾄少存在⼀个服务器进程的交叉,我们使⽤属于仲裁(quorum)来表⽰这样⼀个进程的⼦集,仲裁模式要求服务器之间两两相交

注意:进展

  • ⼀组服务器达到仲裁法定数量是必需条件,如果⾜够多的服务器永久性地退出,⽆法达到仲裁法定数量,ZooKeeper也就⽆法取得进展。即使服务器退出后再次启动也可以,但必须保证仲裁的法定数量的服务器最终运⾏起来。我们先不讨论这个问题,⽽在后面的文章再讨论重新配置集群, 重新配置可以随时间⽽改变仲裁法定数量

二、群首选举的流程

  • 每个新启动的服务器节点在启动之后会进入LOOKING状态,其会在集群中寻找是否已经存在群首:
    • 如果当前集群中已经存在群首(也就是说集群之前已经搭建好了):其他服务器就会通知这个新启动的服务器,告诉它哪个服务器是群首,之后,新的服务器与群首建立连接,以确保自己的状态与群首一致
    • 如果当前集群中的所有服务器均处于LOOKING状态(也就是第一次搭建集群):这些服务器之间就会进行通信来选举一个群首,通过信息交换对群首选举达成共识的选择

群首选举的消息格式

  • 对于群⾸选举的消息,我们称之为群⾸选举通知消息(leader election notifications),或简单地称为通知(notifications)
  • 该协议⾮常简单:选举时,服务器节点会发送向集群中每个服务器发送⼀个通知消息,该消息中包括该服务器的投票(vote)信息,投票中包含服务器标识符(sid)和最近执⾏的事务的zxid信息
  • 例如:
    • ⼀个服务器所发送的投票信息为(1,5),表⽰该服务器的sid为1,最近执⾏的事务的zxid为5
    • 备注:出于群⾸选举的⽬的,zxid只有⼀个数字,⽽在其他协议中,zxid则有时间戳epoch和计数器组成(可参阅后面的Zab协议:https://blog.****.net/qq_41453285/article/details/107251044

群首选举的规则

  • 当⼀个服务器收到另⼀个给自己发来的投票信息,该服务器将会根据以下规则修改⾃⼰的投票信息:
    • 其用voteId来表示另一个服务器头投票信息中的sid,用mySid表示自己的sid;用voreZxid表示另一个服务器头投票信息中的zxid,用myZxid
    • 如果(voteZxid>myZxid)或者(voteZxid>myZxid但是voteId>mySid),那么其就将自己的mySid改为另一个服务器的voteId,将自己的myZxid改为另一个服务器的voreZxid。然后将这个投票消息广播出去
    • 否则,其不同意另一个服务器的投票,而将自己的信息广播出去
  • 总之:
    • 只有Zxid最大的那个服务器会赢得选举,最终成为群首
    • 如果多个服务器的Zxid相等,那么sid最大的那个服务器会赢得选举,最终成为群首
  • 选举成功之后:
    • 当一个服务器接收到仲裁数量的服务器发来的投票欣慰都一样时,就表示群首选举成功
    • 选举成功之后会有一个群首,其他的服务器节点变为追随者追随于群首
    • 注意,我们并未保证追随者必然会成功连接上被选举的群⾸服务器,⽐ 如,被选举的群⾸也许此时崩溃了。⼀旦连接成功,追随者和群⾸之间将 会进⾏状态同步,在同步完成后,追随者才可以处理新的请求

注意:查找群首

  • 在ZooKeeper中对应的实现选举的Java类为QuorumPeer,其中的run⽅ 法实现了服务器的主要⼯作循环。当进⼊LOOKING状态,将会执⾏lookForLeader⽅法来进⾏群首的选举,该⽅法主要执⾏我们刚刚所讨论的 协议,该⽅法返回前,在该⽅法中会将服务器状态设置为LEADING状态 或FOLLOWING状态,当然还可能为OBSERVING状态,我们稍后讨论这 个状态。如果服务器成为群首,就会创建⼀个Leader对象并运⾏这个对 象,如果服务器为追随者,就会创建⼀个Follower对象并运⾏

三、一次群首选举的图示案例

  • 下图展示了三个服务器:
    • 这三个服务器分别以不同的初始投票值开始,其投票值取决于该服务器的标识符和其最新的zxid
    • 每个服务器会收到另外两个服务器发送的投票信息
    • 在第⼀轮之后,服务器s2和服务器s3将会改变其投票值为 (1,6),之后服务器s2和服务器s3在改变投票值之后会发送新的通知消息
    • 在接收到这些新的通知消息后,每个服务器收到的仲裁数量的通知消息拥有⼀样的投票值,最后选举出服务器s1为群⾸

ZooKeeper:11---内部原理(群首选举原理)

四、一个错误选举但最终选举成功的资历

  • 并不是所有执⾏过程都如上图所⽰,在下图中,我们展⽰了另⼀ 种情况的例⼦:
    • 服务器s2做出了错误判断,选举了另⼀个服务器s3⽽不是服务器s1
    • 虽然s1的zxid值更⾼,但在从服务器s1向服务器s2传送消息时发⽣了⽹络故障导致长时间延迟
    • 与此同时,服务器s2选择了服务器s3作为群⾸,最终,服务器s1和服务器s3组成了仲裁数量(quorum),并将忽略服务器s2

ZooKeeper:11---内部原理(群首选举原理)

  • 虽然服务器s2选择了另⼀个群⾸,但并未导致整个服务发⽣错误,因为:服务器s3并不会以群⾸⾓⾊响应服务器s2的请求,最终服务器s2将会在等 待被选择的群⾸s3的响应时⽽超时,并开始再次重试。再次尝试,意味着在这段时间内,服务器s2⽆法处理任何客户端的请求,这样做并不可取
  • 从这个例⼦,我们发现,如果让服务器s2在进⾏群⾸选举时多等待⼀会,它就能做出正确的判断。我们通过下图展示这种情况,我们很难确定服务器需要等待多长时间,在现在的实现中,默认的群⾸选举的实现类为FastLeaderElection,其中使⽤固定值200ms(常量finalizeWait),这个值⽐在当今数据中心所预计的长消息延迟(不到1毫秒到⼏毫秒的时间)要长得多,但与恢复时间相⽐还不够长。万⼀此类延迟(或任何其他延迟)时间并不是很长,⼀个或多个服务器最终将错误选举⼀个群⾸,从⽽导致该群 ⾸没有⾜够的追随者,那么服务器将不得不再次进⾏群⾸选举。错误地选举⼀个群⾸可能会导致整个恢复时间更长,因为服务器将会进⾏连接以及 不必要的同步操作,并需要发送更多消息来进⾏另⼀轮的群⾸选举

ZooKeeper:11---内部原理(群首选举原理)

注意:快速群⾸选举的快速指的是什么?

  • 如果你想知道为什么我们称当前默认的群首选举算法为快速算法,这 个问题有历史原因。最初的群首选举算法的实现采用基于拉取式的模型, ⼀个服务器拉取投票值的间隔⼤概为1秒,该⽅法增加了恢复的延迟时间, 相比较现在的实现⽅式,我们可以更加快速地进⾏群首选举
  • 如果想实现⼀个新的群⾸选举的算法,我们需要实现⼀个quorum包中 的Election接⼜。为了可以让⽤户⾃⼰选择群⾸选举的实现,代码中使⽤了 简单的整数标识符(请查看代码中 QuorumPeer.createElectionAlgorithm()),另外两种可选的实现⽅式为 LeaderElection类和AuthFastLeaderElection类,但在版本3.4.0中,这些类已 经标记为弃⽤状态,因此,在未来的发布版本中,你可能不会再看到这些类