深入剖析Zookeeper原理(二)ZK集群选举原理

1. PAXOS选举算法
  1. Paxos算法概述

    背景:

    主流分布式一致性算法包括Paxos,Raft和ZAB,它们之间有怎样的区别与关系?

    Google Chubby的作者Mike Burrows说过,世上只有一种一致性算法,那就是Paxos,所有其他一致性算法都是Paxos算法的不完整或衍生版。

    什么是Paxos?

    Paxos算法是基于消息传递且具有高度容错特性的一致性算法,是目前公认的解决分布式一致性问题最有效的算法之一,其解决的问题就是在分布式系统中如何就某个值(决议)达成一致。

    Paxos的作用:

    常见的分布式系统中,总会发生机器宕机或网络异常(包括消息的延迟、丢失、重复、乱序)等情况。Paxos 算法是分布式一致性算法用来解决一个分布式系统如何就某个值(决议)达成一致性的问题。

  2. 拜占庭问题

    拜占庭将军问题是一个共识问题:一群将军想要实现某一个目标,必须是全体一致的决定,一致进攻或者一致撤退;但由于叛徒的存在,将军们不知道应该如何达成一致。

    举例来说:

    假设有三个拜占庭将军,分别为A、B、C,他们要讨论的只有一件事情:明天是进攻还是撤退。为此,将军们需要依据“少数服从多数”原则投票表决,只要有两个人意见达成一致就可以。

    如果A和B投进攻,C投撤退,那么传递结果:

    1. 那么A的信使传递给B和C的消息都是进攻;
    2. B的信使传递给A和C的消息都是进攻;
    3. 而C的信使传给A和B的消息都是撤退。

    通过以上决策,三个将军就都知道进攻和撤退占比是 2 : 1 。显而易见,进攻方胜出,第二天大家都要进攻,三者行动最终达成一致。

    但是,如果稍微做一个改动:三个将军中出了一个叛徒呢?叛徒的目的是破坏忠诚将军间一致性的达成,让拜占庭的军队遭受损失。

    假设A和B是忠诚将军,A投进攻,B投撤退,如果你是C这个叛徒将军,那么你该做些什么,才能在第二天让两个忠诚的将军做出相反的决定呢?

    进攻方和撤退方现在是 1 : 1 ,无论C投哪一方,都会变成 2 : 1 ,一致性还是会达成。但是,作为叛徒的C,你必然不会按照常规出牌,于是你让一个信使告诉A的内容是你“要进攻”,让另一个信使告诉B的则是你“要撤退”。

    至此,A将军看到的投票结果是:进攻方 撤退方 = 2 : 1 ,而B将军看到的是 1 : 2 。第二天,忠诚的A冲上了战场,却发现只有自己一支军队发起了进攻,而同样忠诚的B,却早已撤退。最终,A的军队败给了敌人,拜占庭的军队遭受损失。

    file

    拜占庭问题情形在计算机世界中也会出现,如果三个节点中有一个异常节点,那么最坏情况下两个正常节点之间是无法保证一致性的。那么你之前听说过的 etcd 这样的系统可以保证三个节点有一个宕机的情况下依然可以对外提供一致性服务是为什么呢?因为这类系统并没有考虑拜占庭故障,在他们的假设里故障节点可能会停止服务,可能会超时,但是不会发送异常消息。尽管拜占庭的“幽灵”很难处理,但在实际工作应用中,却并不需要过分去考虑它,因为对于大多数系统来说,内部环境里,硬件故障导致消息错误的概率本身就很低,还要按照拜占庭叛徒的策略来处理故障就更为困难了。

  3. Paxos角色

    Paxos将系统中的角色分为三种:

    Proposer:提议者,提出提案Proposal信息。

    Acceptor:决策者,参与决策,回应Proposers的提案。

    Learner:最终决策学习者,不参与决策。

    在具体的实现中,一个进程可能同时充当多种角色。比如一个进程可能既是Proposer又是Acceptor或Learner。Proposer负责提出提案,Acceptor负责对提案作出裁决(accept与否),Learner负责学习提案结果,Acceptor告诉Learner哪个value被选定,Learner则无条件认为相应的value被选定。

    为了避免单点故障,会有一个Acceptor集合群,Proposer向Acceptor集合群发送提案,Acceptor集合群中, 只有一半以上的成员同意了这个提案(Proposal),就认为该提案被接受选定了。

  4. Paxos算法详解

    Paxos包含三种算法: Basic Paxos,Multi Paxos和Fast Paxos。

    1. Basic Paxos

    Basic Paxos 执行过程分为两个阶段:

    • 阶段一(Prepare阶段):

      1. Proposer选择一个提案Proposal编号为N,然后向半数以上的Acceptor发送编号为N的Prepare请求。

      2. 如果某个Acceptor收到一个编号为N的Prepare请求,如果小于它已经响应过的请求,则拒绝

      3. 如果N大于该Acceptor已经响应过的所有请求的编号,那么它就会将它已经接受过(已经经过第二阶段accept的提案)的编号最大的提案作为响应反馈给Proposer,如果还没有的accept提案的话返回{pok,null,null}空信息,同时该Acceptor承诺不再接受任何编号小于N的提案。

    • 阶段二(accept阶段):

      1. 如果一个Proposer收到半数以上Acceptor对其发出的提案响应,那么它就会发送一个针对[N,V]提案的Accept请求给半数以上的Acceptor。注意:N是提案的编号,V就是该提案的决议,该决议是响应编号中最大提案的value。如果响应中不包含任何提案,那么V值就由Proposer自己决定。
      2. 如果Acceptor收到一个针对编号为N的提案的Accept请求,只要该Acceptor没有对编号大于N的Prepare请求做出过响应,那么它就接受该提案。如果N小于Acceptor以及响应的prepare请求,则拒绝,不回应或回复error(当proposer没有收到过半的回应,那么它会重新进入第一阶段,递增提案号,重新提出prepare请求)。

    整体流程:

    file

    上述介绍了Basic Paxos算法, 但在算法运行过程中,可能还会存在一种极端情况,当有两个proposer依次提出一系列编号递增的议案,那么会陷入死循环,无法完成第二阶段,也就是无法选定一个提案。由此产生了活锁问题,如何解决?再看下面的Multi Paxos算法。

    2. Multi Paxos

    原始的基础Paxos算法只能对一个值进行决议,而且每次决议至少需要两次网络来回,在实际应用中可能会产生各种各样的问题,所以不适用在实际工程中。因此,Multi Paxos基于Basix Paxos做了改进,可以连续确定多个值并提高效率:

    1. 针对每一个要提议的值,运行一次Paxos算法实例(Instance),形成决议。每一个Paxos实例使用唯一的Instance ID标识。
    2. 在所有Proposers中选主,选举一个Leader,由Leader唯一提交Proposal给Acceptors进行表决。这样没有Proposer竞争,解决了活锁问题。在系统中仅有一个Leader进行Value提交的情况下,Prepare阶段就可以跳过,从而将两阶段变为一阶段,提高效率。

    示例:

    file

    在Basic Paxos协议中,每一次执行过程都需要经历Prepare->Promise->Accept->Accepted 这四个步骤,这样就会导致消息太多,影响性能。

    在Multi Paxos的实际过程中:如果Leader足够稳定的话,Phase 1 里面的Prepare->Promise 完全可以省略掉,从而使用同一个Leader去发送Accept消息。

    实质上,Multi Paxos模式下首先对所有Proposers进行Leader选举,再利用Basic Paxos来实现

    选出Leader后,只由Leader来提交Proposal,如果Leader出现宕机,则重新选举Leader,在系统中仅有一个Leader可以提交Proposal。 Multi Paxos通过改变Prepare阶段的作用范围,从而使得Leader的连续提交只需要执行一次Prepare阶段,后续只需要执行Accept阶段,将两阶段变为一阶段,提高了效率。

