简介
zookeeper是分布式协调系统,用来协调、同步多服务器之间的状态,容错能力强git
一个应用要保证HA,每每须要N个服务器(N>1)提供服务,其中有M台master,N-M台slave。这样一台挂了,另外N-1台也能提供服务。因此,数据也会备份成N份散布在这些服务器上。如今的问题变成了,如何管理这N台服务器?如何在master节点失败的时候从新选择master?如何保证全部服务器存储的备份数据一致?程序员
若是你碰到上面那些棘手的问题,zookeeper恰好能够帮到你:github
- 存储公共配置信息
- 跟踪全部进程运行状态
- 集群中各节点的管理
- master选主
- 提供分布式同步支持(分布式锁等功能)
上面列出的是官方提供的功能支持,还有不少其余功能等待挖掘。面试
当你的系统依赖zookeeper保证HA和一致性的时候,你确定也好奇,zookeeper自己是如何保证这两个特性的。幕后功臣每每容易被忽视,其实它就是Zookeeper原子广播协议(Zab协议)。若是你了解过二阶段提交、paxos算法、raft算法等大名鼎鼎的分布式一致性算法,Zab确定也不会陌生,由于他们要达到的目的相同,就是保证应用程序的HA和一致性。算法
背景
为了读懂后面的内容,有些术语须要了解:apache
- Peer:节点。表明了系统中的进程,每每系统有多个进程,也就有多个节点提供服务
- Quorum:多数。当有N个Peer,多数就表明Q(Q>N/2)个Peer
- Leader:主节点,最多存在一个。表明zookeeper系统中主要的工做进程,Leader才是真正处理全部zookeeper写请求的节点,写请求会从Leader广播到Quorum Follower
- Follower:从节点,能够有多个。若是client对zookeeper发起一个读请求,Follower能够直接处理。若是client对zookeeper发起一个写请求,Follower须要转发到惟一的Leader,再有Leader处理并发起广播
- transaction:事务,一次写请求就表明一次事务
- zxid:事务id。zk为了保证消息有序性,提出了事务编号这个概念。zxid是一个二元组<e,c>,e表明选举纪元(若是将选举理解更皇帝的选举,这纪元表明年号),c表明事务的计数,同一纪元内每次出现写请求,c自增(表明某个年号内通过了多少年)。容易理解,纪元不变时,计数不断增长,纪元变化时,计数清零
- lastZxid:peer中存储的最新的zxid
- vote:选票,表明二元组<zi,i>。zk会给每一个peer打上惟一标识,即i,二元组的i就表明了选举的peer,而zi表明了这个peer上最新的zxid
-
:判断符号右边的数据比左边的数据更新。若是是zxid,z1z2表明z1.e < z2.e或者z1.e = z2.e,z1.c < z2.c,若是是vote,v1v2表明v1.zi < v2.zi或者v1.zi = v2.zi,v1.i < v2.i
- state:节点状态,目前只有这三种election、leading、following
- history:记录全部事务的日志
- lastCommittedZxid:最近提交成功的事务
- oldThreshold:日志中记录最先提交成功的事务。之因此设置一个最先的门槛,就是以为没有必要保留很是久远之前的事务,以便减小对内存的占用以及数据同步带来的网络开销
详解
Zab Theory and practice这篇论文中解释说,Zab的理论和实现并不彻底一致。理论就是yahoo发布的关于zab的论文,实现也就是Zookeeper开源代码。服务器
实现是在理论的基础之上作了一些优化手段,这些优化是在zookeeper不断发展的过程当中给加上的。我接下来的讲解,也是参照这篇论文,以zookeeper 3.3.3的版本的实现为准,并用本身的语言总结。网络
理论中的协议
随着系统启动或者恢复,会经历Zab协议中描述的以下四个阶段并发
- 阶段0:Leader选举。每一个peer从Quorum peer中选出本身心中的预备leader
- 阶段1:发现。预备leader从Quorum Follower中发现最新的数据,并覆盖本身的过时数据
- 阶段2:同步。预备leader采用二阶段提交的方式将本身的最新数据同步给Quorum Follower。完成这个步骤,预备leader就转为正式leader
- 阶段3:广播。Leader接受写请求,并经过二阶段提交的方式广播给Quorum Follower
以前我只是简述了一下理论中的协议,然而理想很骨感,有不少须要改进或者妥协的地方。下面我会一一阐明:分布式
- 阶段0的选举leader实际上很简陋,只是“看对眼”了就选为预备leader,因此预备leader的数据可能并不是最新
- 预备leader数据过时,就须要用阶段1来弥补,经过互相传输数据,来发现最新的数据,并完成预备leader的数据升级
- 更多的网络间数据传输表明了更大的网络开销
协议实现
了解了理想的骨感以后,咱们回归现实。
真正apache zookeeper在实现上提出设想:优化leader选举,直接选出最新的Peer做为预备Leader,这样就能将阶段0和阶段1合并,减小网络上的开销和多流程的复杂性

