Zab:广播状态改变
当接收到一个写请求时,跟随者会将请求推送到首领,首领解析执行请求后将执行的结果以事务的形式表现为一个状态更新广播出去。一个事务包含了一个服务器在提交该事务时必须应用到其数据数上的改变的精确集合。数据树是Zookeeper保持状态的数据结构(见DataTree)。
接下来的问题就是一个服务器如何确定一个事务是已经被提交了的。这里使用了一个叫Zab:the zookeeper atomic broadcast protocol.的协议。假定现在有一个活跃的首领其拥有一个法定人数的跟随者来支持它的首领权,提交一个事务的协议是非常简单的,就像一个两段提交:
1. 首领发送一个PROPOSAL信息 p到所有的跟随者。
2. 当接收到p时,跟随者会想首领返回ACK相应来声明它已经接收到了这个提议。
3. 当接收的相应达到了其法定人数(法定人数中包含首领本身),首领会发送一个信息到所有的跟随者来进行提交事务。
图9-4解释了这个步骤的流程,在图表中,我们假定隐式的向自己发送了信息。
在提议的响应发送之前,跟随者需要执行一些额外的检查。跟随者需要检查该提议是来自自身当前跟随的首领,这个响应提议和提交任务是按照首领广播的顺序来的。
Zab保证了两个重要的属性:
1. 如果首领先后广播了T和T’,每一个必须在T’提交之前提交T。
2. 如果任何一个服务器先后提交了T和T’,所有其它的服务器也必须在T’之前提交T。
第一个属性保证了事务是按照同样的顺序在服务器之间传播的,而第一个属性保证了服务器不会跳过一些事务。因为一些事务都是状态的改变,而每一个状态的改变都依赖于之前的状态改变,跳过事务可能会产生不一致型。连段提交保证了事务的顺序。Zab在一个法定人数的服务器中记录了事务。一个法定人数的集合必须在提交一个事务之前都承认这个事务,跟随者也会在硬盘值记录下其已经承认了这个事务。
就像我们在170页的LocalStorage(本地存储)这章看到的,事务可能在一些服务器上完整了,而再另一些服务器上没有,原因是服务器在尝试将该事务存储时是可能失败的。Zookeeper在一个新的首领被选中,形成一个新 的法定人数集合时将所有的服务器都更新到最新的状态。
Zookeeper并不指望在真个生命中只有一个活跃的首领。首领是可能崩溃,并暂时性的无法连接的。因此服务器需要连接到一个新的首领来确保系统任然是可用的。Epochs(纪元)的概念就是表示领导权随着时间的改变。一个纪元对应于一个服务器拥有领导权的一段时期。在这个纪元中,首领会广播提议并通过计数器来标识这些提议。记得zxid包含epoch作为其元素的第一部分,一次每一个zxid就能很容易的关联到该事务创建的纪元。
每当一个新的首领选举产生时,纪元(epoch)数据都会被替换。同样的服务器可能是不同纪元的首领,但对已协议来说,一个服务器在不通的纪元都作为首领也表现为不同的首领。如果服务器s是曾经纪元4的首领,但当前是纪元6的首领,而纪元6中s的跟随者会只处理在纪元6中发送的信息,该跟随者在纪元6的恢复期间,在接收到来自纪元6的信息之前可能接受到来自纪元4,而这些提议都会被作为纪元6信息的一部分发送。----究竟这些纪元4的信息会被处理吗?
记录接收到的是在一个法定人数的集合中是非常严格来确保所有的服务器最终都会提交哪些已经被一个或多个服务器提交的事务,即使首领会崩溃掉。精确的检测到首领或任何一台服务器的崩溃是非常难的,甚至是不可能的,在很多配置中,很有可能是错误的怀疑首领已经崩溃了。
实现一个广播协议的最大困难是关于当前首领的存在性,而在分离大脑的场景中就不是必须的了。多个同时存在的首领可能使得服务器以混乱的顺序提交任务或一起跳过一些任务,这样就造成了服务器处于不一致的状态。为了防止系统出现同时有两个 服务器相信他们都是首领是非常难的。时间问题和信息的丢失都可能导致这种场景的出现,因此广播协议是不能依赖于假设的。为了绕过这个问题,Zab保证:
1. 一个首领会在广播一个新的事务之前确保提交所有的事务,哪怕是来自前一个纪元的事务。
2. 任何时间都不允许两个服务器都有一个法定人数集合的支持。
为了实现第一个需求,首领在确保有一个法定人数集合的服务器对新纪元启动时的新状态达成一致之后才可以成为活跃状态。一个纪元初始状态必须是完成了之前提交的所有的事务之后,可能还包括一些已经被接收但还没有提交的状态。这是非常重要的,也就是在一个首领做出一个纪元e的新的提议之前,必须提交所有包括纪元e-1或之前的纪元提交的所有事物。如果存在一个从纪元e’<e的提议,在首领做出他的第一个提议时还没有被纪元e的首领提交,这个旧的提议就永远不会被提交了。
第二点有一些奇怪,因为它并没有真正组织两个手里各自做出自己的前进。也就是说一个首领l当前是首领并在广播事务,与此同时,一个法定人数的服务器集合Q认为l已经崩溃了,并推选除了一个新的首领l’.让我们考虑T作为最为在Q抛弃l的时间广播的一个事务,并且Q的一部分自己已经成功记录了T。在l’被选举出来之后,不在Q中的足够多的进程也记录的T,形成了对应T的一个法定人数集合。在这种情况下,T会被提交哪怕是l’已经被选举出来了。但是别担心,这并不是一个bug.Zab保证T作为l’提交的事务的一部分,通过保证支持l’的法定人数集合中至少包含一个跟随者是承认T 的。关键是l’和l不会同时拥有复合法定人数要求的支持者。
图9-5解释了这种场景,在这个图中,l就是服务器s5,l’是s3,Q包括s1到s3,T的zxid是<1,1>。当接收到第二个确认后,s5可以发送一个提交信息到s4了指示提交该事务。而其他的服务器在跟随s3之后忽略了s5的信息。注意到s3承认了<1,1>,因此在他建立领导权的时候是意识到这个事务的。
我们已经承诺Zab会确保新的首领l’不会丢失掉<1,1>,但是究竟是如何发生的呢?在成为激活状态之前,l’必须了解就得法定人数集合中的服务器已经接受的所有提议,也必须保证这些服务器不会再接收到来自之前首领的更多的提议。在9-5的例子中,形成支持l’的法定人数的服务器集合保证他们不会再接受来自首领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.他们是在当服务器状态从LOOKING转变为Quourmpeer中的LEADING或FOLLOWING实际执行的方法。
对于DIFF和SNAP,跟随LearnerHandler.run方法来查看代码是如何决定哪些提议是通过DIFF发送,而镜像是如何序列化并发送的