zookeeper集群运行详解

CAP理解

    我们说到zk集群经常会说到zk具有CAP中的CP,那CAP是什么呢?CAP是指衡量分布式系统的三个维度:

  • C:数据一致性。表示任意时候,客户端从服务器的任意一个节点获取一个数据,该数据的值都是一致的。
  • A:服务可用性。表示任意时候,面对客户端的请求,服务端都能在合理的时间内做出正确的响应,而不是让前端看到404、内存溢出之类的问题。
  • P:分区容错性。一个集群中多台机器,当其中部分机器宕掉或者失去联系后,其他机器依然能够提供服务,不会受影响。

    其实这三个维度,数据一致性很好理解,另外两个之前我一直比较混淆,总觉得他们两是同一个,所以这里说一下自己的理解,也不完全正确。A和P其实都是关注的集群的可用性,但A关注的是集群是否能够持续稳定可用,而P关注的是当集群中部分不可用时,剩下的是否能继续可用。我们刚好可以拿zk集群来举一个例子。
    由于zk的zab协议,无论什么时候客户端读取集群中的任意一个节点,读到的数据都是一样的从而保证了C。而zk集群的大多数可用即可用的设定保证了P,因为如果其中一台宕了,其他的节点依然不会有问题。但是zk的leader选举造成集群在leader选举期间是整体无响应的,所以zk集群无法保证可用性A。

PAXOS算法

    zk集群存在一个leader选举过程,而其选举的算法则是基于Paxos算法衍生而来的,所以我们先来说说这个数据一致方面堪称鼻祖的算法。首先定义一些概念:

  • Proposer提议者:算法中发出提案的人。
  • Acceptor接受者:算法中对提案进行表决的人。
  • 其他人:比如Leader,Learner这些我们都忽视,他们在paxos算法中几乎没有参与度。
  • (number,value):这是提案中的一组数据,number表示编号,每次重新发起提案,number都会变的更大从而让自己有机会通过提案。value表示提案中的值,最终由value的值决定到底谁成为Leader。
  • acceptedProposal(number,value):Acceptor会保存自己最新同意的一份提案即acceptedProposal(number,value),number为提案的编号,value为提案的值。最初的还没有发生提案时,为(null,null)。
  • max_number:Acceptor除了会保存一份最新的提案外,还会保存一个最大提案编号,同样最初为null。
  • 选举流程: 选举过程可以分为多轮,每一轮分为两个步骤:Prepare步骤和Accept步骤。Prepare步骤的提案只有一个编号(number),Prepare成功到了Accept步骤才会发完整的一个提案(number,value)。

选举流程如下:

hahaha

流程图中需要几点注意:

  • number的递增是需要全局唯一的,比如第一个提议者从1变成2,第二个提议者会直接变成3,不能变成2。你可能会想这个如何保证全局唯一,所以在zk的选举算法中比较完number之后,如果相同,会再比较value(即集群节点的myid,后面会细说),因为myid不可能重复,所以来保证了唯一性。
  • 由于接受者的acceptedProposal(number,value)一开始都是空的,所以每一个接受者都必然会接受他收到的第一个Accept提案,此时接受者们的acceptedProposal(number,value)会各不相同。而之后,在不发生活锁(下面会细说)的情况下,某一个提议者向所有接受者发起Accept请求,然后接受者们都会接受这个提案,然后大多数的接受者的acceptedProposal(number,value)就统一了,之后其他的接受者再次发送Prepare上来,大多数的接受者都会把这个统一的提案返回回去,提议者也就只会拿着这个统一的提案再次发起Accept,然后提议者也完成统一。
  • 对于前一点,你可能回想,如果某一个接受者由于网络延迟,等了很久返回回来一个提案,但是由于number的时序递增,那这个等了很久返回回来的提案number必然要小于大多数已经统一了的提案里的number,所以也就不会被选择成为Accept提案了。

