zookeeper(四)Paxos和ZAB协议

(一)Paxos协议

   为保证分布式系统的高可靠和高可用性,数据在系统中一般存储多个副本。当某个副本所在的节点出现故障时,分布式系统能够自动将服务切换到其他的副本,从而实现自动容错。同一份数据的多个副本中往往有一个副本为主副本,其他为备副本。从一份数据的角度讲,主副本所在的节点为主节点,备副本所在的节点为备节点。但在整个系统范围上看,每个节点即是主节点,也是备节点。

一致性解决方案:

  • 2PC/3PC
  • Paxos
  • Raft

拜占庭将军问题

Leslie Lamport 教授在三十多年前发表了论文《拜占庭将军问题》。拜占庭假设是对现实世界的模型化,由于硬件错误、网络拥塞或断开以及遭到恶意攻击,计算机和网络可能出现不可预料的行为。
拜占庭帝国就是5~15世纪的东罗马帝国,拜占庭即现在土耳其的伊斯坦布尔。我们可以想象,拜占庭军队有许多分支,驻扎在敌人城外,每一分支由各自的将军指挥。将军们智能靠通讯员进行通讯。在观察敌人以后,忠诚的将军们必须制订一个统一的行动计划——进攻或者撤退。然而,这些将军冢有叛徒,他们不希望忠诚的将军们能达成一致,因而影响统一行动计划的制订与传播。
问题:将军们必须有一个协议,使所有忠诚的将军们能够达成一致,而且少数几个叛徒不能使忠诚的将军们作出错误的计划——使有些将军进攻而另一些将军撤退。
假设:如果这11位将军中有间谍呢? 假设有9位忠诚的将军,5位判断进攻,4位判断撤退,还有2个间谍恶意判断撤退,虽然结果是错误的撤退,但这种情况完全是允许的。因为这11位将军依然保持着状态一致性。

分布式数据库最糟糕的问题绝对不是写入或者读取失败,而是状态不同步,还感知不到。这个的后果就是correctness不能保证,那程序就没有任何意义了。

(1)Paxos算法

基本定义

Paxos的目的是让整个集群的结点对某个值的变更达成一致。Paxos算法基本上来说是个大多数的决定会成个整个集群的统一决定。任何一个点都可以提出要修改某个数据的提案,是否通过这个提案取决于这个集群中是否有超过半数的结点同意(所以Paxos算法需要集群中的结点是单数)。

算法中的参与者主要分为三个角色,同时每个参与者又可兼领多个角色:

  • Proposer: 提出提案,提案信息包括提案编号和提议的value
  • Acceptor: 收到提案后可以接受(accept)提案
  • Learner: 只能"学习"被批准的提案

Paxos协议条件设定:

  • 设定1:每个议案比必须有一个编号,并且编号只能增长,不能重复。越往后越大。
  • 设定2:议案有2种(提交的议案,批准的议案)
  • 设定3:如果Acceptor没有接受任何议案,那么他必须接受第一个议案
  • 设定4:接受编号大的议案,如果小于之前接受议案编号,那么不接受

算法详解

Paxos算法有两个阶段

  • Prepare阶段(第一阶段)
  • Accept阶段(第二阶段)

第一阶段

  1. Proposer希望议案V。首先发出Prepare请求至大多数Acceptor。Prepare请求内容为序列号K;
  2. Acceptor收到Prepare请求为编号K后,检查自己手里是否有处理过Prepare请求;
  3. 如果Acceptor没有接受过任何Prepare请求,那么用OK来回复Proposer,代表Acceptor必须接受收到的第一个议案;
  4. 否则,如果Acceptor之前接受过任何Prepare请求(如:MaxN),那么比较议案编号,如果K<MaxN,则用reject或者error回复Proposer;
  5. 如果K>=MaxN,那么检查之前是否有批准的议案,如果没有则用OK来回复Proposer,并记录K;
  6. 如果K>=MaxN,那么检查之前是否有批准的议案,如果有则回复批准的议案编号和议案内容(如:<AcceptN, AcceptV>, AcceptN为批准的议案编号,AcceptV为批准的议案内容)。

