Zookeeper源码分析之启动流程及选举过程讲解

目录

1.zk的冷启动数据恢复

冷启动数据恢复流程

2.准备选举

3.选票的传递过程

WorkerSender 和 SendWorker

WorkerReceiver 和 RecvWorker

4.选举

选举流程总结


在zk启动的过程中,主要概括做了4件事。

  1.                  冷启动数据恢复。
  2.                  服务端的通信组件启动
  3.                  准备选举
  4.                  执行选举

接下来我们针对这4件事看下对应的源码:

# 入口方法
QuorumPeerMain.main();
		# 核心实现,分三步走
		QuorumPeerMain.initializeAndRun(args);
				# 第一步;解析配置
				config = new QuorumPeerConfig();
				config.parse(args[0]);
					Properties cfg = new Properties();
					cfg.load(in);
					parseProperties(cfg);
					
				# 第二步:启动一个线程(定时任务)来执行关于old snapshot的clean
				new DatadirCleanupManager(...).start()
					timer = new Timer("PurgeTask", true);
				    TimerTask task = new PurgeTask(dataLogDir, snapDir,snapRetainCount);
				    timer.scheduleAtFixedRate(task, 0,TimeUnit.HOURS.toMillis(purgeInterval));
						PurgeTxnLog.purge(new File(logsDir), new File(snapsDir), ...);
						
				# 第三步:启动(有两种模式:standalone,集群模式)重点关注集群启动,分两步走
				runFromConfig(config);
					# 服务端的通信组件 的初始化,但是并未启动
					factory = ServerCnxnFactory.createFactory();
					cnxnFactory.configure(....)
					# 抽象一个zookeeper节点,然后把解析出来的各种参数给配置上,然后启动
					quorumPeer = getQuorumPeer(); + quorumPeer.setXXX() + quorumPeer.start();
							# 第一件事:把磁盘数据恢复到内存
							loadDataBase();
								zkDb.loadDataBase();
									# 冷启动的时候,从磁盘恢复数据到内存
									zoo.cfg 中的内容:
									snapLog.restore(...., ....)
										# 从快照恢复
										snapLog.deserialize(dt, sessions);
											# 从操作日志恢复
											fastForwardFromEdits(dt, sessions, listener);
									  			# 恢复执行一条事务
												rocessTransaction(hdr, dt, sessions,itr.getTxn());
												
							# 第二件事:服务端的通信组件的真正启动
							cnxnFactory.start();
                                    thread.start();
                                        factory.run()
							                #第一件事: 处理连接请求
                                            #第二件事: 处理读写请求
							# 第三件事:准备选举的一些必要操作(初始化一些队列和一些线程)
							startLeaderElection();
							
							# 第四件事:调用 start() 跳转到 run() 方法。因为 QuorumPeer被封装成Thread 了
							super.start();
							# 执行选举
							QuorumPeer.run()

 


 

1.zk的冷启动数据恢复

# 冷启动的时候,从快照和事务日志中恢复数据到内存
   snapLog.restore(dataTree,...)
            # 1.从快照恢复
            snapLog.deserialize(dt, sessions);
            #获取最近的100个快照文件,并倒序排序,得到最新的有效的snapshot文件,进行反序列化到内存
            List<File> snapList = findNValidSnapshots(100);
            //执行恢复快照数据到内存
            deserialize(dt, sessions, ia);

            #2.snapshot最新的lastProcessedZxid之前的事务操作都序列化到snapshot中,之后的操作只有事务日志,所以之后的操作需要从事务日志中进行恢复.
            fastForwardFromEdits(dt, sessions, listener);

冷启动数据恢复流程

1.快照恢复: 获取最近100个快照文件,并倒序排序,获取最新的有效snapshot文件,进行反序列化到内存。

2.日志恢复: 通过最新有效的snapshot获取到最新的lastProcessedZxid,在最新的zxid之前的数据通过snapshot进行恢复,在zxid之后的操作只有事务日志,通过事务日志将数据恢复到内存。

 


2.准备选举

1.准备选票,初始化Vote选票对象。封装了myid,zxid,epoch

2.初始化选举算法