这个算法其实却是挺绕的,自己也斟酌了很久,最后纠结于两点:

  • 假设有A1,B1三个提议者,A2,B2三个接受者(实际的情况是A1和A2可以是同一台机器,他同时扮演了两种角色),如果A1向A2,B2发送Prepare提议(number=1),B1向A2,B2发送Prepare提议(number=2),A2先收到A1的Prepare提案通过了返回(null,null),max_number=1,A1回去准备Accept提案。接着A2收到B1Prepare提案,因为2比1大,同样返回(null,null)然后把max_number改成2,准备回去Accept提案。同样B2接受者也按这样的顺序接受了A1和B1。等A1进行Accept提案的时候,就会发现number=1比max_number=2小了,然后被驳回了。A1赶紧改变自己的number=4再次发送Prepare提案。A2,B2收到Prepare提案后,修改了自己的max_number=4,然后A1又回去准备Accept提案。此时B1的Accept提案来了,结果发现max_number=4了,也只能被驳回了。然后A1和B1轮番上阵提交Prepare提案,生生不息,无穷已。后来发现这个问题Paxos是确实存在的,被称之为活锁,真正使用时是需要自己想办法避免的。
  • 最终每个提议者得出的Leader会不会不一样?一开始我是这么想的,A1对A2,B2完成了全流程,确定自己是Leader了,由于B1网太差,这时候A2,B2才收到他的Prepare提案,那B2会不会选出新的Leader?其实B1最终选出来的还是A1,因为B1Prepare提案中返回回来的acceptedProposal是A1的Accept提案提交上去的,然后B1进行Accept的时候这只能选择A1的value,然后B1和A1,A2,B2达成了一致。

ZAB协议

    Paxos算法奠定了分布式数据一致性的基石,而zk中的数据一致性则是由zk中的ZAB协议来保证,ZAB协议中,zookeeper集群一共分为两种状态,正常状态与崩溃恢复状态,正常状态即zk正常使用时的状态,崩溃恢复状态为Leader节点失去联系,其他节点进行Leader选举的状态(当集群启动时也是进入崩溃恢复状态,选取Leader),当zk在崩溃恢复状态时,是无法对外提供服务的,也就是zk无法保证CAP的A。

崩溃恢复状态

    zk在进行崩溃恢复时才用的选举算法叫FastLeaderElection,他是我们前面讲的Paxos算法的改造版,FastLeaderElection选举中的一些概念:

  • 服务器id:之前搭建集群时,每个节点都有的myid。
  • 事务id:当前服务器中最大的Zxid。
  • 逻辑时钟:发起投票轮数的计数器,记录本次投票为第几轮
  • 选举状态:LOOKING(竞选状态)、FOLLOWING(随从状态,同步Leader数据,参与投票)、OBSERVING(观察者状态,同步Leader数据,不参与投票)、LEADING(领导状态)。

集群进入崩溃恢复时,每个节点都进入LOOKING状态,服务器之间进行选举时需要互相传递参数为(服务器id,事务id,逻辑时钟,选举状态)。竞选流程如下:

Created with Raphaël 2.2.0 开始 轮次数++ 投票自己,广播出去 获取其他人的票 判断其他人是否为looking 接受者轮次>自己的轮次 更新自己的轮次数,清空投票箱 PK选票,接受的赢了,更新自己的选票 把自己选票广播出去 根据投票箱判断,自己的投票是否超过半数 如果自己是leader更新状态为Leading,否则为Following 结束 接受者轮次=自己的轮次 跟随其他人结果 yes no yes no yes no yes no

    可以看到FastLeaderElection把Paxos的两步改成了一步,某种意义上避免了Paxos的活锁。流程中有一个选票PK,其实是就是拿着自己投票的zxid和myid与接受到的zxid和myid进行对比,先比较zxid,谁大选谁,如果相同再比较myid,同样谁大选谁,由于集群中每一个节点的myid各不相同,所以就能得到PK结果。
    选举完成之后其他节点从Leading节点同步数据,完成数据同步的节点会被Leader拉入一个可提供服务的列表中,然后对外提供服务。
    需要注意的是,崩溃恢复之后Leader会重置集群的zxid,zxid是一个64位的数字,其中高32位代表轮次,低三十二位表示本轮次的事务id,所以每次重新选举完成之后,高32位会自增加一,低32位会归零。

正常工作状态

    在zk集群正常工作时,ZAB协议通过原子事务保证数据的一致性,当子节点收到来自客户端的一个非查询请求时,需要经历一下步骤:

  1. 将请求发送给Leader
  2. Leader对请求进行处理,并生成该请求的全局唯一事务id(zxid),处理完成之后创建事务,将请求和处理结果同步到所有节点上。
  3. 当有半数以上的节点响应了事务的创建(返回了ACK请求),Leader将事务提交。
  4. 事务提交后,将处理结果返回给最初接受请求的节点。
  5. 最初的节点再将处理结果返回给客户端。

    其实可以看到,所有的请求的处理都是由Leader来完成的,也是Leader来保证了每一个请求的有序性以及数据的全局唯一性。所以zk适合读多写少的场景,如果写入过多,Leader负载就太大了甚至不堪重负。
    

发布了39 篇原创文章 · 获赞 9 · 访问量 1016

猜你喜欢

转载自blog.csdn.net/qq_30095631/article/details/103568829