在接收到⼀个写请求操作后,追随者会将请求转发给群⾸,群⾸将探索性地执⾏该请求,并将执⾏结果以事务的⽅式对状态更新进⾏⼴播。⼀个事务中包含服务器需要执⾏变更的确切操作,当事务提交时,服务器就会将这些变更反馈到数据树上,其中数据树为ZooKeeper⽤于保存状态信息的数据结构(请参考DataTree类)。
之后我们需要⾯对的问题便是服务器如何确认⼀个事务是否已经提交,由此引⼊了我们所采⽤的协议:Zab:ZooKeeper原⼦⼴播协议(ZooKeeper Atomic Broadcast protocol)。假设现在我们有⼀个活动的群⾸服务器,并拥有仲裁数量的追随者⽀持该群⾸的管理权,通过该协议提交⼀个事务⾮常简单,类似于⼀个两阶段提交。
-
群⾸向所有追随者发送⼀个PROPOSAL消息p。
-
当⼀个追随者接收到消息p后,会响应群⾸⼀个ACK消息,通知群⾸其已接受该提案(proposal)。
-
当收到仲裁数量的服务器发送的确认消息后(该仲裁数包括群⾸⾃⼰),群⾸就会发送消息通知追随者进⾏提交(COMMIT)操作。
图9-4说明了这⼀个过程的具体步骤顺序,我们假设群⾸通过隐式⽅式给⾃⼰发送消息。
图9-4:提交提案的常规消息模式在应答提案消息之前,追随者还需要执⾏⼀些检查操作。追随者将会检查所发送的提案消息是否属于其所追随的群⾸,并确认群⾸所⼴播的提案消息和提交事务消失的顺序正确。Zab保障了以下⼏个重要属性:
-
·如果群首按顺序⼴播了事务T和事务T,那么每个服务器在提交T?事务前保证事务T已经提交完成。
-
·如果某个服务器按照事务T、事务T的顺序提交事务,所有其他服务器也必然会在提交事务T前提交事务T。
第⼀个属性保证事务在服务器之间的传送顺序的⼀致,⽽第⼆个竖向地保证服务器不会跳过任何事务。假设事务为状态变更操作,每个状态变更操作又依赖前⼀个状态变更操作的结果,如果跳过事务就会导致结果的不⼀致性,⽽两阶段提交保证了事务的顺序。Zab在仲裁数量服务器中记录了事务,集群中仲裁数量的服务器需要在群⾸提交事务前对事务达成⼀致,⽽且追随者也会在硬盘中记录事务的确认信息。
我们在后面的文章将会看到,事务在某些服务器上可能会终结,⽽其他服务器上却不会,因为在写⼊事务到存储中时,服务器也可能发⽣崩溃。⽆论何时,只要仲裁条件达成并选举了⼀个新的群⾸,ZooKeeper都可以将所有服务器的状态更新到最新。
但是,ZooKeeper⾃始⾄终并不总是有⼀个活动的群⾸,因为群⾸服务器也可能崩溃,或短时间地失去连接,此时,其他服务器需要选举⼀个新的群⾸以保证系统整体仍然可⽤。其中时间戳(epoch)的概念代表了管理权随时间的变化情况,⼀个时间戳表⽰了某个服务器⾏使管理权的这段时间,在⼀个时间戳内,群⾸会⼴播提案消息,并根据计数器(counter)识别每⼀个消息。我们知道zxid的第⼀个元素为时间戳信息,因此每个zxid可以很容易地与事务被创建时间戳相关联。
时间戳的值在每次新群⾸选举发⽣的时候便会增加。同⼀个服务器成为群⾸后可能持有不同的时间戳信息,但从协议的⾓度出发,⼀个服务器⾏使管理权时,如果持有不同的时间戳,该服务器就会被认为是不同的群⾸。如果服务器s成为群⾸并且持有的时间戳为4,⽽当前已经建⽴的群⾸的时间戳为6,集群中的追随者会追随时间戳为6的群⾸s,处理群⾸在时间戳6之后的消息。当然,追随者在恢复阶段也会接收时间戳4到时间戳6之间的提案消息,之后才会开始处理时间戳为6之后的消息,⽽实际上这些提案消息是以时间戳6的消息来发送的。
ZooKeeper的10大内部原理其三:Zab,来了解一波在仲裁模式下,记录已接收的提案消息⾮常关键,这样可以确保所有的服务器最终提交了被某个或多个服务已经提交完成的事务,即使群⾸在此时发⽣了故障。完美检测群⾸(或任何服务器)是否发⽣故障是⾮常困难的,虽然不是不可能,但在很多设置的情况下,都可能发⽣对⼀个群⾸是否发⽣故障的错误判断。
实现这个⼴播协议所遇到最多的困难在于群⾸并发存在情况的出现,这种情况并不⼀定是脑裂场景。多个并发的群⾸可能会导致服务器提交事务的顺序发⽣错误,或者直接跳过了某些事务。为了阻⽌系统中同时出现两个服务器⾃认为⾃⼰是群⾸的情况是⾮常困难的,时间问题或消息丢失都可能导致这种情况,因此⼴播协议并不能基于以上假设。为了解决这个问题,Zab协议提供了以下保障:
- ·⼀个被选举的群首确保在提交完所有之前的时间戳内需要提交的事务,之后才开始⼴播新的事务。
- ·在任何时间点,都不会出现两个被仲裁支持的群首。
为了实现第⼀个需求,群⾸并不会马上处于活动状态,直到确保仲裁数量的服务器认可这个群⾸新的时间戳值。⼀个时间戳的最初状态必须包含所有的之前已经提交的事务,或者某些已经被其他服务器接受,但尚未提交完成的事务。这⼀点⾮常重要,在群⾸进⾏时间戳e的任何新的提案前,必须保证⾃时间戳开始值到时间戳e-1内的所有提案被提交。如果⼀个提案消息处于时间戳e'<e,在群⾸处理时间戳e的第⼀个提案消息前没有提交之前的这个提案,那么旧的提案将永远不会被提交。
对于第⼆个需求有些棘⼿,因为我们并不能完全阻⽌两个群⾸独⽴地运⾏。假如⼀个群⾸l管理并⼴播事务,在此时,仲裁数量的服务器Q判断群⾸l已经退出,并开始选举了⼀个新的群⾸l',我们假设在仲裁机构Q放弃群⾸l时有⼀个事务T正在⼴播,⽽且仲裁机构Q的⼀个严格的⼦集记录了
这个事务T,在群⾸l'被选举完成后,在仲裁机构Q之外服务器也记录了这个事务T,为事务T形成⼀个仲裁数量,在这种情况下,事务T在群⾸l'被选举后会进⾏提交。不⽤担⼼这种情况,这并不是个bug,Zab协议保证T作为事务的⼀部分被群⾸l'提交,确保群⾸l'的仲裁数量的⽀持者中⾄少有⼀个追随者确认了该事务T,其中的关键点在于群⾸l'和l在同⼀时刻并未获得⾜够的仲裁数量的⽀持者。
图9-5说明了这⼀场景,在图中,群⾸l为服务器s5,l'为服务器s3,仲裁机构由s1到s3组成,事务T的zxid为(1,1)。在收到第⼆个确认消息之后,服务器s5成功向服务器s4发送了提交消息来通知提交事务。其他服务器因追随服务器s3忽略了服务器s5的消息,注意服务器s3所了解的xzid为(1,1),因此它知道获得管理权后的事务点。
图9-5:群首发⽣重叠的情况之前我们提到Zab保证新群⾸l'不会缺失(1,1),现在我们来看看其中的细节。在新群⾸l'⽣效前,它必须学习旧的仲裁数量服务器之前接受的所有提议,并且保证这些服务器不会继续接受来⾃旧群⾸的提议。此时,如果群⾸l还能继续提交提议,⽐如(1,1),这条提议必须已经被⼀个以上的认可了新群⾸的仲裁数量服务器所接受。我们知道仲裁数量必须在⼀台以上的服务器之上有所重叠,这样群⾸l'⽤来提交的仲裁数量和新群⾸l使⽤的仲裁数量必定在⼀台以上的服务器上是⼀致的。因此,l'将(1,1)加⼊⾃⾝的状态并传播给其跟随者。
在群⾸选举时,我们选择zxid最⼤的服务器作为群⾸。这使得ZooKeeper不需要将提议从追随者传到群⾸,⽽只需要将状态从群⾸传播到追随者。假设有⼀个追随者接受了⼀条群⾸没有接受的提议。群⾸必须确保在和其他追随者同步之前已经收到并接受了这条提议。但是,如果我们选择zxid最⼤的服务器,我们将可以完完全全跳过这⼀步,可以直接发送更新到追随者。
在时间戳发⽣转换时,Zookeeper使⽤两种不同的⽅式来更新追随者来优化这个过程。如果追随者滞后于群⾸不多,群⾸只需要发送缺失的事务点。因为追随者按照严格的顺序接收事务点,这些缺失的事务点永远是最近的。这种更新在代码中被称之为DIFF。如果追随者滞后很久,ZooKeeper将发送在代码中被称为SNAP的完整快照。因为发送完整的快照会增⼤系统恢复的延时,发送缺失的事务点是更优的选择。可是当追随者滞后太远的情况下,我们只能选择发送完整快照。
群⾸发送给追随者的DIFF对应于已经存在于事务⽇志中的提议,⽽SNAP对应于群⾸拥有的最新有效快照。将在后面的文中讨论这两种保存在磁盘上的⽂件。
注意:深⼊代码
这里我们给出⼀点代码指导。⼤部分Zab的代码存在于Leader、LearnerHandler和Follower。Leader和LearnerHandler的实例由群首服务器执⾏,⽽Follower的实例由追随者执⾏。Leader.lead和Follower.followLeader是两个重要的⽅法,他们在服务器在QuorumPeer中从LOOKING转换到LEADING或者FOLLOWING时得到调用。
如果你对DIFF和SNAP的区别感兴趣,可以查看LearnerHandler.run的代码,其中包含了使用DIFF时如何决定发送哪条提议,以及关于如何持久化和发送快照的细节。
网友评论