#选举 
synchronized public void startLeaderElection() {
        try {

            /*************************************************
             *  注释: 第一件事: 准备选票 ,初始化 Vote选票对象
             */
            currentVote = new Vote(myid, getLastLoggedZxid(), getCurrentEpoch());
        } catch(IOException e) {
            RuntimeException re = new RuntimeException(e.getMessage());
            re.setStackTrace(e.getStackTrace());
            throw re;
        }
        for(QuorumServer p : getView().values()) {
            if(p.id == myid) {
                myQuorumAddr = p.addr;
                break;
            }
        }
        if(myQuorumAddr == null) {
            throw new RuntimeException("My id " + myid + " not in the peer list");
        }
        if(electionType == 0) {
            try {
                udpSocket = new DatagramSocket(myQuorumAddr.getPort());
                responder = new ResponderThread();
                responder.start();
            } catch(SocketException e) {
                throw new RuntimeException(e);
            }
        }

        /*************************************************
         *  注释: 第二件事: 初始化选举算法
 
         */
        this.electionAlg = createElectionAlgorithm(electionType);
    }

接着进入选举方法中createElectionAlgorithm(0,进行初始化选举算法

#初始化选举算法,默认是第三种
protected Election createElectionAlgorithm(int electionAlgorithm) {
        Election le = null;

        //TODO: use a factory rather than a switch
        switch(electionAlgorithm) {
            case 0:

                // 注释: 选举算法使用的网络通信技术: UDP BIO
                le = new LeaderElection(this);
                break;
            case 1:
                le = new AuthFastLeaderElection(this);
                break;
            case 2:
                // 注释: ServerSocket TCP
                le = new AuthFastLeaderElection(this, true);
                break;


            /*************************************************
             *  注释: 选举算法的初始化
             *  总共做了三件非常重要的事情:
             *  1、初始化选举过程中,负责一切网络通信的一个 管理组件: QuorumCnxnManager
             *     在初始化  QuorumCnxnManager 的过程中,还会初始化一个 Listener
             *     这个 Listener 就可以理解成是 选举过程中,负责网络通信的服务端
             *  2、启动 Listener
             *  3、创建选举算法实例
             */
            case 3:

                qcm = createCnxnManager();
                QuorumCnxManager.Listener listener = qcm.listener;
                if(listener != null) {

                    listener.start();

                    le = new FastLeaderElection(this, qcm);
                } else {
                    LOG.error("Null listener when initializing cnx manager");
                }
                break;
            default:
                assert false;
        }
        return le;
    }

 createCnxnManager(0方法进行了选举的准备工作

 public QuorumCnxManager(final long mySid, Map<Long, QuorumPeer.QuorumServer> view, QuorumAuthServer authServer, QuorumAuthLearner authLearner,
            int socketTimeout, boolean listenOnAllIPs, int quorumCnxnThreadsSize, boolean quorumSaslAuthEnabled,
            ConcurrentHashMap<Long, SendWorker> senderWorkerMap) {

        // 注释:ConcurrentHashMap<Long, SendWorker>  senderWorkerMap
        // 注释: key: serverid myid sid
        // 注释: value: SendWorker
        // 注释: 专门负责给对应的 serverid 发送选票的线程
        this.senderWorkerMap = senderWorkerMap;

        // 注释: 既然有一个线程专门负责一个对应serverid的选票的发送
        // 注释: 也有一个线程专门负责一个对应serverid的投票的接收

        // 注释: 接收投票的队列, 负责接收投票的线程 ReicWorker 接收投票之后,放置在 recvQueue 队列里面
        this.recvQueue = new ArrayBlockingQueue<Message>(RECV_CAPACITY);

        // 注释: key: serverid myid sid
        // 注释: value:  ArrayBlockingQueue<ByteBuffer>  只用来存储发送给当前这台服务器的选票
        this.queueSendMap = new ConcurrentHashMap<Long, ArrayBlockingQueue<ByteBuffer>>();

        // 注释: key: serverid myid sid
        // 注释: value: ByteBuffer 存储是的我这个节点给其他所有服务器节点发送的最近的一次选票
        this.lastMessageSent = new ConcurrentHashMap<Long, ByteBuffer>();

        String cnxToValue = System.getProperty("zookeeper.cnxTimeout");
        if(cnxToValue != null) {
            this.cnxTO = Integer.parseInt(cnxToValue);
        }
        this.mySid = mySid;
        this.socketTimeout = socketTimeout;
        this.view = view;
        this.listenOnAllIPs = listenOnAllIPs;

        initializeAuth(mySid, authServer, authLearner, quorumCnxnThreadsSize, quorumSaslAuthEnabled);

        /*************************************************
         *  注释: 仅仅只是创建了一个 Listener 实例对象
         *  当调用 listener.start()  初始化了一个在选举构成中用来通信的服务端
         */
        // Starts listener thread that waits for connection requests 
        listener = new Listener();
    }

listener线程启动,其实是专门开启了一个线程,指定了一个选举端口,开启选举服务端,等待其他zk节点发送连接请求过来建立连接,进行选票

3.选票的传递过程

接下来我们看下选票是如何传递的

 

准备好选票后,所有的选票发送前都会放到sendQueue容器中,所有接收地投票的数据都会放到recvqueue容器中

#初始化选举需要的队列
1.
public FastLeaderElection(QuorumPeer self, QuorumCnxManager manager) {
        this.stop = false;
        this.manager = manager;

         //启动
        starter(self, manager);
    }


2.
private void starter(QuorumPeer self, QuorumCnxManager manager) {
        this.self = self;
        proposedLeader = -1;
        proposedZxid = -1;

        /*************************************************
         *  注释: 现在一起的发送选票都先放在 sendqueue 中, 然后经过处理之后,在放到: 对应的 serverid 对应的发送队列中
         */
        sendqueue = new LinkedBlockingQueue<ToSend>();

        /*************************************************
         *  注释: 统一的接收投票的容器
         */
        recvqueue = new LinkedBlockingQueue<Notification>();

        /*************************************************
         *  注释: 初始化两个线程
         */
        this.messenger = new Messenger(manager);
    }



3.
#在Messenger中,会初始化统一发送和接受线程
 Messenger(QuorumCnxManager manager) {

            /*************************************************
             *  注释: WorkerSender   处理选票  从sendqueue中获取选票进行发送
             */
            this.ws = new WorkerSender(manager);
            Thread t = new Thread(this.ws, "WorkerSender[myid=" + self.getId() + "]");
            t.setDaemon(true);
            t.start();


            /*************************************************
             *  注释: WorkerReceiver  处理投票 从recvqueue容器中接收投票进行处理
             */
            this.wr = new WorkerReceiver(manager);
            t = new Thread(this.wr, "WorkerReceiver[myid=" + self.getId() + "]");
            t.setDaemon(true);
            t.start();
        }

WorkerSender 和 SendWorker

所以,WorkerSender是统一的,负责从sendQueue队列中取出待发送的选票消息,放置到queueSendMap中,交给下层的连接管理类QuorumCnxManager进行发送。可能是发给自己,可能是发给别人。会针对sid,有 QuorumCnxManager.senderWorkerMap,会针对每个sid有对应专门的发送线程senderWorker。进行消息的发送。

WorkerSender的职责:

public void run() {
                while (true) {
                    try {
                        ToSend m = sendqueue.take();
                        // 注释: 交由QuorumCnxManager 的 SendWorker 来处理消息的发送
                        process(m);
                    } catch (InterruptedException e) {
                        break;
                    }

                }
            }



#process逻辑

 void process(ToSend m) {

                //  注释: 把 ToSend 变成  ByteBuffer
                ByteBuffer requestBuffer = buildMsg(m.state.ordinal(), m.leader, m.zxid, m.electionEpoch, m.peerEpoch);

                //  注释: 发送    将消息放置到queueSendMap,等待SendWorker处理
                manager.toSend(m.sid, requestBuffer);
            }
        }

#toSend逻辑
  /**
     * Processes invoke this message to queue a message to send. Currently, 
     * only leader election uses it.
     */
    public void toSend(Long sid, ByteBuffer b) {
        /*
         * If sending message to myself, then simply enqueue it (loopback).
         */

        //  注释: 如果发送给自己
        if(this.mySid == sid) {
            b.position(0);

            //  注释: 最开始是: ToSend, 然后是 ByteBuffer, 现在是 Message
            addToRecvQueue(new Message(b.duplicate(), sid));
            /*
             * Otherwise send to the corresponding thread to send.
             */

            //  注释: 发送给别人
        } else {
            /*
             * Start a new connection if doesn't have one already.
             */
            ArrayBlockingQueue<ByteBuffer> bq = new ArrayBlockingQueue<ByteBuffer>(SEND_CAPACITY);
            ArrayBlockingQueue<ByteBuffer> bqExisting = queueSendMap.putIfAbsent(sid, bq);
            if(bqExisting != null) {

                //  注释: 把 ByteBuffer(选票) 放入到 发送队列(bq)
                addToSendQueue(bqExisting, b);
            } else {
                addToSendQueue(bq, b);
            }

            //  联系对方服务器,建立连接!然后执行选票的发送
            /*************************************************
             *  注释: 最重要的两个细节
             *  1、当发送请求连接给对方服务器的时候,我自己会初始化一对线程(SendWorker + RecvWorker)
             *     对方在处理链接的时候,也会初始化这一对线程
             *  -
             *  2、只允许 myid 大的节点发送请求给 myid 小的建立连接!
             *  为什么呢?
             *  第一是因为放置建立重复链接
             *  第二:提高效率(胜选规则规定了只能是myid大的胜出。所以,myid小的节点,把自己的选票发送给myid大是相对来说没有意义的)
             *  即使是已经建立了链接,也会关闭,重复建立!
             */
            connectOne(sid);
        }
    }

 SendWorker的职责(socket连接):

 class SendWorker extends ZooKeeperThread {  
 @Override
        public void run() {
            threadCnt.incrementAndGet();
            try {
                /**
                 * If there is nothing in the queue to send, then we
                 * send the lastMessage to ensure that the last message
                 * was received by the peer. The message could be dropped
                 * in case self or the peer shutdown their connection
                 * (and exit the thread) prior to reading/processing
                 * the last message. Duplicate messages are handled correctly
                 * by the peer.
                 *
                 * If the send queue is non-empty, then we have a recent
                 * message than that stored in lastMessage. To avoid sending
                 * stale message, we should send the message in the send queue.
                 */
                ArrayBlockingQueue<ByteBuffer> bq = queueSendMap.get(sid);
                if(bq == null || isSendQueueEmpty(bq)) {
                    ByteBuffer b = lastMessageSent.get(sid);
                    if(b != null) {
                        LOG.debug("Attempting to send lastMessage to sid=" + sid);
                        send(b);
                    }
                }
            } catch(IOException e) {
                LOG.error("Failed to send last message. Shutting down thread.", e);
                this.finish();
            }

            try {
                while(running && !shutdown && sock != null) {

                    ByteBuffer b = null;
                    try {

                        //  注释: 先获取这个 sid 对应的发送队列
                        ArrayBlockingQueue<ByteBuffer> bq = queueSendMap.get(sid);
                        if(bq != null) {

                            //  注释: b ByteBuffer  从发送队列获取选票!
                            b = pollSendQueue(bq, 1000, TimeUnit.MILLISECONDS);
                        } else {
                            LOG.error("No queue of incoming messages for " + "server " + sid);
                            break;
                        }

                        if(b != null) {
                            lastMessageSent.put(sid, b);

                            //  注释: 执行发送
                            send(b);
                        }
                    } catch(InterruptedException e) {
                        LOG.warn("Interrupted while waiting for message on queue", e);
                    }
                }
            } catch(Exception e) {
                LOG.warn("Exception when using channel: for id " + sid + " my id = " + QuorumCnxManager.this.mySid + " error = " + e);
            }
            this.finish();
            LOG.warn("Send worker leaving thread");
        }
    }
}

