http://blog.youkuaiyun.com/bruceleexiaokan/article/details/7849601
1. Zab介绍
ZooKeeper服务的内部通信,是基于Zab协议,即ZooKeeper Atomic Broadcast协议。原子广播(AB)是分布式计算普遍使用的原语。本质上说,ZooKeeper服务是基于复制分发的。它需要半数以上的服务器能正常工作。崩溃的服务器能恢复并且重新加入集群。ZooKeeper采用主备方式来维护被复制状态的一致性。在ZooKeeper中,leader接受所有客户的请求并加以执行,然后将增量的状态变化,以事务的方式通过Zab复制到所有的followers。一旦leader崩溃,在进行常规操作之前,必须执行一套恢复协议,来a). 协调各followers达到一致状态,b). 选举出新的leader。为了选举出leader,新的leader必须得到多数支持(quorum)。由于zookeeper server随时都可能崩溃或恢复,随时间流逝,同一个进程可能好几次成为leader。为了能区分随时间变化的不同的leader实例,ZooKeeper为每一个leader都分配一个实例值。一个leader实例值最多能映射到一个进程。
对Zab设计而言很关键的一点是,对于每个状态变化的观察,都是基于上一个状态的增量变化。这也就意味着对于状态变化,必须依赖于其更新的次序。状态改变过程是不能乱序的。只要增量的发送是有序的,状态的更新便是幂等的,即使某个增量变化被反复执行多次。因此,对于消息系统而言,保证at-least once语义已经足够,以此来简化整个Zab的实现。
Zab是Zookeeper核心的重要组件,因此必须高效运行。ZooKeeper被设计成能支持高吞吐量、低时延的系统。应用能广泛地在集群环境下使用Zookeeper,能接受来自不同数据中心大量的客户端连接。
在设计zookeeper过程中,我们发现很难在隔离的环境下解释清楚原子广播,必定要提及必须满足的应用的需求和目标。在解释原子广播必须涉及应用,这导致了不同的协议元素,甚至是一些有意思的优化。
1.1 Multiple outstanding transactions
在设计中,我们让zookeeper能支持multiple outstanding transactions,即一个zookeeper的客户端能以FIFO次序,并发地提交一组事务操作。传统的协议,如Paxos,并不直接支持这样的特性。如果leader每次提交单个事务,则事务之间的次序将不被保证,在某些情况下,这是不可接受的。Paxos能采取的一个已知的方案,是把多个事务打包成一个提议请求,每次最多提交一个提议而不能并发。这样的设计,会导致吞吐或时延受到影响,这取决于batch size的选择。
1.2 有效恢复(Efficient recovery)
一个重要的设计目标是能够从leader崩溃的情况下快速恢复。为了快速恢复,ZooKeeper使用了事物标识规划(transaction identification scheme),以事务的编号这样简单的方式,来帮助新的leader决定如何恢复状态。在这个事物标识规划中,事物标识以以下两个值组成,实例编号值,以及该事务在该实例中以leader角色广播事物的次序号。在这种规划下,那些已接受最高识别号的实例,才可能需要将相应的事务拷贝到新的leader。没有其他需要恢复的事务了。这就意味了,新的leader只需简单收集其他实例的最高事物标识,就知道需要恢复那些事务了。
然而,这种恢复方式在Paxos下是行不通的。收集每个实例的最大事务标识号,不足以解决快速恢复问题。这是因为,即使序列号相同,不同实例可能会接受不同的值。这导致了必须执行Paxos的Phase I过程,来收集新leader之前不了解的所有的序列号。
1.3 总结(Summary of contributions)
Zab是针对主备系统(primary-backup systems)设计的高性能原子广播(atomic broadcast)协议。同之前的其他原子广播协议进行比较,Zab满足一组不同的纠错属性。特别的,Zab提出了叫主次序(primary order)的属性,这对于主备系统非常重要。它能支持正确的状态更新次序,即使随时间流逝不同进程变成leader,同时支持Multiple outstanding transactions。主次序和因果次序(causal order)是不同的,这在随后章节会讲到。Zab是基于主次序的原子广播。最后,ZooKeeper的事务标识规则,使得ZooKeeper较其他经典算法如Paxos,在恢复上更快速。
系统模型