在这里插入图片描述
第二阶段

  1. Proposer收到过半Acceptor发来的回复,回复都是OK,且没有附带任何批准过的议案编号和议案内容。那么Proposer继续提交批准请求,不过此时会连议案编号K和议案内容V一起提交(<K, V>这种数据形式)
  2. Proposer收到过半Acceptor发来的回复,回复都是OK,且附带批准过的议案编号和议案内容(<pok,议案编号,议案内容>)。那么Proposer找到所有回复中AcceptN最大的那个AccpetV(假设为<pok,AcceptNx,AcceptVx>)作为提交批准请求(请求为<K,AcceptVx>)发送给Acceptor。
  3. Proposer没有收到过半Acceptor发来的回复,则修改议案编号K为Kx,并将编号重新发送给Acceptors(重复Prepare阶段的过程)
  4. Acceptor收到Proposer发来的Accept请求,如果编号K<MaxN则不回应或者reject。
  5. Acceptor收到Proposer发来的Accept请求,如果编号K>=MaxN则批准该议案,并设置手里批准的议案为<K,接受议案的编号,接受议案的内容>,回复Proposer。
  6. 经过一段时间Proposer对比手里收到的Accept回复,如果超过半数,则结束流程(代表议案被批准),同时通知Leaner可以学习议案。
  7. 经过一段时间Proposer对比手里收到的Accept回复,如果未超过半数,则修改议案编号重新进入Prepare阶段。

(2)Paxos流程图
在这里插入图片描述
(二)ZAB协议

关于ZXID

ZXID是一个64位的数据结构。分为高32位,低32位。(由Leader产生,而且它是自增唯一有顺序性)。高32位用来存储epoch(年号),低32位用来存储事务编号。有3种情况导致高32位会变化:

  1. 事务编号满了,触发Leader选举。
  2. 集群重启,触发Leader选举
  3. Leader宕机,触发Leader选举

org.apache.zookeeper.server.util.ZxidUtils Zxid生成源码

public class ZxidUtils {
    static public long getEpochFromZxid(long zxid) {
	return zxid >> 32L;
    }
    static public long getCounterFromZxid(long zxid) {
	return zxid & 0xffffffffL;
    }
    // 1. 高32位存放epoch年号
    // 2. 低32位存放事务递增序列
    static public long makeZxid(long epoch, long counter) {
	return (epoch << 32L) | (counter & 0xffffffffL);
    }
    static public String zxidToString(long zxid) {
	return Long.toHexString(zxid);
    }
}

所有的事物性操作都是由Leader来进行的,即便这个请求是Follower接收到,Follower也会把这个事务性请求转发给Leader。

ZAB协议简介

ZooKeeper并没有完全采用Paxos算法,而是使用了一种称之为ZooKeeper Atomic Broadcast(ZAB,ZooKeeper原子广播协议)的协议作为其数据一致性的核心算法。
ZAB协议并不像Paxos算法那样,是一种通用的分布式一致性算法,它是一种特别为ZooKeeper设计的崩溃恢复的原子消息广播算法。ZooKeeper采用一个单一的主进程接受并处理客户端的所有事务请求,并将服务器数据的状态变更以事务Proposal的形式广播到所有的副本进程上去。

ZAB协议包含两种基本的模式:

  • 崩溃恢复

  • 消息广播

在这里插入图片描述
ZAB协议包含三个阶段:

  • 阶段1:发现(Leader选举过程)
  • 阶段2:同步(数据同步过程)
  • 阶段3:广播(正式接受请求过程)

当整个服务框架在启动过程中,或是当 Leader 服务器出现网络中断、崩溃退出与重启等异常情况时, ZAB 协议就会进入恢复模式并选举产生新的 Leader 服务器。当选举产生了新的Leader 服务器同时集群中已经有过半的机器与该 Leader 服务器完成了状态同步之后,ZAB 协议就会退出恢复模式。

当集群中已经有过半的 Follower 服务器完成了和 Leader 服务器的状态同步,那么整个服务框架就可以进入消息广播模式了。当一台同样遵守 ZAB 协议的服务器启动后加入到集群中时,如果此时集群中已经存在一个 Leader 服务器在负责进行消息广播 , 那么新加入的服务器就会自觉地进入数据恢复模式:找到 Leader 所在的服务器,并与其进行数据同步,然后一起参与到消息广播流程中去。

org.apache.zookeeper.server.quorum.QuorumPeer.ServerState

public enum ServerState {
   LOOKING, FOLLOWING, LEADING, OBSERVING;
}

(1)崩溃恢复

当整个服务器在启动过程中,或者当Leader服务器出现网络中断、崩溃退出与重启等异常情况时,ZAB协议就会进入恢复模式并通过选举产生新的Leader服务器。