由图可知,代码实现上,协议被简化为三个阶段
- 快速选举Leader阶段:从Quorum Peer中选出数据最新的peer做为leader
- 恢复阶段:Leader将数据同步给Quorum Follower
- 广播阶段:Leader接受写请求,并广播给Quorum Follower
Talk is cheap.Show me the Code
这时Linus说过的一句话,不管语言多么有力,只有代码才能真正展示做者的思想。以前我只是对是协议实现的三个阶段作了一番简述,只有代码才能真正拨开Zab协议外面那层迷雾。(为了避免浪费篇幅,这里采用伪代码)
快速选举Leader阶段(FLE)

首先初始化一些数据
- ReceivedVotes:投票箱,存储投票节点以及当前投票信息
- OutOfElection:存储已经成为Leader、Follower,不参与投票的节点信息,以及它的历史投票信息
- 在选票中写上本身的大名
- send notification:发起选票通知,该节点会携带选票,进入目标节点的队列中,至关于给本身投了一票,并毛遂自荐给其余人
若是当前节点处于选举状态,则它也会接到选票通知。它会从队列中不断轮询节点,以便获取选票信息(若是超时,则不断放松超时时间,直至上限)。根据轮询出来的发送节点的状态,来作相应的处理。
- election:若是发送节点的轮次(round)大于本身,说明本身的选举信息过期,则更新本身的选举轮次,清空投票箱,更新本身的选票内容,并将新的选票通知给其余节点;若是发送节点的轮次等于本身,而且投票内容比本身的更新,则只须要更新本身的选票,并通知给其余节点就行;若是发送节点的轮次小于本身,说明投票内容过时,没有参考意义,直接忽略。全部未被忽略的选票,都会进入投票箱。最终根据选票箱中的结果,判断当前节点的选票是否是占大多数,若是是就根据当前节点的选票选出Leader
- leading或following:发送节点轮次等于本身,说明发送节点还参与投票,若是发送节点是Leading或者它的选票在选票箱中占大多数,则直接完成选举;若是发送节点已经完成选举(轮次不一样)或者它收集的选票较少,那么它的信息都会存放在OutOfElection中。当节点不断完成选举,OutOfElection中数量逐渐变成Quorum时,就把OutOfElection当作投票箱,从中检查发送节点的选票是否占多数,若是是就直接选出Leader
恢复阶段

