Zab系列3 选举

Zab系列博客

Raft Vs Zab
https://www.jianshu.com/p/24307e7ca9da
Zab系列1 核心概念
https://www.jianshu.com/p/76e5dba31ea4
Zab系列2 角色和存储
https://www.jianshu.com/p/d80f9250ffd1
Zab系列3 选举
https://www.jianshu.com/p/0d2390c242f6
Zab系列4 zookeeper特性
https://www.jianshu.com/p/08b62ca1fe4e
Zab系列5 选举恢复(源码分析)
https://www.jianshu.com/p/b6acd99921b7
Zab系列6 zk单机版工作原理
https://www.jianshu.com/p/ed45982b18b4
Zab系列7 集群工作原理Leader篇
https://www.jianshu.com/p/59240c36ba1b
Zab系列8 集群工作原理Follower篇
https://www.jianshu.com/p/8d7c7f1b2838
Zab系列9 消息顺序性
https://www.jianshu.com/p/0aa96b6a2070

申明

  1. 必须要看懂Zab系列1 基本概念才好理解这篇,因为内容都是环环相扣的!
  2. 最好结合Zab系列3 Raft Vs Zab一起看

核心算法 FastLeaderElection.lookForLeader()

只要Node进入Looking状态,就会触发这个方法

  1. 触发条件:
  • 节点刚刚启动,会进入Looking状态
  • follower监测超时,会进入Looking状态
  • Leader未收到过半节点的心跳恢复,会进入Looking状态
  1. 结束该循环的3种情况(2种大类)
  • 要么recvset中半数节点的投票给A(可以是following的投票,也可以是leading的投票累加),才会认可A为自己的leader,退出循环

  • 要么outofelection中半数节点都认可A为leader,自己才会认可A为自己的leader,退出循环

核心参数:

  • 这些参数初始化的值都是自己
  • 所有的参数值都是递增的,会更新成最新最大的那个值
  • 会随着接收到更新数据的投票信息,来动态更新这些参数值,如果有更新就广播出去
  • 如果没有更新的值,就不处理
    QuorumPeer self;
    Messenger messenger;
    AtomicLong logicalclock = new AtomicLong();  //数据最新的leader electionEpoch
    long proposedLeader; //数据最新的leader myid
    long proposedZxid;   //数据最新的leader lastCommitZxid
    long proposedEpoch;  //数据最新的leader peerEpoch

重点注意:proposedZxid 不等于lastCommitZxid,proposedZxid>=lastCommitZxid。lastCommitZxid只是初始值,后面如果收到了更新的zxid,会更新的。
更新参数信息

    //先比较,如果投票信息里面的数据更新,就更新自己的参数,再广播出去
    if(totalOrderPredicate(n.leader, n.zxid, n.peerEpoch,getInitId(), getInitLastLoggedZxid(), getPeerEpoch())) {
                                    updateProposal(n.leader, n.zxid, n.peerEpoch);
    } 
    sendNotifications();


    synchronized void updateProposal(long leader, long zxid, long epoch){
        proposedLeader = leader;
        proposedZxid = zxid;
        proposedEpoch = epoch;
    }
  1. 核心变量:recvset、voteSet、outofelection
  • recvset 归档set,key是vote发起方的ID,value是该发起方的最新的Vote值。包括state为Following的消息,也包括Leading的消息
 Map recvset = new HashMap();
recvset.put(n.sid, new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch));
  • voteSet,根据recvset推算出来的,最适合当leader的节点数据,如果voteSet.hasAllQuorums()返回true,则代表满足了产生新leader的条件
  • outofelection 选主成功消息,如果半数节点都选某节点为leader,那么即使该节点的数据更新,也没办法,必须服从成follower,一般这种情况的概率很小。

退出选举的3种情况

  1. Notification.state=following
    如果NodeA收到一个新的“正在投票未有新leader”消息,并且新得主的选举票数超过半数,且200ms内仍然没有更适合的leader产生的话,退出选举。新leader可能是自己,也可能是别人。
 switch (n.state) {
    case LOOKING:
            比较数据
            ...
        
            if (voteSet.hasAllQuorums()) {
            while((n = recvqueue.poll(finalizeWait,TimeUnit.MILLISECONDS)) != null){
                if(totalOrderPredicate(n.leader, n.zxid, n.peerEpoch,proposedLeader, proposedZxid, proposedEpoch)){
                    recvqueue.put(n);
                    break;
                }
            }

            if (n == null) {
                setPeerState(proposedLeader, voteSet);
                Vote endVote = new Vote(proposedLeader,proposedZxid, logicalclock.get(), proposedEpoch);
                leaveInstance(endVote);
                return endVote;
            }
        }
}
  1. Notification.state=leading,这种情况下只会比较只会比较electionEpoch而不比较Zxid,也就是说,可能数据更新myid更大的Node,因为收到了半数以上的其它节点的投票认同leader的消息,也不得不委曲求全,接收比自己差的Node为新leader

