群⾸为集群中的服务器选择出来的⼀个服务器,并会⼀直被集群所认可。设置群⾸的⽬的是为了对客户端所发起的ZooKeeper状态变更请求进⾏排序,包括:create、setData和delete操作。群⾸将每⼀个请求转换为⼀个事务,将这些事务发送给追随者,确保集群按照群⾸确定的顺序接受并处理这些事务。
为了了解管理权的原理,⼀个服务器必须被仲裁的法定数量的服务器所认可。法定数量必须集群数量是能够交错在⼀起,以避免我们所说的脑裂问题(split brain):即两个集合的服务器分别独⽴的运⾏,形成了两个集群。这种情况将导致整个系统状态的不⼀致性,最终客户端也将根据其连接的服务器⽽获得不同的结果。
选举并⽀持⼀个群⾸的集群服务器数量必须⾄少存在⼀个服务器进程的交叉,我们使⽤属于仲裁(quorum)来表⽰这样⼀个进程的⼦集,仲裁模式要求服务器之间两两相交。
注意:进展
<u style="box-sizing: border-box; text-decoration: none; margin: 0px; padding: 0px; border: 0px;">⼀组服务器达到仲裁法定数量是必需条件,如果⾜够多的服务器永久性地退出,⽆法达到仲裁法定数量,ZooKeeper也就⽆法取得进展。即使服务器退出后再次启动也可以,但必须保证仲裁的法定数量的服务器最终运⾏起来。我们先不讨论这个问题,⽽在下⼀文中再讨论重新配置集群,重新配置可以随时间⽽改变仲裁法定数量。</u>
每个服务器启动后进⼊LOOKING状态,开始选举⼀个新的群⾸或查找已经存在的群⾸,如果群⾸已经存在,其他服务器就会通知这个新启动的服务器,告知哪个服务器是群⾸,与此同时,新的服务器会与群⾸建⽴连接,以确保⾃⼰的状态与群⾸⼀致。
如果集群中所有的服务器均处于LOOKING状态,这些服务器之间就会进⾏通信来选举⼀个群⾸,通过信息交换对群⾸选举达成共识的选择。在本次选举过程中胜出的服务器将进⼊LEADING状态,⽽集群中其他服务器将会进⼊FOLLOWING状态。
对于群⾸选举的消息,我们称之为群⾸选举通知消息(leader electionnotifications),或简单地称为通知(notifications)。该协议⾮常简单,当⼀个服务器进⼊LOOKING状态,就会发送向集群中每个服务器发送⼀个通知消息,该消息中包括该服务器的投票(vote)信息,投票中包含服务器标识符(sid)和最近执⾏的事务的zxid信息,⽐如,⼀个服务器所发送的投票信息为(1,5),表⽰该服务器的sid为1,最近执⾏的事务的zxid为5(出于群⾸选举的⽬的,zxid只有⼀个数字,⽽在其他协议中,zxid则有时间戳epoch和计数器组成)。
当⼀个服务器收到⼀个投票信息,该服务器将会根据以下规则修改⾃⼰的投票信息:
- 将接收的voteId和voteZxid作为⼀个标识符,并获取接收⽅当前的投票中的zxid,⽤myZxid和mySid表⽰接收⽅服务器⾃⼰的值。
- 如果(voteZxid>myZxid)或者(voteZxid=myZxid且voteId>mySid),保留当前的投票信息。
- 否则,修改⾃⼰的投票信息,将voteZxid赋值给myZxid,将voteId赋值给mySid。
简⽽⾔之,只有最新的服务器将赢得选举,因为其拥有最近⼀次的zxid。我们稍后会看到,这样做将会简化群⾸崩溃后重新仲裁的流程。如果多个服务器拥有最新的zxid值,其中的sid值最⼤的将赢得选举。
当⼀个服务器接收到仲裁数量的服务器发来的投票都⼀样时,就表⽰群⾸选举成功,如果被选举的群⾸为某个服务器⾃⼰,该服务器将会开始⾏使群⾸⾓⾊,否则就成为⼀个追随者并尝试连接被选举的群⾸服务器。注意,我们并未保证追随者必然会成功连接上被选举的群⾸服务器,⽐如,被选举的群⾸也许此时崩溃了。⼀旦连接成功,追随者和群⾸之间将会进⾏状态同步,在同步完成后,追随者才可以处理新的请求。
注意:查找群⾸
<u style="box-sizing: border-box; text-decoration: none; margin: 0px; padding: 0px; border: 0px;">在ZooKeeper中对应的实现选举的Java类为QuorumPeer,其中的run⽅法实现了服务器的主要⼯作循环。当进⼊LOOKING状态,将会执⾏lookForLeader⽅法来进⾏群首的选举,该⽅法主要执⾏我们刚刚所讨论的协议,该⽅法返回前,在该⽅法中会将服务器状态设置为LEADING状态或FOLLOWING状态,当然还可能为OBSERVING状态,我们稍后讨论这个状态。如果服务器成为群首,就会创建⼀个Leader对象并运⾏这个对象,如果服务器为追随者,就会创建⼀个Follower对象并运⾏。</u>
现在,让我们通过例⼦来重温这个协议的执⾏过程。图9-1展⽰了三个服务器,这三个服务器分别以不同的初始投票值开始,其投票值取决于该服务器的标识符合其最新的zxid。每个服务器会收到另外两个服务器发送的投票信息,在第⼀轮之后,服务器s2和服务器s3将会改变其投票值为(1,6),之后服务器服务器s2和服务器s3在改变投票值之后会发送新的通知消息,在接收到这些新的通知消息后,每个服务器收到的仲裁数量的通知消息拥有⼀样的投票值,最后选举出服务器s1为群⾸。
返璞归真二:ZooKeeper内部原理——群⾸选举图9-1:群首选举过程的示例
并不是所有执⾏过程都如图9-1中所⽰,在图9-2中,我们展⽰了另⼀种情况的例⼦。服务器s2做出了错误判断,选举了另⼀个服务器s3⽽不是服务器s1,虽然s1的zxid值更⾼,但在从服务器s1向服务器s2传送消息时发⽣了⽹络故障导致长时间延迟,与此同时,服务器s2选择了服务器s3作为群⾸,最终,服务器s1和服务器s3组成了仲裁数量(quorum),并将忽略服务器s2。
返璞归真二:ZooKeeper内部原理——群⾸选举图9-2:消息交错导致⼀个服务器选择了另⼀个群首
虽然服务器s2选择了另⼀个群⾸,但并未导致整个服务发⽣错误,因为服务器s3并不会以群⾸⾓⾊响应服务器s2的请求,最终服务器s2将会在等待被选择的群⾸s3的响应时⽽超时,并开始再次重试。再次尝试,意味着在这段时间内,服务器s2⽆法处理任何客户端的请求,这样做并不可取。
从这个例⼦,我们发现,如果让服务器s2在进⾏群⾸选举时多等待⼀会,它就能做出正确的判断。我们通过图9-3展⽰这种情况,我们很难确定服务器需要等待多长时间,在现在的实现中,默认的群⾸选举的实现类为FastLeaderElection,其中使⽤固定值200ms(常量finalizeWait),这个值⽐
在当今数据中⼼所预计的长消息延迟(不到1毫秒到⼏毫秒的时间)要长得多,但与恢复时间相⽐还不够长。万⼀此类延迟(或任何其他延迟)时间并不是很长,⼀个或多个服务器最终将错误选举⼀个群⾸,从⽽导致该群⾸没有⾜够的追随者,那么服务器将不得不再次进⾏群⾸选举。错误地选
注意:快速群⾸选举的速指的是什么?
<u style="box-sizing: border-box; text-decoration: none; margin: 0px; padding: 0px; border: 0px;">如果你想知道为什么我们称当前默认的群首选举算法为快速算法,这个问题有历史原因。最初的群首选举算法的实现采用基于拉取式的模型,⼀个服务器拉取投票值的间隔⼤概为1秒,该⽅法增加了恢复的延迟时间,相比较现在的实现⽅式,我们可以更加快速地进⾏群首选举。</u>
返璞归真二:ZooKeeper内部原理——群⾸选举图9-3:群首选举时的长延迟
如果想实现⼀个新的群⾸选举的算法,我们需要实现⼀个quorum包中的Election接⼜。为了可以让⽤户⾃⼰选择群⾸选举的实现,代码中使⽤了简单的整数标识符(请查看代码中QuorumPeer.createElectionAlgorithm()),另外两种可选的实现⽅式为LeaderElection类和AuthFastLeaderElection类,但在版本3.4.0中,这些类已经标记为弃⽤状态,因此,在未来的发布版本中,你可能不会再看到这些类。
网友评论