3. Fast Paxos

上述Paxos协议中,消息最后到达Learner一般都要经历 Client-->Proposer-->Acceptor-->Learner 多个步骤,实质上由Learner是真正执行任务,为了更快的让消息到达Learner,如果Proposer本身没有数据需要被确认的话,那么可以跳过Proposer这一步,直接将请求发送给Accepter,由leader先进行检查, 这样的操作叫做Fast Paxos。

Fast Paxos, non-conflicting:

file

Fast Paxos, conflicting proposals:

在Fast Round中因为允许多个Proposer同时提交不同的Value到Acceptor,这将导致在Fast Round中没有任何value被作为最终决议,这也称为“冲突”:

file

  1. Leader检测到冲突之后,根据规定的算法从冲突中选择一个数据,重新发送Accept请求;
  2. 当检测到冲突的时候,如果Acceptors自己就能解决冲突,那么就完全不需要Leader再次发送Accept请求了,这样就又减少了一次请求,节省开销。
2. ZAB一致性协议解析
  • ZAB协议概述

    ZAB (Atomic Broadcast Protocol)协议是为分布式协调服务 ZooKeeper 专门设计的一种支持崩溃恢复的原子广播协议。基于该协议,ZooKeeper 实现了一种主备模式的系统架构来保持集群中各个副本之间数据一致性。

  • ZAB与PAXOS的联系与区别

    Paxos算法的目的在于设计分布式的一致性状态机系统。

    ZAB协议的设计目的在于分布式的高可用数据主备系统。

    ZAB借鉴了Paxos算法,做了相应的改进,ZAB协议除了遵从Paxos算法中的读阶段和写阶段,还有加入了同步阶段

  • ZAB协议两个过程:

    ZAB 协议主要包括:

    两个过程: 消息广播和崩溃恢复。

    三个阶段: 分为Discovery 发现,Synchronization 同步,Broadcast 广播三个阶段。

    消息广播过程

    Leader 节点接受事务提交,并且将新的 Proposal 请求广播给 Follower 节点,收集各个节点的反馈,决定是否进行 Commit。

    崩溃恢复过程

    如果在同步过程中出现 Leader 节点宕机,会进入崩溃恢复阶段,重新进行 Leader 选举,崩溃恢复阶段还包含数据同步操作,同步集群中最新的数据,保持集群的数据一致性。

    file

    源码参阅

    查看RequestProcessor的实现类:

    AckRequestProcessor -> processRequest()

  • ZAB专业术语

    • CEpoch:Follower 发送自己处理过的最后一个事务 Proposal 的 epoch 值。

    • NewEpoch:Leader 根据接收 Follower 的 epoch,来生成新一轮 epoch 值。

  • Ack-E:Follower 确认接收 Leader 的新 epoch。

  • NewLeader:确立领导地位,向其他 Follower 发送 NewLeader 消息。

    • Ack-LD:Follower 确认接收 Leader 的 NewLeader 消息。

    • Commit-LD:提交新 Leader 的 proposal。

    • Propose:Leader 开启一个新的事务。

    • Ack:Follower 确认接收 Leader 的 Proposal。

    • Commit:Leader 发送给 Follower,要求所有 Follower 提交事务 Proposal。