2.1 recvset投票过半,如果NodeA收到一个新的"投票结束,new leader有了"消息,并且新得主的选举票数超过半数
这是一种非常灵活的方法,因为当LEADING的消息时,

 switch (n.state) {
    case LEADING:
         if(n.electionEpoch == logicalclock.get()){
                recvset.put(n.sid, new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch));
                voteSet = getVoteTracker(recvset, new Vote(n.version, n.leader, n.zxid, n.electionEpoch, n.peerEpoch, n.state));
                if (voteSet.hasAllQuorums() && checkLeader(outofelection, n.leader, n.electionEpoch)) {
                    setPeerState(n.leader, voteSet);
                    Vote endVote = new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch);
                    leaveInstance(endVote);
                    return endVote;
                }
           }
}

2.2 Notification.state=leading,且outofelection投票过半,一般这种情况发生的概率不大,因为recvset里面包含outofelection的所有信息,所以recvset里面更容易产生leader。因为网络原因,可能某个结点错过了广播中的voting前期的投票消息,但是接收到了超过半数的节点都选举结束的消息时,也可以退出选举,进入follower状态

    outofelection.put(n.sid, new Vote(n.version, n.leader,n.zxid, n.electionEpoch, n.peerEpoch, n.state));
    voteSet = getVoteTracker(outofelection, new Vote(n.version, n.leader, n.zxid, n.electionEpoch, n.peerEpoch, n.state));

    if (voteSet.hasAllQuorums() &&checkLeader(outofelection, n.leader, n.electionEpoch)) {
        synchronized(this){
            logicalclock.set(n.electionEpoch);
            setPeerState(n.leader, voteSet);
        }
        Vote endVote = new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch);
        leaveInstance(endVote);
        return endVote;
    }

选主的逻辑

  1. 在选举初期,大家都在根据广播里面的Voting Notification更新自己的recvset,这个时候,都是PK 数据和myid的,谁更有资格当老大。
    如果自己接收到有比自己当前存储的数据更新的数据,就更新自己的数据,并且广播出来。这样Zxid和myid越大,被广播的次数越多,越有机会成为leader。大家每次都会统计投票,看是否有过半节点的投票同一节点当leader的情况产生。

  2. 在选举中后期,部分节点率先完成leader选举,就会把自己的选主信息广播出来,未完成选主的节点就不仅仅比较竞选投票,也要比较选主投票,只要半数节点都认同了一个Node当leader,那么即使新leader的数据比自己旧,那么也不得不低头。

选举的过程(有一个广播的功能)

  1. serverA首先将electionEpoch自增,然后为自己投票
  2. 从内存树和日志中获取到 lastProcessedZxid
  3. 向所有节点发送投票消息:electionEpoch、lastProcessedZxid、proposedLeader(myid)
    logicalclock.incrementAndGet();
    //getInitId:myid
    updateProposal(getInitId(), getInitLastLoggedZxid(), getPeerEpoch());
    sendNotifications();
  1. 循环监听,直到PK完之后,如果本机器投票被pk掉,则更新投票信息为对方投票信息,同时重新发送该投票信息给所有的server。
while ((self.getPeerState() == ServerState.LOOKING) &&(!stop)){
                Notification n = recvqueue.poll(notTimeout,TimeUnit.MILLISECONDS);
}

得到选票的条件:

  • 如果serverB收到的通知中的electionEpoch比自己的大,则serverB更新自己的electionEpoch为serverA的electionEpoch
  • 如果该serverB收到的通知中的electionEpoch比自己的小,则serverB向serverA发送一个通知,将serverB自己的投票以及electionEpoch发送给serverA,serverA收到后就会更新自己的electionEpoch
  • 如果electionEpoch相等,则继续PK Zxid,仍然相等则PK myid

解析:
electionEpoch:谁先发现leader异常,谁先自增electionEpoch
Zixd:在投票期间,指的就是lastProcessedZxid,也就是谁的数据更新

  1. 正常情况下,第一个发现Leader挂了的Node会自增electionEpoch,而此时B节点还没发觉到leader过期,那么A的electionEpoch就会比B大,那么理所当然B应该接受投票
  2. 如果leader挂了,A、B节点几乎同时发现了这个事情,都自增electionEpoch,都发起投票,那么此时A想要得到B的投票,就要PK zxid,要让数据最新的节点获胜,这样才能保证已经confirm了的数据不会丢失
  3. 如果A、B几乎同时发现了leader挂了,而且数据都一样新,那么就PK myid,这是为了避免投票失败而设置的,因为这个设置,每轮投票,肯定有一个Node当选。
  4. 如果ServerA的electionEpoch比B的要小,说明A节点的数据已经是过期的数据,则B会通知A要更新数据了