■ 当选举产生了新的Leader服务器,同时集群中已经有过半机器与该Leader服务器完成状态同步之后,ZAB协议就会退出恢复模式。这里的状态同步指的就是数据同步,用来保证集群中存在过半的机器能够和Leader服务器的数据保持一致。
■ 当集群中有过半的Follower服务器完成和Leader服务器的同步,那么整个服务器集群就可以进入消息广播模式。
■ 当新的机器加入集群,由于集群已经存在一个Leader,那么新加入的机器会进入数据同步模式,即找到Leader服务器,并与其进行数据同步。
■ 当Leader崩溃退出或者重启,或者及集群中不存在过半的服务器可以和Leader保持正常通信,那么在开始新一轮事务操作前所有机器会使用崩溃恢复协议来达到一个一致性的状态。
在这里插入图片描述
ZAB协议规定了如果一个事务Proposal在一台机器上被处理成功,那么应该在所有的机器上都被处理成功,哪怕机器出现故障崩溃。

  1. 每个Server会发出一个投票,第一次都是投自己。投票信息:(myid,ZXID)
  2. 收集来自各个服务器的投票
  3. 处理投票并重新投票,处理逻辑:优先比较ZXID,然后比较myid
  4. 统计投票,只要超过半数的机器接收到同样的投票信息,就可以确定Leader
  5. 改变服务器状态
// 选票和服务器的当前选票进行对比
protected boolean totalOrderPredicate(long newId, long newZxid, long newEpoch, long curId, long curZxid, long curEpoch) {
    LOG.debug("id: " + newId + ", proposed id: " + curId + ", zxid: 0x" +
              Long.toHexString(newZxid) + ", proposed zxid: 0x" + Long.toHexString(curZxid));
    if(self.getQuorumVerifier().getWeight(newId) == 0){
        return false;
    }

    /*
     * 以下三种情况成立,则返回true:
     * 1- 新epoch更大
     * 2- 新epoch与当前epoch相同,但新zxid更大
     * 3- 新epoch与当前epoch相同,新zxid与当前zxid相同,但是服务器id更高。
     */
    return ((newEpoch > curEpoch) || 
            ((newEpoch == curEpoch) &&
             ((newZxid > curZxid) || ((newZxid == curZxid) && (newId > curId)))));

(2)消息广播

消息广播类似于一个2PC提交过程。根据客户端的事务请求,Leader服务器会为其生成对应的事务投票(即Proposal)并将其发送给集群中其他服务器,然后在分表搜集各自的选票,最后进行事务提交。

与2PC不同的是,ZAB协议没有中断逻辑(所有Follower要么对Leader提成的事务Ack,要么就不回应),而且当过半的Follower服务器反馈Ack之后就开始提交事务,不用等待所有Follower都反馈。

整个消息广播协议是基于FIFO特性的TCP协议来进行网络通信,因此能够很容易地保证消息广播过程中消息接受与发送的顺序性。

在这里插入图片描述

  1. Leader接收到消息请求后,将消息赋予一个全局唯一的64位自增id,叫做:Zxid,通过zxid的大小比较即可实现因果有序这一特性。
  2. Leader通过先进先出队列(通过 TCP 协议来实现,以此实现了全局有序这一特性)将带有zxid的消息作为一个提案(Proposal)分发给所有Follower。
  3. 当Follower接收到Proposal,先将Proposal写到硬盘,写硬盘成功后再向Leader回一个ACK。
  4. 当Leader接收到合法数量的ACKs后,Leader就向所有Follower发送COMMIT命令,同时会在本地执行该消息。
  5. 当Follower收到消息的COMMIT命令时,就会执行该消息。

(3)数据同步

整个集群完成Leader选举后,Learner会向Leader进行注册,当Learner向Leader完成注册后,就进入数据同步环节,同步过程就是Leader将那些没有在Learner服务器上提交过的事务请求同步给Learner服务器。

直接差异化同步

举例,某个时刻Leader服务器的事务队列对应的ZXID依次是:

0x200000001
0x200000002
0x200000003
0x200000004
0x200000005

而需要数据同步的服务器最后处理的ZXID为:

0x200000003

这种场景就执行“直接差异化同步”,Leader会依次将0x200000004,0x200000005同步给服务器,同步过程中顺序如下:
在这里插入图片描述

先回滚再差异化同步

假如在ZooKeeper集群中有A、B、C三台服务器,B当选为Leader服务器。
某个时刻,B正要处理一个ZXID=0x200000003的事务,并且已经将该事务写入到B服务器的本地的事务日志中,就在B要发送给其他Follower A、C机器进行同步的时候,B服务器挂了,Proposal并没有发送出去,而此时此时ZooKeeper会进行新一轮选举。假设A当选为新的Leader服务器对外进行工作,客户端又提交了。

0x300000001
0x300000002

而此时之前的奔溃的B服务器再次启动,并开始进行数据同步。
因为B之前为Leader,故它的本地日志中事务编号为:

0x200000001
0x200000002
0x200000003

而A、C的本地日志中的事务编号为:

0x200000001
0x200000002
0x300000001
0x300000002

这时候就需要A服务器对数据进行回滚之后再同步,这个就称之为“先回滚再差异化同步”
在这里插入图片描述
仅回滚同步

先回滚再差异化的特殊模式。

全量同步

如:新加入的Follower服务器。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值