WorkerReceiver 和 RecvWorker

WorkerReciver也是统一的,负责通过QuorumCnxManager.RecvWorker(针对每一个sid有对应的recvWorker)将别的服务器的投票结果消息放到recvqueue容器中。然后等待处理

recvWorker的职责(socket连接):

       
class RecvWorker extends ZooKeeperThread {
 public void run() {
            threadCnt.incrementAndGet();
            try {
                while(running && !shutdown && sock != null) {
                    /**
                     * Reads the first int to determine the length of the message
                     */
                    int length = din.readInt();
                    if(length <= 0 || length > PACKETMAXSIZE) {
                        throw new IOException("Received packet with invalid packet: " + length);
                    }
                    /**
                     * Allocates a new ByteBuffer to receive the message
                     */
                    byte[] msgArray = new byte[length];

                    //  注释: din BIO 的输入流, 读取数据, 消息内容在 msgArray 里面
                    din.readFully(msgArray, 0, length);
                    ByteBuffer message = ByteBuffer.wrap(msgArray);

                    //  注释: 把从对方接受的投票放置在 reciveQueue 中
                    addToRecvQueue(new Message(message.duplicate(), sid));
                }
            } catch(Exception e) {
                LOG.warn("Connection broken for id " + sid + ", my id = " + QuorumCnxManager.this.mySid + ", error = ", e);
            } finally {
                LOG.warn("Interrupting SendWorker");
                sw.finish();
                if(sock != null) {
                    closeSocket(sock);
                }
            }
        }
}