1)Discovery(发现阶段)

在这个阶段,followers 跟准 leader 进行通信,同步 followers 最近接收的事务提议。这个一阶段的主要目的是发现当前大多数节点接收的最新提议,并且准 leader 生成新的 epoch,让 followers 接受,更新它们的 acceptedEpoch。

file

处理过程:

  1. Follower 将自己最后处理的事务 Proposal 的 epoch 值发送给 Leader,消息 CEpoch(F.p)F.p 可以提取出 zxid。

  2. 当 Leader 接收到过半的 Follower 的 CEpoch 消息后,Leader 生成 NewEpoch(e') 发送给这些过半的 Follower,e' 是比任何从 CEpoch 消息中收到的 epoch 值都要大。

  3. Follower 一旦从 Leader 处收到 NewEpoch(e') 消息,会先做判断,如果 e'<F.acceptedEpoch,并且F.state = election 也就是Looking状态,那么会重新回到Leader选举阶段。

  4. Leader 一旦收到了过半的 Follower 的确认消息。它会从这些过半的 Follower 中选取一个 F,并使用它作为初始化事务的集合(用于同步集群数据),然后结束发现阶段。既然要选择需要同步的事务集合,必然要选择事务最全的,所以,须满足epoch 是最大的zxid 也是最大的条件。

源码参阅:

Follower -> followLeader() -> connectToLeader():比较选举轮次,newEpoch < self.getAcceptedEpoch():syncWithLeader()同步主节点的history历史信息

2)Synchronization(同步阶段)

同步阶段主要是利用 leader 前一阶段获得的最新提议历史信息,同步集群中所有的副本。只有当 quorum 都同步完成,准 leader 才会成为真正的 leader。follower 只会接收 zxid 比自己的 lastZxid 大的提议

file

在处理完上面发现阶段之后(即确定了数据源 Follower F 的事务集合 S'),接下来就进入了同步阶段处理流程:

  1. 第一个过程 NewLeader:Leader 将新 epoch 和 S'NewLeader(e', S')的消息形式发送给所有过半 (Quorum) 的 Follower。在上一阶段 L.history = F.history,所以 S' 就是流程图中的 L.history
  2. 第二过程ACK:当 Follower 接收到 NewLeader(e', S') 消息后,做相应判断:
    1. 如果 Follower 的 epoch 等于 e',也就是确认是不是该Follower的信息,因为前一阶段已经存储了最新的e'。Follower 将会执行事务应用操作,将接收 S' 中的所有事务 Proposal,只是接收不作其他处理。
    2. 如果 Follower 的 epoch 不等于 e',即不是这一轮的 Follower信息,直接回退至选举阶段
    3. Leader 在接收到过半的 Follower 的 Ack 消息后,发送 Commit 消息至所有的 Follower进行同步,之后进入下一阶段即 Broadcast(消息广播)。
  3. 第三个过程 Commit:在收到 Leader 的 Commit 消息后,按顺序依次调用 abdeliver() 处理 S' 的每一个事务,随后结束这一阶段的处理

参阅源码:

Learner -> syncWithLeader() ->writePacket(ack, true); 发送ACK响应消息

Leader -> processAck() -> lastCommitted >= zxid 事务已经被提交

->commit(zxid); 提交事务ID

3)Broadcast(广播阶段)

进入到广播阶段,Zookeeper 集群才能正式对外提供事务服务,Leader 负责写请求(如果有写请求落到 Follower 上,会转发给 Leader),并且 leader 可以进行消息广播。如果有新的节点加入,还需要对新节点进行同步

file

处理流程:

第一个过程 Propose:Leader 收到来自客户端新的事务请求后,会生成对应的事务 Proposal,并根据 zxid 的顺序(递增)向追随自己的所有 Follower 发送 P<e', <v, z>>,其中 epoch(z) == e'

第二个过程 Ack:Follower 根据收到消息的次序来处理这些 Proposal,并追加到 H 中去,然后通知给 Leader。

第三个过程 Commit:一旦 Follower 收到来自 Leader 的 Commit消息,将调用 abdeliver() 提交事务 。这里是按照zxid的顺序来提交事务。

广播请求处理流程:

file

  1. Leader(主)服务器接收 Client(客户端) 的请求,为其生成 Proposal(提案)。

  2. 然后将 Proposal 发送给所有 Follower (从)。主会为每一个从分配一个单独的队列,以保证消息的有序性。

  3. 主节点等待所有从服务器反馈 Ack,当有过半的从服务器 Ack 之后,主节点会提交本地事务。然后广播 Commit 给所有从,从节点接收到 Commit 之后完成提交。

参阅源码:

Leader -> self.getQuorumVerifier().containsQuorum(p.ackSet)

QuorumHierarchical -> containsQuorum(); // 过半响应的判断处理

3. 选主过程剖析

FastLeaderElection其实是标准的Fast Paxos的实现,它首先向所有Server提议自己要成为leader,当其它Server收到提议以后,根据规则(zxid与myid 的比较判断),选取合适的主节点。

FastLeaderElection算法通过异步的通信方式来收集其它节点的选票,同时在分析选票时又根据投票者的状态来作不同的处理,从而加快Leader的选举过程

  1. 第一步(初始化投票)

    file

    初始化投票,所有服务器的logicClock都为1,zxid都为0, 每个节点都会投票给自己,投票信息(1,1,0)主要分为三部分:

    第一位数代表服务器的logicalClock(自增的整数),即选举轮次,它表示这是该服务器发起的第多少轮投票;

    第二位数代表被推荐的服务器的myid,它是ZooKeeper集群唯一的ID

    第三位数代表被推荐服务器的最大zxid,类似于RDBMS中的事务ID,实质上用于标识一次更新操作的事务Proposal(提议)ID

    ZXID组成:

    zxid(Zookeeper Transaction Id)是 ZAB 协议的事务编号,其是一个 64 位的整数:

    file

    低 32 位counter是一个单调递增的计数器,每当 Leader 服务器产生一个新的事务 Proposal 时,递增 1。

    高 32 位代表 Leader 的 epoch 编号,有点类似于 Raft 的任期(改朝换代),每当选举一个新 Leader 时,就会从新 Leader 取出本地日志中的最大事务 Proposal 的 zxid,解析出 epoch 然后加 1,并将低 32 位重置 0 来开始新的 zxid。

  2. 第二步更新投票信息

    file

    1. 服务器1收到上一流程图中, 服务器2的选票(1, 2, 0)和服务器3的选票(1, 3, 0)后,由于所有的logicClock都相等,所有的zxid都相等,因此根据myid判断应该将自己的选票按照服务器3的选票更新为(1, 3, 0)。投票规则:

      • 优先检查ZXID。ZXID比较大的服务器优先作为Leader。
      • 如果ZXID相同,那么就比较myid。myid较大的服务器作为Leader服务器。
    2. 服务器1将自己的票箱全部清空,再将服务器3的选票与自己的选票存入自己的票箱,接着将自己更新后的选票信息广播出去。此时服务器1票箱内的选票为1号选举3号(1, 3),3号选举3号(3, 3)

    3. 服务器2收到服务器3的选票后,与服务器1的处理逻辑一样, 也将自己的选票更新为(1, 3)并存入票箱然后广播。此时服务器2票箱内的选票为(2, 3),(3, 3)

    4. 服务器3根据上述规则,无须更新选票,自身的票箱内选票仍为(3, 3)

    5. 服务器1与服务器2更新后的选票广播出去后,由于三个服务器最新选票都相同,最后三者的票箱内都包含三张投给服务器3的选票。

    源码分析:

    FastLeaderElection-> lookForLeader() -> case LOOKING: -> totalOrderPredicate()

    代码片段:

    if (n.electionEpoch > logicalclock) { // 其选举周期大于逻辑时钟
      // 更新选举轮次
      logicalclock = n.electionEpoch;
      // 清空之前所有接收到的选票信息
      recvset.clear();
      if(totalOrderPredicate(n.leader, n.zxid, n.peerEpoch,
              getInitId(), getInitLastLoggedZxid(), getPeerEpoch())) { // 当前节点与接收的选票信息进行PK
          // PK失败, 更新所接收的选票
          updateProposal(n.leader, n.zxid, n.peerEpoch);
      } else { 
          // PK成功,更新选票为自身
          updateProposal(getInitId(),
                  getInitLastLoggedZxid(),
                  getPeerEpoch());
      }
      // 将更新的选票信息通知发送给所有节点
      sendNotifications();
    } else if (n.electionEpoch < logicalclock) { // 选举周期小于逻辑时钟,不做处理,直接忽略
      if(LOG.isDebugEnabled()){
          LOG.debug("Notification election epoch is smaller than logicalclock. n.electionEpoch = 0x"
                        + Long.toHexString(n.electionEpoch)
                        + ", logicalclock=0x" + Long.toHexString(logicalclock));
            }
            break;
        } else if (totalOrderPredicate(n.leader, n.zxid, n.peerEpoch,
                proposedLeader, proposedZxid, proposedEpoch)) { // 当前节点与接收的选票信息进行PK
            // 更新选票
            updateProposal(n.leader, n.zxid, n.peerEpoch);
            // 发送消息
            sendNotifications();
        }
  3. 第三步确定集群角色

    file

    根据上述选票结果,三个服务器一致认为此时服务器3应该是Leader。因此服务器1和2都进入FOLLOWING状态,而服务器3进入LEADING状态。之后由Leader发起并维护与Follower间的心跳。

    参考源码:

    1. QuorumPeer类 -> case LEADING: -> leader.lead(); -> f.ping(); 发起ping心跳
    2. LearnerHandler -> run方法内 -> case Leader.PING, 569行, 维护心跳连接
4. FOLLOW重启选举剖析

如果FOLLOW跟随者节点出现问题重启之后, 是如何重新选举呢?

第一步:

file

由于某些原因,比如宕机、重启、或者发生网络等问题,导致Follower节点找不到Leader主节点的情况,就会进入LOOKING状态并发起新的一轮投票

第二步:

file

服务器3收到服务器1的投票后,将自己的状态LEADING以及选票返回给服务器1。

服务器2收到服务器1的投票后,将自己的状态FOLLOWING及3号主节点的选票信息返回给服务器1。

服务器1知道服务器3是Leader,并且通过服务器2与服务器3的选票可以证实服务器3确实得到了超过半数的选票。因此服务器1进入FOLLOWING状态。

参考源码:

FastLeaderElection -> case FOLLOWING: ->ooePredicate主节点检查。 ->更新选举信息, 清空票箱。

5. LEADER重启选举剖析

实际情形, 服务器可能会由于硬件、网络等原因导致服务器出现不稳定情况, Leader如果重启之后该如何选举?

第一步:

file

Leader主节点(服务器3)宕机后,Follower(服务器1和2)发现Leader不工作了,因此进入LOOKING状态并发起新的一轮投票,并且都将投票选举为自己。

第二步:

file

服务器1和2根据外部投票确定是否要更新自身的选票。这里就会出现两种情况:

  1. 服务器1和2的zxid相同。例如在服务器3宕机前服务器1与2完全同步,zxid一致。此时选票的更新主要取决于myid的大小,根据选举规则,myid越大的优先级越高

  2. 服务器1和2的zxid不同。在旧Leader宕机之前,其所主导的写操作,只需过半服务器确认即可,而不需所有服务器确认。这个时候,服务器1和2可能其中有一个与旧Leader同步(即zxid与之相同),而另一个则没有同步(即zxid比之小)。这时选票的更新主要取决于谁的zxid较大,优先选取为主节点

在第一步的图中所示,服务器1的zxid为11,而服务器2的zxid为10,因此服务器2将自身选票更新为(3, 1, 11)。

第三步:

file

经过第二步的选票更新后,服务器1与服务器2均将选票投给服务器1,因此服务器2成为Follower,而服务器1成为新的Leader并维护与服务器2的心跳。

第四步:

file

旧的Leader节点3恢复后进入LOOKING状态并发起新一轮领导选举,并将选票投给自己。此时服务器1会将自己的LEADING状态及选票(3, 1, 11)返回给服务器3,而服务器2将自己的FOLLOWING状态及选票(3, 1, 11)返回给服务器3, 旧的Leader节点3已经被新的Leader节点1取代, 这就类似于上面所讲的FOLLOW重启选举的过程。也就是源码FastLeaderElection中, 为什么case FOLLOWING和case LEADING, 处理流程是一致。


本文由mirson创作分享,如需进一步交流,请加QQ群:19310171或访问www.softart.cn

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

麦神-mirson

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值