本文属于分布式系统学习笔记系列,上一篇笔记整理了paxos算法,本文属于原书第四章,梳理zookeeper的目标特性及ZAB协议。
ZooKeeper是一个典型的分布式数据一致性的解决方案,分布式程序可以基于它实现诸如数据发布/订阅、负载均衡、命名服务、分布式协调通知、集群管理、master选举、分布式锁、分布式队列等功能。ZooKeeper可以保证如下分布式一致性特性。
1、顺序一致性:
从同一个客户端发起的事务请求,最终将严格按照其发起顺序被应用到ZooKeeper中。
2、原子性:
更新操作要么成功要么失败,没有中间状态。
3、单一视图(Single system image):
不管客户端连接哪一个服务器,客户端看到服务端的数据模型都是一致的(the same view of service)。
4、可靠性(Reliability):
一旦一个更新成功,那么那就会被持久化,直到客户端用新的更新覆盖这个更新。
5、实时性(Timeliness):
Zookeeper仅保证在一定时间内,客户端最终一定能够从服务端读到最新的数据状态。
Zookeeper使得分布式程序能够通过一个共享的、树形结构的名字空间来进行相互协调。其有一系列的被称为ZNODE的数据节点构成。类似于文件系统,但是Zookeeper将全部数据都存放在内存中,以提高服务器吞吐,减少延迟。
1.2.2构建集群
可以看出Zookeeper的实现是有Client、Server构成,Server端提供了一个一致性复制、存储服务,Client端会提供一些具体的语义,如分布式锁等。这里不做过多说明,结合上图:组成Zookeeper的集群的的每台机器都在内存中维护当前服务器的状态,并且每台机器之间都互相保持着通信,过半机器能够正常工作,集群就能对外提供服务。Zookeeper的client会选择集群内任意一台机器创建TCP连接。
对于来自客户端的每个更新请求,zookeeper会分配一个全局唯一的递增编号,这个编号反应了所有事物的操作先后顺序,应用程序可以基于此实现更高层次的同步原语。
数据都在内存中存放,适合以读为主的场景。
ZooKeeper为高可用的一致性协调框架,并没有完全采用paxos算法,而是使用了ZAB(ZooKeeper Atomic Broadcast )原子消息广播协议作为数据一致性的核心算法,ZAB协议是专为zookeeper设计的支持崩溃恢复原子消息广播算法。
基于ZAB协议,zookeeper实现了一种基于主备模式的系统架构来保证集群中各副本之间的数据一致性。具体的:ZooKeeper使用单一主进程Leader用于处理客户端所有事务请求,采用ZAB协议将服务器数状态以事务形式广播到所有Follower上,因此能很好的处理客户端的大量并发请求(这里我理解就是ZK通过使用TCP协议及一个事务ID来实现事务的全序特性,leader模式就是先到先执行解决因果顺序);另一方面,由于事务间可能存在着依赖关系,ZAB协议保证Leader广播的变更序列被顺序的处理,一个状态被处理那么它所依赖的状态也已经提前被处理;最后,考虑到住进程leader在任何时候可能崩溃或者异常退出,因此ZAB协议还要Leader进程崩溃的时候可以重新选出Leader并且保证数据的完整性;
协议核心如下:
所有的事务请求必须一个全局唯一的服务器(leader)来协调处理,集群其余的服务器称为follower服务器。leader服务器负责将一个客户端请求转化为事务提议(Proposal),并将该proposal分发给集群所有的follower服务器。之后leader服务器需要等待所有的follower服务器的反馈,一旦超过了半数的follower服务器进行了正确反馈后,那么leader服务器就会再次向所有的follower服务器分发commit消息,要求其将前一个proposal进行提交。
书上这块写的比较详细,我简单整理重点。上面我们介绍了协议的核心内容,可以总结出ZAB协议的两个基本模式:消息广播和崩溃恢复。
客户端提交事务请求时Leader节点为每一个请求生成一个事务Proposal,将其发送给集群中所有的Follower节点,收到过半Follower的反馈后开始对事务进行提交,ZAB协议使用了原子广播协议;在ZAB协议中只需要得到过半的Follower节点反馈Ack就可以对事务进行提交,这也导致了Leader几点崩溃后可能会出现数据不一致的情况,ZAB使用了崩溃恢复来处理数字不一致问题;消息广播使用了TCP协议进行通讯所有保证了接受和发送事务的顺序性。广播消息时Leader节点为每个事务Proposal分配一个全局递增的ZXID(事务ID),每个事务Proposal都按照ZXID顺序来处理;
之前我们我们介绍了2阶段提交协议,ZAB协议有所不同简化了协议:
Follower收到proposal后,写到磁盘,返回ACK。Leader收到大多数ACK后,广播COMMIT消息,自己也提交该消息。Follower收到COMMIT之后,提交该消息。
上面我们讲了ZAB协议在正常情况下的消息广播过程,那么一旦leader服务器出现崩溃或者与过半的follower服务器失去联系,就进入崩溃恢复模式。
崩溃恢复过程中,为了保证数据一致性需要处理特殊情况:已经被leader提交的proposal也能够所有的follower提交,跳过已经被丢弃的事务proposal。针对这个要求,如果让leader选举算法能保证选举出来的leader服务器拥有集群中所有机器中编号最大(ZXID最大)的事务proposal,那么就可以保证新选举出来的leader服务器一定拥有所有已提交的提案,省去了leader服务器检查proposal提交和丢弃的工作。
数据同步
完成了leader选举这步,在正式接受新的事务请求之前,leader服务器要确认事务日志中的proposal是不是都已经被集群过半的机器提交了,即是否完成了数据同步。
1. leader等待server连接;
2 .Follower连接leader,将最大的zxid发送给leader;
3 .Leader根据follower的zxid确定同步点;
4 .完成同步后通知follower 已经成为uptodate状态;
5 .Follower收到uptodate消息后,又可以重新接受client的请求进行服务了。
ZAB协议中使用ZXID作为事务编号,ZXID为64位数字,低32位为一个递增的计数器,每一个客户端的一个事务请求时Leader产生新的事务后该计数器都会加1,高32位为Leader周期epoch编号,当新选举出一个Leader节点时Leader会取出本地日志中最大事务Proposal的ZXID解析出对应的epoch把该值加1作为新的epoch,将低32位从0开始生成新的ZXID;ZAB使用epoch来区分不同的Leader周期,能有效避免了不同的leader服务器错误的使用相同的ZXID编号提出不同的事务proposal的异常情况,大大简化了提升了数据恢复流程;
上面我们介绍了ZAB协议的大体内容及消息广播和崩溃恢复基本模式,原书还从系统模型、问题描述等不同角度介绍了ZAB协议,这里只贴出来问题描述相关概念:
主进程周期:
ZooKeeper使用单一主进程Leader用于处理客户端所有事务请求,采用ZAB协议将服务器数状态以事务形式广播到所有Follower上;为了保证所有主进程广播的事务的一致性,我们需要确保主进程仅当Zab层的恢复都已完成的情况下,才会开始发送状态变化消息。为了达到这个目的,我们假定所有进程都实现了一个ready(e)的调用,用以让Zab层来通知应用(主进程和所有备份复制进程),Zab已经可以开始广播状态变化了。ready调用同时会为变量instance设值,让主进程决定它的实例值。实例值用于唯一标识当前主进程的的周期,在广播的时候,主进程用实例值来设置事务标识号的epoch字段———我们假定e的值做所有的主进程实例中是唯一的。实例的唯一性由Zab来保证。
事务:
把主进程将状态变化传播给备份进程,我们称做事务。我们假设存在一个类似于 transaction(V,Z)这样的函数调用,用来实现主进程对于状态变更的广播。主进程每次对于transaction调用包含<v, z>有两个部分:事务的值v以及事务的标识z(或叫做zxid)。每个事务标识z=<e, c>,即z由两部分组成,时间标识e和计数器c。我们采用epoch(z)来标识事务标识号的时间部分,counter(z)来标识事务标识号的计数器值。我们说,时间(epoch)e是先于时间e’,即e<e’。对于一个给定的主进程实例ρe,epoch(z) = instance = e。对于每一个新的事务,我们会递增计数器c。我们说事务标识号z先于事务标识号z’,即要么epoch(z)< epoch(z’),或者epoch(z) == epoch(z’)但counter(z) < counter(z’)。
我们从算法角度来看ZAB协议,可以细分为三个阶段:发现(discovery)、同步(sync)、广播(Broadcast),之前我们把发现(discovery)与同步(sync)合并为恢复(recovery) 阶段。
F1.1 follower F将自己最后接受的事务proposal的epoch值CEPOCH(F.p)发送给准leader L。
L1.1 当接受到过半follower的CEPOCH(F.p)消息后,准leader L会生成NEWEPOCH(e')消息给这些过半的follower。
其中e'= max(epoch)+1;
F1.2 当follower接受到来自准leader L的NEWEPOCH(e')消息后,如果其检测到当前的CEPOCH(F.p)小于e',就会把CEPOCH(F.p)赋值为e',同时向这个准leader L反馈ACK消息,消息中包含了该follower的epoch CEPOCH(F.p)及历史事务集合hf.
当Leader L接受到过半follower的确认消息ack后,会从这过半的服务器中选取一个follower F,使用其作为初始化事务集合Ie.
在完成发现阶段后,就进入同步阶段。将Follower与Leader的数据进行同步,由Leader发起同步指令,最总保持集群数据的一致性;
L2.1 leader L 将e' 及Ie''以NEWLEADER(e',Ie')消息的形式发送给所有的quorum中的follower。
F2.1 当follower接受到来自leader L的NEWLEADER(e',Ie')消息后,如果follower发现自己的CEPOCH(F.p)≠e'时,直接接入下 一轮循环。
如果CEPOCH(F.p)=e',那么follower会执行事务应用操作,最后会反馈给leader L。
L2.2 当leader L接受到超过半数的follower针对NEWLEADER(e',Ie')反馈消息后,就会向所有的follower发送commit消息。
F2.2 当follower接受到来自leader的commit消息后,会依次处理并提交所有所有Ie'中未处理的事务。
完成同步阶段后,ZAB协议就可以正式接受客户端新的事务请求,并进行消息广播流程。
leader收到一个request后,会生成一个propose。然后执行两阶段提交.Leader节点为每一个Follower节点分配一个队列按事务ZXID顺序放入到队列中,且根据队列的规则FIFO来进行事务的发送。Follower节点收到事务Proposal后会将该事务以事务日志方式写入到本地磁盘中,成功后反馈Ack消息给Leader节点,Leader在接收到过半Follower节点的Ack反馈后就会进行事务的提交,以此同时向所有的Follower节点广播Commit消息,Follower节点收到Commit后开始对事务进行提交;
这块参见上面的2.2那个广播流程图,不展开。
下图就是整个过程中各个进程之间的消息收发情况,
图:
CEPOCH = Follower sends its last promise to the prospective leader
NEWEPOCH = Leader proposes a new epoch e'
ACK-E = Follower acknowledges the new epoch proposal
NEWLEADER = Prospective leader proposes itself as the new leader of epoch e'
ACK-LD = Follower acknowledges the new leader proposal
COMMIT-LD = Commit new leader proposal
PROPOSE = Leader proposes a new transaction
ACK = Follower acknowledges leader proosal
COMMIT = Leader commits proposal
书上只介绍了三个阶段,这里补充下选举,选举有多种方法,这里只介绍默认的fast leader election:
限制条件:
就是之前崩溃恢复哪里提到的,为了保证数据一致性,election阶段必须确保选出的Leader具有最大ZXID,暗含的规则就是能看到所有历史的commited 事务。
那么fast leader election是如何选择出一个拥有highest lastZxid的leader?
选举流程:
1. 每个Follower都向其他节点发送选自身为Leader的Vote投票请求,等待回复;
2. Follower接受到的Vote如果比自身的大(ZXID更新)时则投票,并更新自身的Vote,否则拒绝投票;
3. 每个Follower中维护着一个投票记录表,当某个节点收到过半的投票时,结束投票并把该Follower选为Leader,投票结束;
ZAB协议中存在着三种状态,每个节点都属于以下三种中的一种:
1. Looking:系统刚启动时或者Leader崩溃后正处于选举状态
2. Following:Follower节点所处的状态,Follower与Leader处于数据同步阶段;
3. Leading:Leader所处状态,当前集群中有一个Leader为主进程;
ZooKeeper启动时所有节点初始状态为Looking,这时集群会尝试选举出一个Leader节点,选举出的Leader节点切换为Leading状态;当节点发现集群中已经选举出Leader则该节点会切换到Following状态,然后和Leader节点保持同步;当Follower节点与Leader失去联系时Follower节点则会切换到Looking状态,开始新一轮选举;在ZooKeeper的整个生命周期中每个节点都会在Looking、Following、Leading状态间不断转换;
选举出Leader节点后ZAB进入原子广播阶段,这时Leader为和自己同步的每个节点Follower创建一个操作序列,一个时期一个Follower只能和一个Leader保持同步,Leader节点与Follower节点使用心跳检测来感知对方的存在;当Leader节点在超时时间内收到来自Follower的心跳检测那Follower节点会一直与该节点保持连接;若超时时间内Leader没有接收到来自过半Follower节点的心跳检测或TCP连接断开,那Leader会结束当前周期的领导,切换到Looking状态,所有Follower节点也会放弃该Leader节点切换到Looking状态,然后开始新一轮选举;
作者认为ZAB不是paxos的一个典型实现,而是设计目标不同。
联系:
ZAB还额外引入了同步阶段。Paxos算法的确是不关心请求之间的逻辑顺序,而只考虑数据之间的全序,但很少有人直接使用paxos算法,都是进行简化或者改进。可以认为ZAB是一种paxos算法的简化。
*******************************************************************************************
参考:
http://www.solinx.co/archives/435?utm_source=tuicool&utm_medium=referral
http://my.oschina.net/zhengyang841117/blog/186676