WorkerReceiver的职责: 

class WorkerReceiver implements Runnable { 

public void run() {
                byte responseBytes[] = new byte[48];
                ByteBuffer responseBuffer = ByteBuffer.wrap(responseBytes);
                DatagramPacket responsePacket = new DatagramPacket(
                        responseBytes, responseBytes.length);
                while (true) {
                    // Sleeps on receive
                    try {
                        responseBuffer.clear();
                        mySocket.receive(responsePacket);
                    } catch (IOException e) {
                        LOG.warn("Ignoring exception receiving", e);
                    }
                    // Receive new message
                    if (responsePacket.getLength() != responseBytes.length) {
                        LOG.warn("Got a short response: "
                                + responsePacket.getLength() + " "
                                + responsePacket.toString());
                        continue;
                    }
                    responseBuffer.clear();
                    int type = responseBuffer.getInt();
                    if ((type > 3) || (type < 0)) {
                        LOG.warn("Got bad Msg type: " + type);
                        continue;
                    }
                    long tag = responseBuffer.getLong();

                    QuorumPeer.ServerState ackstate = QuorumPeer.ServerState.LOOKING;
                    switch (responseBuffer.getInt()) {
                    case 0:
                        ackstate = QuorumPeer.ServerState.LOOKING;
                        break;
                    case 1:
                        ackstate = QuorumPeer.ServerState.LEADING;
                        break;
                    case 2:
                        ackstate = QuorumPeer.ServerState.FOLLOWING;
                        break;
                    default:
                        throw new IllegalStateException("bad state "+responseBuffer.getInt());
                    }

                    Vote current = self.getCurrentVote();

                    switch (type) {
                    case 0:
                        // Receive challenge request
                        ToSend c = new ToSend(ToSend.mType.challenge, tag,
                                current.getId(), current.getZxid(),
                                logicalclock, self.getPeerState(),
                                (InetSocketAddress) responsePacket
                                        .getSocketAddress());
                        sendqueue.offer(c);
                        break;
                    case 1:
                        // Receive challenge and store somewhere else
                        long challenge = responseBuffer.getLong();
                        saveChallenge(tag, challenge);

                        break;
                    case 2:
                        Notification n = new Notification();
                        n.leader = responseBuffer.getLong();
                        n.zxid = responseBuffer.getLong();
                        n.epoch = responseBuffer.getLong();
                        n.state = ackstate;
                        n.addr = (InetSocketAddress) responsePacket
                                .getSocketAddress();

                        if ((myMsg.lastEpoch <= n.epoch)
                                && ((n.zxid > myMsg.lastProposedZxid) 
                                || ((n.zxid == myMsg.lastProposedZxid) 
                                && (n.leader > myMsg.lastProposedLeader)))) {
                            myMsg.lastProposedZxid = n.zxid;
                            myMsg.lastProposedLeader = n.leader;
                            myMsg.lastEpoch = n.epoch;
                        }

                        long recChallenge;
                        InetSocketAddress addr = (InetSocketAddress) responsePacket
                                .getSocketAddress();
                        if (authEnabled) {
                            ConcurrentHashMap<Long, Long> tmpMap = addrChallengeMap.get(addr);
                            if(tmpMap != null){
                                if (tmpMap.get(tag) != null) {
                                    recChallenge = responseBuffer.getLong();

                                    if (tmpMap.get(tag) == recChallenge) {
                                        recvqueue.offer(n);

                                        ToSend a = new ToSend(ToSend.mType.ack,
                                                tag, current.getId(),
                                                current.getZxid(),
                                                logicalclock, self.getPeerState(),
                                                addr);

                                        sendqueue.offer(a);
                                    } else {
                                        LOG.warn("Incorrect challenge: "
                                                + recChallenge + ", "
                                                + addrChallengeMap.toString());
                                    }
                                } else {
                                    LOG.warn("No challenge for host: " + addr
                                            + " " + tag);
                                }
                            }
                        } else {
                            recvqueue.offer(n);

                            ToSend a = new ToSend(ToSend.mType.ack, tag,
                                    current.getId(), current.getZxid(),
                                    logicalclock, self.getPeerState(),
                                    (InetSocketAddress) responsePacket
                                            .getSocketAddress());

                            sendqueue.offer(a);
                        }
                        break;

                    // Upon reception of an ack message, remove it from the
                    // queue
                    case 3:
                        Semaphore s = ackMutex.get(tag);
                        
                        if(s != null)
                            s.release();
                        else LOG.error("Empty ack semaphore");
                        
                        ackset.add(tag);

                        if (authEnabled) {
                            ConcurrentHashMap<Long, Long> tmpMap = addrChallengeMap.get(responsePacket
                                    .getSocketAddress());
                            if(tmpMap != null) {
                                tmpMap.remove(tag);
                            } else {
                                LOG.warn("No such address in the ensemble configuration " + responsePacket
                                    .getSocketAddress());
                            }
                        }

                        if (ackstate != QuorumPeer.ServerState.LOOKING) {
                            Notification outofsync = new Notification();
                            outofsync.leader = responseBuffer.getLong();
                            outofsync.zxid = responseBuffer.getLong();
                            outofsync.epoch = responseBuffer.getLong();
                            outofsync.state = ackstate;
                            outofsync.addr = (InetSocketAddress) responsePacket
                                    .getSocketAddress();

                            recvqueue.offer(outofsync);
                        }

                        break;
                    // Default case
                    default:
                        LOG.warn("Received message of incorrect type " + type);
                        break;
                    }
                }
            }
        }
}

