zookeeper(简称zk),顾名思义,为动物园管理员的意思,动物对应服务节点,zk是这些节点的管理者。在分布式场景中,zk的应用很是普遍,如:数据发布/订阅、命名服务、配置中心、分布式锁、集群管理、选主与服务发现等等。这不只得益于zk类文件系统的数据模型和基于Watcher机制的分布式事件通知,也得益于zk特殊的高容错数据一致性协议。
这里的一致性,是指数据在多个副本之间保持一致的特性。分布式环境里,多个副本处于不一样的节点上,若是对副本A的更新操做,未同步到副本B上,外界获取数据时,A与B的返回结果会不同,这是典型的分布式数据不一致状况。而强一致性,是指分布式系统中,若是某个数据更新成功,则全部用户都能读取到最新的值。CAP定理告诉咱们,在分布式系统设计中,P(分区容错性)是不可缺乏的,所以只能在A(可用性)与C(一致性)间作取舍。本文主要探究zk在数据一致性方面的处理逻辑。
1、基本概念:
算法
数据节点(dataNode):zk数据模型中的最小数据单元,数据模型是一棵树,由斜杠(/)分割的路径名惟一标识,数据节点能够存储数据内容及一系列属性信息,同时还能够挂载子节点,构成一个层次化的命名空间。数据库
会话(Session):指zk客户端与zk服务器之间的会话,在zk中,会话是经过客户端和服务器之间的一个TCP长链接来实现的。经过这个长链接,客户端可以使用心跳检测与服务器保持有效的会话,也能向服务器发送请求并接收响应,还可接收服务器的Watcher事件通知。Session的sessionTimeout,是会话超时时间,若是这段时间内,客户端未与服务器发生任何沟通(心跳或请求),服务器端会清除该session数据,客户端的TCP长链接将不可用,这种状况下,客户端须要从新实例化一个Zookeeper对象。apache
事务及ZXID:事务是指可以改变Zookeeper服务器状态的操做,通常包括数据节点的建立与删除、数据节点内容更新和客户端会话建立与失效等操做。对于每一个事务请求,zk都会为其分配一个全局惟一的事务ID,即ZXID,是一个64位的数字,高32位表示该事务发生的集群选举周期(集群每发生一次leader选举,值加1),低32位表示该事务在当前选择周期内的递增次序(leader每处理一个事务请求,值加1,发生一次leader选择,低32位要清0)。缓存
事务日志:全部事务操做都是须要记录到日志文件中的,可经过 dataLogDir配置文件目录,文件是以写入的第一条事务zxid为后缀,方便后续的定位查找。zk会采起“磁盘空间预分配”的策略,来避免磁盘Seek频率,提高zk服务器对事务请求的影响能力。默认设置下,每次事务日志写入操做都会实时刷入磁盘,也能够设置成非实时(写到内存文件流,定时批量写入磁盘),但那样断电时会带来丢失数据的风险。安全
数据快照:数据快照是zk数据存储中另外一个很是核心的运行机制。数据快照用来记录zk服务器上某一时刻的全量内存数据内容,并将其写入到指定的磁盘文件中,可经过dataDir配置文件目录。可配置参数snapCount,设置两次快照之间的事务操做个数,zk节点记录完事务日志时,会统计判断是否须要作数据快照(距离上次快照,事务操做次数等于snapCount/2~snapCount 中的某个值时,会触发快照生成操做,随机值是为了不全部节点同时生成快照,致使集群影响缓慢)。服务器
过半:所谓“过半”是指大于集群机器数量的一半,即大于或等于(n/2+1),此处的“集群机器数量”不包括observer角色节点。leader广播一个事务消息后,当收到半数以上的ack信息时,就认为集群中全部节点都收到了消息,而后leader就不须要再等待剩余节点的ack,直接广播commit消息,提交事务。选举中的投票提议及数据同步时,也是如此,leader不须要等到全部learner节点的反馈,只要收到过半的反馈就可进行下一步操做。网络
2、数据模型
zk维护的数据主要有:客户端的会话(session)状态及数据节点(dataNode)信息。zk在内存中构造了个DataTree的数据结构,维护着path到dataNode的映射以及dataNode间的树状层级关系。为了提升读取性能,集群中每一个服务节点都是将数据全量存储在内存中。可见,zk最适于读多写少且轻量级数据(默认设置下单个dataNode限制为1MB大小)的应用场景。数据仅存储在内存是很不安全的,zk采用事务日志文件及快照文件的方案来落盘数据,保障数据在不丢失的状况下能快速恢复。
3、集群架构 zk集群由多个节点组成,其中有且仅有一个leader,处理全部事务请求;follower及observer统称learner。learner须要同步leader的数据。follower还参与选举及事务决策过程。zk客户端会打散配置文件中的serverAddress 顺序并随机组成新的list,而后循环按序取一个服务器地址进行链接,直到成功。follower及observer会将事务请求转交给leader处理。
session
要搭建一个高可用的zk集群,咱们首先须要肯定好集群规模。通常咱们将节点(指leader及follower节点,不包括observer节点)个数设置为 2*n+1 ,n为可容忍宕机的个数。 zk使用“过半”设计原则,很好地解决了单点问题,提高了集群容灾能力。可是zk的集群伸缩不是很灵活,集群中全部机器ip及port都是事先配置在每一个服务的zoo.cfg 文件里的。若是要往集群增长一个follower节点,首先须要更改全部机器的zoo.cfg,而后逐个重启。
集群模式下,单个zk服务节点启动时的工做流程大致以下:
数据结构
统一由QuorumPeerMain做为启动类,加载解析zoo.cfg配置文件;
架构
初始化核心类:ServerCnxnFactory(IO操做)、FileTxnSnapLog(事务日志及快照文件操做)、QuorumPeer实例(表明zk集群中的一台机器)、ZKDatabase(内存数据库)等;
加载本地快照文件及事务日志,恢复内存数据;
完成leader选举,节点间经过一系列投票,选举产生最合适的机器成为leader,同时其他机器成为follower或是observer。关于选举算法,就是集群中哪一个机器处理的数据越新(经过ZXID来比较,ZXID越大,数据越新),其越有可能被选中;
完成leader与learner间的数据同步:集群中节点角色肯定后,leader会从新加载本地快照及日志文件,以此做为基准数据,再结合各个learner的本地提交数据,leader再肯定须要给具体learner回滚哪些数据及同步哪些数据;
当leader收到过半的learner完成数据同步的ACK,集群开始正常工做,能够接收并处理客户端请求,在此以前集群不可用。
4、zookeeper一致性协议
zookeeper实现数据一致性的核心是ZAB协议(Zookeeper原子消息广播协议)。该协议须要作到如下几点:
(1)集群在半数如下节点宕机的状况下,能正常对外提供服务;
(2)客户端的写请求所有转交给leader来处理,leader需确保写变动能实时同步给全部follower及observer;
(3)leader宕机或整个集群重启时,须要确保那些已经在leader服务器上提交的事务最终被全部服务器都提交,确保丢弃那些只在leader服务器上被提出的事务,并保证集群能快速恢复到故障前的状态。
Zab协议有两种模式, 崩溃恢复(选主+数据同步)和消息广播(事务操做)。任什么时候候都须要保证只有一个主进程负责进行事务操做,而若是主进程崩溃了,就须要迅速选举出一个新的主进程。主进程的选举机制与事务操做机制是紧密相关的。下面详细讲解这三个场景的协议规则,从细节去探索ZAB协议的数据一致性原理。
一、选主:leader选举是zk中最重要的技术之一,也是保证分布式数据一致性的关键所在。当集群中的一台服务器处于以下两种状况之一时,就会进入leader选举阶段——服务器初始化启动、服务器运行期间没法与leader保持链接。
选举阶段,集群间互传的消息称为投票,投票Vote主要包括二个维度的信息:ID、ZXID
ID 被推举的leader的服务器ID,集群中的每一个zk节点启动前就要配置好这个全局惟一的ID。
ZXID 被推举的leader的事务ID ,该值是从机器DataTree内存中取的,即事务已经在机器上被commit过了。
节点进入选举阶段后的大致执行逻辑以下:
(1)设置状态为LOOKING,初始化内部投票Vote (id,zxid) 数据至内存,并将其广播到集群其它节点。节点首次投票都是选举本身做为leader,将自身的服务ID、处理的最近一个事务请求的ZXID(ZXID是从内存数据库里取的,即该节点最近一个完成commit的事务id)及当前状态广播出去。而后进入循环等待及处理其它节点的投票信息的流程中。
(2)循环等待流程中,节点每收到一个外部的Vote信息,都须要将其与本身内存Vote数据进行PK,规则为取ZXID大的,若ZXID相等,则取ID大的那个投票。若外部投票胜选,节点须要将该选票覆盖以前的内存Vote数据,并再次广播出去;同时还要统计是否有过半的赞同者与新的内存投票数据一致,无则继续循环等待新的投票,有则须要判断leader是否在赞同者之中,在则退出循环,选举结束,根据选举结果及各自角色切换状态,leader切换成LEADING、follower切换到FOLLOWING、observer切换到OBSERVING状态。
算法细节可参照FastLeaderElection.lookForLeader(),主要有三个线程在工做:选举线程(主动调用lookForLeader方法的线程,经过阻塞队列sendqueue及recvqueue与其它两个线程协做)、WorkerReceiver线程(选票接收器,不断获取其它服务器发来的选举消息,筛选后会保存到recvqueue队列中。zk服务器启动时,开始正常工做,不中止)以及WorkerSender线程(选票发送器,会不断地从sendqueue队列中获取待发送的选票,并广播至集群)。WorkerReceiver线程一直在工做,即便当前节点处于LEADING或者FOLLOWING状态,它起到了一个过滤的做用,当前节点为LOOKING时,才会将外部投票信息转交给选举线程处理;若是当前节点处于非LOOKING状态,收到了处于LOOKING状态的节点投票数据(外部节点重启或网络抖动状况下),说明发起投票的节点数据跟集群不一致,这时,当前节点须要向集群广播出最新的内存Vote(id,zxid),落后节点收到该Vote后,会及时注册到leader上,并完成数据同步,跟上集群节奏,提供正常服务。
二、选主后的数据同步:选主算法中的zxid是从内存数据库中取的最新事务id,事务操做是分两阶段的(提出阶段和提交阶段),leader生成提议并广播给followers,收到半数以上的ACK后,再广播commit消息,同时将事务操做应用到内存中。follower收到提议后先将事务写到本地事务日志,而后反馈ACK,等接到leader的commit消息时,才会将事务操做应用到内存中。可见,选主只是选出了内存数据是最新的节点,仅仅靠这个是没法保证已经在leader服务器上提交的事务最终被全部服务器都提交。好比leader发起提议P1,并收到半数以上follower关于P1的ACK后,在广播commit消息以前宕机了,选举产生的新leader以前是follower,未收到关于P1的commit消息,内存中是没有P1的数据。而ZAB协议的设计是须要保证选主后,P1是须要应用到集群中的。这块的逻辑是经过选主后的数据同步来弥补。
选主后,节点须要切换状态,leader切换成LEADING状态后的流程以下:
(1)从新加载本地磁盘上的数据快照至内存,并从日志文件中取出快照以后的全部事务操做,逐条应用至内存,并添加到已提交事务缓存commitedProposals。这样能保证日志文件中的事务操做,一定会应用到leader的内存数据库中。
(2)获取learner发送的FOLLOWERINFO/OBSERVERINFO信息,并与自身commitedProposals比对,肯定采用哪一种同步方式,不一样的learner可能采用不一样同步方式(DIFF同步、TRUNC+DIFF同步、SNAP同步)。这里是拿learner内存中的zxid与leader内存中的commitedProposals(min、max)比对,若是zxid介于min与max之间,但又不存在于commitedProposals中时,说明该zxid对应的事务须要TRUNC回滚;若是 zxid 介于min与max之间且存在于commitedProposals中,则leader须要将zxid+1~max 间全部事务同步给learner,这些内存缺失数据,极可能是由于leader切换过程当中形成commit消息丢失,learner只完成了事务日志写入,未完成提交事务,未应用到内存。
(3)leader主动向全部learner发送同步数据消息,每一个learner有本身的发送队列,互不干扰。同步结束时,leader会向learner发送NEWLEADER指令,同时learner会反馈一个ACK。当leader接收到来自learner的ACK消息后,就认为当前learner已经完成了数据同步,同时进入“过半策略”等待阶段。当leader统计到收到了一半已上的ACK时,会向全部已经完成数据同步的learner发送一个UPTODATE指令,用来通知learner集群已经完成了数据同步,能够对外服务了。
细节可参照Leader.lead() 、Follower.followLeader()及LearnerHandler类。
三、事务操做:ZAB协议对于事务操做的处理是一个相似于二阶段提交过程。针对客户端的事务请求,leader服务器会为其生成对应的事务proposal,并将其发送给集群中全部follower机器,而后收集各自的选票,最后进行事务提交。流程以下图。
ZAB协议的二阶段提交过程当中,移除了中断逻辑(事务回滚),全部follower服务器要么正常反馈leader提出的事务proposal,要么就抛弃leader服务器。follower收到proposal后的处理很简单,将该proposal写入到事务日志,而后立马反馈ACK给leader,也就是说若是不是网络、内存或磁盘等问题,follower确定会写入成功,并正常反馈ACK。leader收到过半follower的ACK后,会广播commit消息给全部learner,并将事务应用到内存;learner收到commit消息后会将事务应用到内存。
ZAB协议中屡次用到“过半”设计策略 ,该策略是zk在A(可用性)与C(一致性)间作的取舍,也是zk具备高容错特性的本质。相较分布式事务中的2PC(二阶段提交协议)的“全量经过”,ZAB协议可用性更高(牺牲了部分一致性),能在集群半数如下服务宕机时正常对外提供服务。
5、参考:
《从paxos到Zookeeper分布式一致性原理与实践》、org.apache.zookeeper:zookeeper:3.4.0