PK数据的过程

比较哪个节点更适合当leader

  1. 如果接收到的约票的任期大于自己的(比如S1节点首先发现Leader挂了,自增了任期,发给S2,因为S2还是之前的任期,所以 Epoch1>Epoch2)
    但是Epoch比自己大的约票不一定数据比自己新,可能S1和leader的网络分区了,导致S1的数据不全,且第一个超时而发起投票,所以还需要比较数据是否比自己新

那么节点会:

  • 更新自己的任期为最新任期
  • 清空recvset,因为那些投票都是前一任期的投票,保证了每个recvset里面不会存储不同任期的投票
  • 比较数据totalOrderPredicate(),挨个比较epoch、Zxid、myid
  • 发送广播投票结果(因为至少自己的logicalclock发生了变更)
  1. 如果接收到的约票的任期小于自己的,说明发起投票人的数据过期了,直接忽略,不做处理

  2. 如果接收到的约票的任期等于自己的,而且Myid还比自己的大,则发送一个更新投票的广播,会覆盖掉之前

// If notification > current, replace and send messages out
if (n.electionEpoch > logicalclock.get()) {
    logicalclock.set(n.electionEpoch);
    recvset.clear();
    if(totalOrderPredicate(n.leader, n.zxid, n.peerEpoch,
            getInitId(), getInitLastLoggedZxid(), getPeerEpoch())) {
        updateProposal(n.leader, n.zxid, n.peerEpoch);
    } else {
        updateProposal(getInitId(),
                getInitLastLoggedZxid(),
                getPeerEpoch());
    }
    sendNotifications();
} else if (n.electionEpoch < logicalclock.get()) {
    if(LOG.isDebugEnabled()){
        LOG.debug("Notification election epoch is smaller than logicalclock. n.electionEpoch = 0x"
                + Long.toHexString(n.electionEpoch)
                + ", logicalclock=0x" + Long.toHexString(logicalclock.get()));
    }
    break;
} else if (totalOrderPredicate(n.leader, n.zxid, n.peerEpoch,proposedLeader, proposedZxid, proposedEpoch)) {
    updateProposal(n.leader, n.zxid, n.peerEpoch);
    sendNotifications();
}

算法细节

  1. 通过FastLeaderElection类本身的logicalclock、proposedLeader、proposedZxid来记录当前节点认为数据最新或者myid最大的节点的信息

  2. 通过recvset这个Set来记录所有的来自其它节点的投票或者是投票结果广播消息,的最新值。也就是某个sid的节点认为的最适合当leader的信息值

  3. 不是PK following选票过半就能成为最终的leader,选举结束后,会等待200ms,看还有没有变更的,没有则宣布选举结束。
    有新的消息话,会先pk一下,如果最新的消息没有这个新,则put到recvqueue中,继续等待更新的投票信息
    如果有更适合当leader的数据产生,会开始一个新的循环。
    这种机制促使让选主变的更严格,系统促使数据最新且myid最大的Node成为leader

 while((n = recvqueue.poll(finalizeWait,TimeUnit.MILLISECONDS)) != null){
            if(totalOrderPredicate(n.leader, n.zxid, n.peerEpoch,proposedLeader, proposedZxid, proposedEpoch)){
                recvqueue.put(n);
                break;
            }
        }
   if (n == null) {
                setPeerState(proposedLeader, voteSet);
                Vote endVote = new Vote(proposedLeader,proposedZxid, logicalclock.get(), proposedEpoch);
                leaveInstance(endVote);
                return endVote;
    }

参考

ZooKeeper的一致性算法赏析
https://my.oschina.net/pingpangkuangmo/blog/778927?spm=a2c4e.11153940.blogcont62901.11.12c62ee5vwzG6o

辅助
https://www.jianshu.com/p/357ca7c3b2af

zookeeper官网说明
https://zookeeper.apache.org/doc/r3.5.4-beta/zookeeperOver.html

zookeeper官网说明中文翻译版
https://blog.csdn.net/lisuo1234/article/details/55826380

大白话,真的好
https://blog.csdn.net/gaoshan12345678910/article/details/67638657

你可能感兴趣的:(Zab系列3 选举)