4.选举

首先: 所有的节点一上线就是LOOKING状态,当选举结束后,有选举权的角色中就只有Leader和Follower.

           选票是Vote对象。封装了epoch,zxid,myid.

           在进行选举发送的时候,每个zk节点都会为别的zk服务节点生成对应的一个SendWorker 和 一个 ArrayBlockingQueue.  ArrayBlockingQueue存放待发送的选票,SendWorker从队列中获取选票执行发送。

     

选举源码简述:

# 入口,当调用 QuorumPeer 的 start() 方法的时候,它内部的 super.start() 会转到 run 方法
QuorumPeer.run()
# 真正负责选举的是 FastLeaderElection
FastLeaderElection.lookForLeader()
# 初始化存储 合法选票的 容器
HashMap<Long, Vote> recvset = new HashMap<Long, Vote>();
# 自增逻辑时钟
logicalclock.incrementAndGet();
# 准备选票
updateProposal(getInitId(), getInitLastLoggedZxid(), getPeerEpoch());
# 广播选票
sendNotifications();
# 将选票存入发送队列
sendqueue.offer(notmsg);
# 处理选票的发送
WorkerSender.run() + process(m);
manager.toSend(m.sid, requestBuffer);

真正选举的方法:

   public Vote lookForLeader() throws InterruptedException {

        try {
            self.jmxLeaderElectionBean = new LeaderElectionBean();
            MBeanRegistry.getInstance().register(self.jmxLeaderElectionBean, self.jmxLocalPeerBean);
        } catch(Exception e) {
            LOG.warn("Failed to register with JMX", e);
            self.jmxLeaderElectionBean = null;
        }
        if(self.start_fle == 0) {
            self.start_fle = Time.currentElapsedTime();
        }


        try {

            //  注释: 初始化了一个容器: 存储合法选票
            HashMap<Long, Vote> recvset = new HashMap<Long, Vote>();

            //  注释: 我一上来,结果leader早就选举好了,那么我在发起选举的时候,其他节点给我返回的信息就存储在这个里面
            HashMap<Long, Vote> outofelection = new HashMap<Long, Vote>();

            //  注释: 投票的最大等待时间
            int notTimeout = finalizeWait;

            /*************************************************
             *  注释: 第一个重要点:做两件事:
             *  1、logicalclock.incrementAndGet();
             *      自增逻辑时钟
             *      选举轮次 = epoch
             *      正常的胜选的规则: 三个条件:(epoch,zxid, myid)
             *  2、updateProposal(getInitId(), getInitLastLoggedZxid(), getPeerEpoch());
             *      Proposal 就是指一次提案(如果客户端需要往zk集群中写入一条数据,在zk内部,是以分布式事务的方式来执行的)
             *      一次分布式事务,就理解成 Proposal, 同样,每次选举也是!
             *      选票的所有内容,都是推举自己成为leader
             */
            synchronized(this) {
                logicalclock.incrementAndGet();
                // TODO_MA 注释:    myid      zxid                   epoch
                updateProposal(getInitId(), getInitLastLoggedZxid(), getPeerEpoch());
            }

            /*************************************************
             *  注释: 第二个重要点!
             *  关于选票/投票的概念: Vote Notification ByteBuffer Message ToSend
             *  发送选票给《所有其他节点》 到底是那些节点?
             *      zoo.cfg 中配置的 server
             */
            LOG.info("New election. My id =  " + self.getId() + ", proposed zxid=0x" + Long.toHexString(proposedZxid));
            sendNotifications();

            /*
             * Loop in which we exchange notifications until we find a leader
             */

            while((self.getPeerState() == ServerState.LOOKING) && (!stop)) {

                //  注释: 统一接收队列!
                //  注释: Notification 投票
                /*
                 * Remove next notification from queue, times out after 2 times
                 * the termination time
                 */
                Notification n = recvqueue.poll(notTimeout, TimeUnit.MILLISECONDS);

                /*
                 * Sends more notifications if haven't received enough.
                 * Otherwise processes new notification.
                 */
                if(n == null) {
                    if(manager.haveDelivered()) {
                        sendNotifications();
                    } else {
                        manager.connectAll();
                    }

                    /**
                     * Exponential backoff
                     */
                    int tmpTimeOut = notTimeout * 2;
                    notTimeout = (tmpTimeOut < maxNotificationInterval ? tmpTimeOut : maxNotificationInterval);
                    LOG.info("Notification time out: " + notTimeout);

                    /*************************************************
                     *  注释: 重要的分支
                     *  1、n 投票
                     *  2、n.sid 对方服务器的编号
                     *  3、n.leader 对方推举的leader的编号
                     */
                } else if(validVoter(n.sid) && validVoter(n.leader)) {
                    /**
                     * Only proceed if the vote comes from a replica in the
                     * voting view for a replica in the voting view.
                     */
                    //  注释: n.state 对方服务器的状态
                    switch(n.state) {
                        case LOOKING:

                            //  注释: 第一个分支: 对方的逻辑时钟比我大
                            //  注释: 我的票是无效的!,我接收到的投我当leader的票也都是无效的
                            // If notification > current, replace and send messages out
                            if(n.electionEpoch > logicalclock.get()) {

                                //  注释: 把自己的逻辑时钟改成给别人的
                                logicalclock.set(n.electionEpoch);

                                //  注释: 清空所有投我当leader的投票
                                recvset.clear();

                                //  注释: 去判断是否要更新选票
                                //  注释: 前三个参数是别人的 epoch zxid serverid,
                                //  注释:  后三个参数是我自己的: epoch zxid serverid,
                                if(totalOrderPredicate(n.leader, n.zxid, n.peerEpoch, getInitId(), getInitLastLoggedZxid(), getPeerEpoch())) {

                                    //  注释: 如果n胜出,则把自己的选票信息个,更新成别人的
                                    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();
                            }

                            if(LOG.isDebugEnabled()) {
                                LOG.debug("Adding vote: from=" + n.sid + ", proposed leader=" + n.leader + ", proposed zxid=0x" + Long
                                        .toHexString(n.zxid) + ", proposed election epoch=0x" + Long.toHexString(n.electionEpoch));
                            }

                            /*************************************************
                             *  注释:  把当前获取到的 Notificatioin n 变成 Vote 然后存储到了  recvset (存储有效投票的容器)
                             */
                            recvset.put(n.sid, new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch));

                            /*************************************************
                             *  注释: 执行唱票!
                             *  统计一下,看是否有任何一个节点的获取票数,超过半数!
                             *  如果是,选举能出结果!
                             */
                            if(termPredicate(recvset, new Vote(proposedLeader, proposedZxid, logicalclock.get(), proposedEpoch))) {

                                //  注释: 再次获取一次投票,检查是否有更新的选举轮次出现
                                // Verify if there is any change in the proposed leader
                                while((n = recvqueue.poll(finalizeWait, TimeUnit.MILLISECONDS)) != null) {
                                    if(totalOrderPredicate(n.leader, n.zxid, n.peerEpoch, proposedLeader, proposedZxid, proposedEpoch)) {
                                        recvqueue.put(n);
                                        break;
                                    }
                                }

                                /*
                                 * This predicate is true once we don't read any new
                                 * relevant message from the reception queue
                                 */
                                if(n == null) {

                                    //  注释: 设置状态: 胜选的节点设置成 leading ,败选的就设置成 following
                                    self.setPeerState((proposedLeader == self.getId()) ? ServerState.LEADING : learningState());

                                    //  注释: 存储最新的投票
                                    Vote endVote = new Vote(proposedLeader, proposedZxid, logicalclock.get(), proposedEpoch);

                                    //  注释: 清空投票
                                    leaveInstance(endVote);

                                    //  注释: 返回投票
                                    return endVote;
                                }
                            }
                            break;
                        case OBSERVING:
                            LOG.debug("Notification from observer: " + n.sid);
                            break;
                        case FOLLOWING:
                        case LEADING:
                            /*
                             * Consider all notifications from the same epoch
                             * together.
                             */
                            if(n.electionEpoch == logicalclock.get()) {
                                recvset.put(n.sid, new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch));

                                if(ooePredicate(recvset, outofelection, n)) {
                                    self.setPeerState((n.leader == self.getId()) ? ServerState.LEADING : learningState());

                                    Vote endVote = new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch);
                                    leaveInstance(endVote);
                                    return endVote;
                                }
                            }

                            /*
                             * Before joining an established ensemble, verify
                             * a majority is following the same leader.
                             */
                            outofelection.put(n.sid, new Vote(n.version, n.leader, n.zxid, n.electionEpoch, n.peerEpoch, n.state));

                            if(ooePredicate(outofelection, outofelection, n)) {
                                synchronized(this) {
                                    logicalclock.set(n.electionEpoch);
                                    self.setPeerState((n.leader == self.getId()) ? ServerState.LEADING : learningState());
                                }
                                Vote endVote = new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch);
                                leaveInstance(endVote);
                                return endVote;
                            }
                            break;
                        default:
                            LOG.warn("Notification state unrecognized: {} (n.state), {} (n.sid)", n.state, n.sid);
                            break;
                    }
                } else {
                    if(!validVoter(n.leader)) {
                        LOG.warn("Ignoring notification for non-cluster member sid {} from sid {}", n.leader, n.sid);
                    }
                    if(!validVoter(n.sid)) {
                        LOG.warn("Ignoring notification for sid {} from non-quorum member sid {}", n.leader, n.sid);
                    }
                }
            }
            return null;
        } finally {
            try {
                if(self.jmxLeaderElectionBean != null) {
                    MBeanRegistry.getInstance().unregister(self.jmxLeaderElectionBean);
                }
            } catch(Exception e) {
                LOG.warn("Failed to unregister with JMX", e);
            }
            self.jmxLeaderElectionBean = null;
            LOG.debug("Number of connection processing threads: {}", manager.getConnectionThreadCount());
        }
    }

 