通过FLE,已经选出了日志中具备最新已提交的事务的节点做为预备Leader。下面就分Leader和Follower两个视角来介绍具体实现。
Leader视角
首先,更新lastZxid,将纪元+1,计数清零,宣布改朝换代啦。而后在每次接收到Follower的数据同步请求时,都会将本身lastZxid反馈回去,表示全部Follower以本身的lastZxid为准。接下来,根据具体状况来判断该如何将数据同步给Follower
- 若是Leader的历史提交事务比Follower的最新事务要新,说明Follower的数据有待更新。更新方式取决于,Leader最先事务有没有比Follower最新事务要新:若是前者更新,说明在Leader看来Follower全部记录的事务都太过陈旧,没有保留价值,这时只须要将Leader全部history发给Follower就行(响应SNAP);若是后者更新,说明在Leader看来,Follower从本身的lastZxid开始到Leader日志的最新事务,都须要同步,因而将这一部分截取并发送给Follower(响应DIFF)
- 若是Leader的历史提交事务没有Follower的最新事务新,说明Follower存在没有提交的事务,这些事务都应该被丢弃(响应TRUNC)
当Follower完成同步时,会发送同步ack,当Leader收到Quorum ack时,表示数据同步阶段大功告成,进入最后的广播阶段。
Follower视角
通知Leader,表示本身但愿能同步Leader中的数据。
- 当收到Leader的拒绝响应时,说明Leader不认可本身做为Follower,有可能该Leader并不可靠,因而开始从新开始FLE
- 当收到SNAP或DIFF响应时,Follower会同步Leader发送过来的事务
- 当收到TRUNC响应,Follower会丢弃全部未完成的数据
当每一个Follower完成上述的同步过程时,会发送ack给Leader,并进入广播阶段。
广播阶段

进入到这个阶段,说明全部数据完成同步,Leader已经转正。开始zookeper最多见的工做流程:广播。
广播阶段是真正接受事务请求(写请求)的阶段,也表明了zookeeper正常工做阶段。全部节点都能接受客户端的写请求,可是Follower会转发给Leader,只有Leader才能将这些请求转化成事务,广播出去。这个节点同样有两个角色,下面仍是按照这两个角色来说解。
Leader视角:
- Leader必须通过ready,才能接受写请求。完成ready的Leader不断接受写请求,转化成事务请求,广播给Quorum Follower。
- 当Leader接收到ack时,说明Follower完成相应处理,Leader广播提交请求,Follower完成提交。
- 当发现新Peer请求做为Follower加入时,将本身的纪元、事务日志发送给该Peer,以便它完成上述恢复阶段的过程。收到该Peer的同步完成的ack时,Leader会发送提交请求,以便Peer提交全部同步完成的事务。这时,该Peer转正为Follower,被Leader归入Quorum Follower中。
Follower视角:
- Follower被发现是Leading状态,则执行ready过程,用来接受写请求。
- 当接受到Leader广播过来的事务请求时,Follower会将事务记录在history,并响应ack。
- 当接收到Leader广播过来的提交请求时,Follower会检查history中有没有还没有提交的事务,若是有,须要等待以前的事务按照FIFO顺序提交以后,才能提交本事务。
总结
这篇文章没有介绍Zookeeper的使用,而是着重讲解它的核心协议Zab的实现。正如文中说起,Zab最先的设想和如今的实现并不相同,今日的实现是在Zookeeper不断发展壮大的过程当中不断优化、改进而来的,也许早期的实现就是yahoo论文中构想的那样。罗马不是一日建成,任何人都不能期望一口吃个大胖子。若是Zookeeper刚开始就想着如何优化到极致,那反而会严重影响到这个项目自己的价值,由于它极可能还没面试就被淘汰。
We should forget about small e ciencies, say about 97% of the time: premature optimization is the root of all evil.
Knuth提醒咱们,过早优化是万恶之源。可是同时,一个好的程序员也不会忘记须要优化的那部分,他会定位相应的代码,而后针对性的修改。这也是zookeeper的开发者所作的。
问题汇总
-
通常Leader election算法都收到Quorum节点的选票,为何?
基于如下两点:
- 防止选出多个Leader,每人一票,同一纪元最多选出一个Leader
- 当log被提交时,说明Quorum节点存储了这个log。当进行Leader election的时候,当收到Quorum选票称为Leader时,一定存储了最新被提交的log