进程之间通过双向的信道来交换消息。更准确地说,位于进程Pi和Pj之间信道Cij,对于Pi和Pj中任何一个都有一对缓存,即输入缓存和输出缓存。事件send(m,Pj),表示将消息m发送给进程Pj。消息m会被插入到Pi的信道Cij的输出缓存。消息以被发送的次序传送,并被插入到Pj的输入缓存。事件recv(m,Pi),表示进程Pi从它的输入缓存读取下一个消息m。
完整性(Integrity):
仅当进程Pi发送了消息m,进程Pj才会从与Pi通信的信道接收到消息m。
前缀性(Prefix):
如果进程Pj接收到了消息m,并且存在消息m',也同属于消息序列S(i,j,k,k'),且m'的序列号小于m,那么消息m'必定先于消息m被Pj接收。
单次循环性(Single iteration):
对于信道Cij,进程Pj的输入缓存只包含一次循环处理的消息。
信道的实现
为了实现以上的特性,我们为信道定义其状态,保证其活性(liveness)。实际上,我们采用TCP连接。在一个新的循环开始,我们会建立Pi和Pj之间的连接。这么做,我们就保证只有被Pi发送的消息才会被Pj接收(完整性);消息序列的前缀一定会被接收;每次创建新的TCP连接,以保证一个消息序列只在一个循环里被处理。
问题描述
ZooKeeper采用主备(primary-backup)方案来进行请求,并以主进程次序原子广播(primary order)将状态变化传播到备用进程。因此只有主进程才要广播。如果主进程崩溃,我们认为存在一个外部机制来选择新的主进程。然而,要保证任何时候只存在最多一个主进程并只允许该主进程进行广播是非常重要的。在我们的实现中,主进程选举机制同我们用以消息广播机制是紧密耦合的。假定存在这样的机制来选择主进程并保证在任何时间只有一个主进程工作,从协议的角度来看这已经足够了。随时间流逝,我们会有一组无限的主进程序列,ρ1ρ2…ρeρe+1…,这里ρe∈П。我们说主进程ρe先于主进程ρe’,如果e<e’。主进程次序队列(Precedence of primaries)是指按时间变化顺序的所有主进程的序列。事实上,由于进程可能恢复,所以有可能存在ρe和ρe’,这里e≠e’,但是ρe和ρe’ 却是同一个进程,但是是不同的实例。
为了保证所有主进程广播的事务的一致性,我们需要确保主进程仅当Zab层的恢复都已完成的情况下,才会开始发送状态变化消息。为了达到这个目的,我们假定所有进程都实现了一个ready(e)的调用,用以让Zab层来通知应用(主进程和所有备份复制进程),Zab已经可以开始广播状态变化了。ready调用同时会为变量instance设值,让主进程决定它的实例值。在广播的时候,主进程用instance变量值作为事务标识号的时间部分。我们假定e的值做所有的主进程实例中是唯一的。实例的唯一性由Zab来保证。
把主进程将状态变化传播给备份进程,我们称做事务。一个事务<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’)。
一旦主进程有一个事务要广播,abcast(<v, z>)会被调用。进程调用abdeliver(<v, z>)来投递(或者提交)一个事务。abcast(<v,z>)不保证成功,譬如主进程崩溃或主进程有变化。因此,在主进程广播的状态变化序列中,只有状态变化的前缀才会被传输。一旦一个事务被传输,进程会将该事务放入txns集合。
A. 核心特性
ZooKeeper要求以下特性来维护所有进程的一致性:
完整性:如果某进程采用了<v,z>的状态变化,那么一定存在一个进程Pi∈П,Pi已经广播过了<v, z>的状态变化
全序性(totalorder):如果某进程在采用<v, z>之前先采用了<v’,z’>,那么任何其他进程在采用<v’, z’>时,<v, z>必须已被采用
这两个特性保证了任何事物不会被自发创建(created spontaneously)或被破坏,并且进程在处理事务时,必须按照一致的次序采用。然后,全序性的特性,还是可以允许两个进程以不同的事务序列进行采用。为了避免不希望的处理发生,我们进一步要求以下特性:
一致性(agreement):如果进程Pi正在采用<v, z>,另一个进程Pj正在采用<v’, z’>,那么Pi已采用了<v’, z’>或者Pj已采用了<v, z>。
需要注意的是,一致性的表述,是和先前的论文是不同的。在先前的论文里,一致性已经被表述成针对原子广播的一个活性(liveness)特性,是一种抽象。而这里,我们把一致性描述成一个安全特性,它保证两个进程的状态不会存在分歧。我们在第四和第五章讨论活性(liveness)。
以上三个安全特性保证所有进程的一致性。然而,我们还需要满足一个特性,从而使得来自一个主进程同时处理多个状态变化。由于每个状态变化都是基于前一个状态。如果之前的状态变化被忽略了,那依赖于它的状态变化也必须忽略。我们把这个特性叫做主次序(primary order)。我们把它分成两个部分:
本地主次序(Local primaryorder):
如果主进程早于<v’, z’>广播了<v, z>,那么一个进程,如果它采用<v’,z’>,那么它必须早于<v’, z’>已采用了<v, z>。
全局主次序(Global primaryorder):
设状态变化<v, z>和<v’, z’>如下:
- 主进程ρi广播了<v, z>
- 主进程ρi,ρi < ρi,广播了<v’, z’>
如果进程Pi∈П,它采用了<v, z>和<v’, z’>,那么Pi必须先于<v’, z’>采用了<v, z>。
请注意,本地主次序对应于单个主进程的FIFO次序,而全局主次序是为了避免下图出现的情况:
最后,主进程必须得保证,所产生的状态更新必须是一致的。因此,主进程只有采用了之前的所有事务,才可以进行下一轮的状态更新广播。这个行为是由下面的特性来保障的:
主完整性(primary integrity):
如果主进程ρe广播<v,z>,有某进程采用<v’, z’>,而<v’,z’>是由另一个主进程ρe’广播的,且e’< e,那么ρe必定先采用了<v’,z’>,然后才广播<v,z>。
B.同因果原子广播的比较
主次序原子广播设计成保持了因果次序特性,并瘾式地在增量状态更新创建时产生。在这一章节,我们将比较因果原子广播和主次序原子广播,并将论证,这两者是无法比较的。
因果次序的定义,是基于事件的前序(precedence, or happens before)关系。对于广播协议,事件分两种,要么是广播事件,要么是采用事件。我们采用<v, z> <c <v’, z’>,来表示abcast(<v, z>)先于abcast(<v’,z’>)发生。对于原子广播而言,因果次序基本定义为:
定义III.1. 因果次序:
如果<v, z> <c <v’, z’>,并且有进程p采用了<v’, z’>,那么该进程必定采用了<v,z>,并且先于<v’, z’>采用了<v, z>。
然而主次序原子广播并不满足因果次序特性。图三给出了一个例子。图中有两个事务<v, z>和<v’’, z’’>是因果关系,满足epoch(z) < epoch(z’) < epoch(z’’),但是事务<v,z>并没有被采用。简化起见,我们仅用事件来代表两个进程:
主次序原子广播下采用的顺序以主因果次序关系<po来表示,它比因果次序关系<c更弱。事实上,不同主进程发送的事务,没有必要必须因果关联,即使它们是被同一个进程发送,也是如此。我们说,一个事件ε以主次序先于ε’,或表示为ε->poε’,当且仅当以下条件之一成立:
1) 事件ε和ε’属于同一个进程,ε先于ε’发生,且以下条件之一成立:a). ε≠abcast(<v, z>)且ε’≠ abcast(<v’, z’>),或者b).epoch(z) = epoch(z’);
2) ε= abcast(<v, z>)并且ε’=abdeliver(<v’, z’>);
3) 存在事件ε’’,使得ε->poε’’且ε’’->poε’。
主因果次序关系<po的定义基于主次序先序关系(PO-precedence)。在因果关系定义中,我们用符号<po来替换<c的,以此来表明主次序因果次序(PO causal order)。
主次序原子广播同样实现了另一个重要的特性,严格因果性(strict causality),即如果某个进程采用<v, z>和<v’, z’>,那么要么<v, z><po<v’, z’>,要么<v’, z’><po<v, z>。严格因果性是需要的,因为事务是基于增量更新方式广播,因此事务必须以产生它们时的相同状态,才能对该进行状态更新。于是这就是产生了因果关系更新链。然而,因果次序可以通过基于事务方式,而非因果关联进行发送。
图四展示了一个满足因果次序(也满足主次序因果次序 PO causal oder)的次序图,但它不满足严格因果性,因为<v, z>和<v’, z’>,即使它们没有因果关系,但都可以被采用。这表明严格因果性和主次序因果次序这两个原语,任何一个不比另一个更强(注:两者强弱无法比较)。