选举流程总结

1.自增选举轮次: zookeeper规定所有有效的投票都必须在同一个批次中,在开始新的一轮投票时,会先对logicalclock进行自增操作。

2.初始化选票:    在开始进行新一轮投票之前,每个服务器都会初始化自身的选票,并且在初始化阶段,每台服务器都会将自己推举为leader.

3.发送初始化选票: 完成选票的初始化后,服务器就会发起第一次投票,zookeeper会将刚刚初始化好的选票放入sendqueue中,由发送器WorkerSender负责发送出去。

4.接收外部投票:  每台服务器会不断的从recvqueue队列中获取外部投票,如果服务器发现无法获取到任何外部投票,那么就会立即确认自己是否和集群中其他服务器保持着有效的连接,如果没有连接,则马上建立连接,如果已经建立了连接,则再次发送自己当前的内部投票。

5.判断选举轮次: 在发送完初始化选票之后,接着开始处理外部投票,在处理外部投票时,会根据选举轮次来进行不同的处理。

  •                        外部投票的选举轮次大于内部投票。若服务器自身的选举轮次落后于该外部投票对应服务器的选举轮次,那么就会立即更新自己的选举轮次(logicalclock),并且清空所有已经收到的投票,然后使用初始化的投票来进行PK以确定是否变更内部投票,最终再将内部投票发送出去。
  •                        外部投票的选举轮次小于内部投票。若服务器接收的外选票的选举批次落后于自身的选举轮次,那么zookeeper就会直接忽略该外部投票,不做任何处理。并返回步骤4.
  •                        外部投票的选举轮次等于内部投票。此时可以开始进行选票PK。

6.选举PK。 在进行选票PK时,符合任意一个条件就需要变更投票。

  •                              若外部投票中推荐的Leader服务器的选举轮次大于内部投票,那么需要变更投票。
  •                              若选举轮次一致,那么就对比两者的ZXID,若外部投票的ZXID大,那么需要变更投票。
  •                              若两者的ZXID一致,那么就对比两者的SID,若外部投票的SID大,那么就需要变更投票。

7.变更投票。经过PK之后,若确定了外部投票优于内部投票,那么就变更投票,即使外部投票的选票信息来覆盖内部信息,变更完成后,再次将这个变更后的内部投票发送出去。

8.选票归档。无论是否变更了投票,都会将刚刚收到的那么外部投票放入选票集合recvset中进行归档,recvset用于记录当前服务器在本轮次的Leader选举中收到的所有外部投票(按照服务器Sid,如:{(1,vote),(2,vote)})

9.统计投票。完成选票归档后,就可以开始统计投票,统计投票是为了统计集群中是否已经有过半的服务器认可了当前的内部投票。如果确定已经有过半服务器认可了该投票,则终止投票,否则返回步骤4.

10.更新服务器状态。若已经确定可以终止投票,那么就开始更新服务器状态,服务器首选判断当前被过半服务器认可的投票所对应的Leader服务器是否是自己,若是自己,则将自己的服务器状态更新为Leading,若不是,则根据具体情况来确定自己是Following或是Observing.

 

 

 

 

 

 

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

刘狗

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

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

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

打赏作者

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

抵扣说明:

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

余额充值