摘要:上一篇文章,我着重介绍了Zookeeper集群中leader选举的流程分析,感兴趣的可以看看leader选举流程详解。这里就不再赘述了,今天要讲的是从源码的角度去分析是如何进行leader选举的,下面开始今天的主题。
本文是基于zookeeper3.4.12来进行讲解的,大家可自行去github上下载并完成构建。
一.参数讲解
在开始讲解源码前,有必要提前了解以下几个参数:
- Notification:来自其他节点的选票信息,其中包含了被推选为leader的id,被推选为leader的zxid等信息;
- ToSend:本节点待发送的选票信息,其中包含了被推选为leader的id,被推选为leader的zxid等信息;
- sendqueue:这是一个队列,用来保存所有待发送的选票信息,存在于FastLeaderElection中;
- recvqueue:这是一个队列,用来保存所有来自其他节点的选票信息,存在于FastLeaderElection中;
- WorkerSender:选票发送器,不断从sendqueue中获取待发送的选票,并将其传递到底层QuorumCnxManager中;
- WorkerReceiver:选票接收器。不断从QuorumCnxManager中获取其他服务器发来的投票消息,保存到recvqueue中,在选票接收过程中,如果发现该外部选票的选举轮次小于当前服务器的,那么忽略该外部投票,同时立即发送自己的内部投票。
- recvQueue:消息接收队列,用于存放那些从其他服务器接收到的消息,存在于QuorumCnxManager中;
- queueSendMap:消息发送队列,每一台服务器根据id会创建的发送队列,存在于QuorumCnxManager中;
- lastMassageSend:最近一次发送的消息,同样也是根据ID分组,存在于QuorumCnxManager中;
- SendWorker:是一个线程,当节点间建立连接后,就会启动,负责选票信息的发送;
- RecvWorker:是一个线程,当节点建立连接后,就会启动,负责选票信息的接受;
- QuorumCnxManager:负责底层网络IO,发送选票和接受选票。
- FastLeaderElection:里面最重要的方法,lookForLeader就是用来完成领导选举的。
二.内部选举流程
看完上面的参数后,想必应该有一个大致的了解。那就是Zookeeper集群中的leader选举,分工非常明确。QuorumCnxManager就专门来负责网络IO,与其他服务器节点建立网络通信,完成票据信息的发送和接收。FastLeaderElection就专门负责处理投票逻辑,选出leader。内部大致的流程如下:
三.源码剖析
从启动类开始分析,当我们通过./zkServer.sh start命令启动shell脚本时,其实是运行了Zookeeper程序中的启动类org.apache.zookeeper.server.quorum.QuorumPeerMain。下面看下这个启动类具体的实现逻辑。
public static void main(String[] args) { QuorumPeerMain main = new QuorumPeerMain(); //进行配置的初始化并启动程序,参数args是配置文件的全路径名 main.initializeAndRun(args); } protected void initializeAndRun(String[] args) throws ConfigException, IOException { QuorumPeerConfig config = new QuorumPeerConfig(); if (args.length == 1) { //解析配置文件中的相关参数并加载到内存,如zoo.cfg文件 config.parse(args[0]); } //集群模式的配置文件跟单机模式的配置文件略有差别,集群会配置多个server if (args.length == 1 && config.servers.size() > 0) { //集群模式启动 runFromConfig(config); } else { //单机模式启动 ZooKeeperServerMain.main(args); } }
逻辑比较清晰,运行main方法,进行相关配置文件的解析和加载,根据配置中是否存在server节点判断是否以集群模式启动,这里我们只分析集群模式,因为只有集群模式才会存在leader选举,然后让我们进入runFromConfig方法。
public void runFromConfig(QuorumPeerConfig config) throws IOException { try { //这个ServerCnxnFactory很重要,开启NIO负责与Client进行通信 ServerCnxnFactory cnxnFactory = ServerCnxnFactory.createFactory(); cnxnFactory.configure(config.getClientPortAddress(), config.getMaxClientCnxns()); quorumPeer = getQuorumPeer(); quorumPeer.setQuorumPeers(config.getServers()); //初始化所有参与选举的server节点 quorumPeer.setTxnFactory(new FileTxnSnapLog( new File(config.getDataLogDir()), new File(config.getDataDir()))); quorumPeer.setElectionType(config.getElectionAlg()); //选举算法类型,目前仅支持FastLeaderElection算法 quorumPeer.setMyid(config.getServerId()); //当前机器的id quorumPeer.setTickTime(config.getTickTime()); //机器间的心态间隔时间 quorumPeer.setCnxnFactory(cnxnFactory); //NIO通信 quorumPeer.setClientPortAddress(config.getClientPortAddress()); //客户端通信端口地址 quorumPeer.setMinSessionTimeout(config.getMinSessionTimeout()); //会话最小超时时间 quorumPeer.setMaxSessionTimeout(config.getMaxSessionTimeout()); quorumPeer.setZKDatabase(new ZKDatabase(quorumPeer.getTxnFactory())); quorumPeer.setLearnerType(config.getPeerType()); quorumPeer.setSyncEnabled(config.getSyncEnabled()); quorumPeer.initialize(); quorumPeer.start(); //能让当前线程获取优先执行权限,直至执行结束 quorumPeer.join(); } catch (InterruptedException e) { // warn, but generally this is ok LOG.warn("Quorum Peer interrupted", e); } }
这段代码我省略很多的属性设置,只展示了一些我们熟知的,比较常见的属性配置。这一步做了两件事,开启网络通信用来接收客户端的请求,然后初始化QuorumPeer对象,该对象每一个节点都单独拥有一份,里面存储了当前节点的一系列属性值。ServerCnxnFactory是一个接口,它的默认实现是NIOServerCnxnFactory类,我们来看下里面的具体实现。
public class NIOServerCnxnFactory extends ServerCnxnFactory implements Runnable { //实现了Runnable接口,当线程启动后会执行run方法 } static public ServerCnxnFactory createFactory() throws IOException { //从系统参数中获取zookeeper.serverCnxnFactory属性的值 String serverCnxnFactoryName = System.getProperty(ZOOKEEPER_SERVER_CNXN_FACTORY); if (serverCnxnFactoryName == null) { //没有则默认使用NIOServerCnxnFactory serverCnxnFactoryName = NIOServerCnxnFactory.class.getName(); } //反射创建NIOServerCnxnFactory实例 ServerCnxnFactory serverCnxnFactory = (ServerCnxnFactory) Class.forName(serverCnxnFactoryName) .getDeclaredConstructor().newInstance(); return serverCnxnFactory; } public void configure(InetSocketAddress addr, int maxcc) throws IOException { configureSaslLogin(); thread = new ZooKeeperThread(this, "NIOServerCxn.Factory:" + addr); thread.setDaemon(true); maxClientCnxns = maxcc; this.ss = ServerSocketChannel.open();//打开通讯管道 ss.socket().setReuseAddress(true); LOG.info("binding to port " + addr); ss.socket().bind(addr);//绑定监听端口 ss.configureBlocking(false);//设置为非阻塞 ss.register(selector, SelectionKey.OP_ACCEPT); }
这里就是对NIOServer端进行一些基础的初始化工作。QuorumPeer对象初始化完后,会调用start方法,继续往下跟。
public synchronized void start() { //加载数据 loadDataBase(); //开启NIO线程,执行run方法接收来自客户端的请求 cnxnFactory.start(); //选举算法的选中,初始化QuorumCxnManager startLeaderElection(); //开始选举 super.start(); }
这里做了四件事,都十分的重要,
第一步:loadDataBase方法从zoo.cfg文件中配置的dataDir,snapDir数据目录读取数据文件,然后将数据加载到内存中,其中会设计到之后的zxid,currEpoch的获取,这些参数以后进行选票会用到。
private void loadDataBase() { //从当前配置的snapDir文件目录下加载 File updating = new File(getTxnFactory().getSnapDir(), UPDATING_EPOCH_FILENAME); zkDb.loadDataBase(); //获取epoch属性 long lastProcessedZxid = zkDb.getDataTree().lastProcessedZxid; long epochOfZxid = ZxidUtils.getEpochFromZxid(lastProcessedZxid); currentEpoch = readLongFromFile(CURRENT_EPOCH_FILENAME); } public long loadDataBase() throws IOException { long zxid = snapLog.restore(dataTree, sessionsWithTimeouts,commitProposalPlaybackListener); initialized = true; return zxid; }
第二步:启动NIOServer线程,自然而然会去执行run方法,来处理客户端的连接请求。
public void run() { while (!ss.socket().isClosed()) { selector.select(1000); Set<SelectionKey> selected; synchronized (this) { selected = selector.selectedKeys(); } ArrayList<SelectionKey> selectedList = new ArrayList<SelectionKey>( selected); Collections.shuffle(selectedList); //轮询遍历客户端的请求状态 for (SelectionKey k : selectedList) { if ((k.readyOps() & SelectionKey.OP_ACCEPT) != 0) { SocketChannel sc = ((ServerSocketChannel) k .channel()).accept(); InetAddress ia = sc.socket().getInetAddress(); int cnxncount = getClientCnxnCount(ia); if (maxClientCnxns > 0 && cnxncount >= maxClientCnxns){ sc.close(); } else { sc.configureBlocking(false); SelectionKey sk = sc.register(selector,SelectionKey.OP_READ); NIOServerCnxn cnxn = createConnection(sc, sk); sk.attach(cnxn); addCnxn(cnxn); } } else if ((k.readyOps() & (SelectionKey.OP_READ | SelectionKey.OP_WRITE)) != 0) { NIOServerCnxn c = (NIOServerCnxn) k.attachment(); c.doIO(k); } } selected.clear(); } closeAll(); }
第三步:调用startLeaderElection方法,首先会初始化自己的选票。,选中选举算法FastLeaderElection的创建。
try { //初始化自己的内部选票(当前的id,事务id,当前的选举轮次) currentVote = new Vote(myid, getLastLoggedZxid(), getCurrentEpoch()); } catch(IOException e) { RuntimeException re = new RuntimeException(e.getMessage()); re.setStackTrace(e.getStackTrace()); throw re; }
然后初始化底层网络IO的QuorumCnxManager对象,创建对应的接收队列和发送队列,为选举做准备。
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) { //构建sendWorker,消息发送器,用来发送自身投票 this.senderWorkerMap = senderWorkerMap; //消息接收队列,保存来自其他服务器节点的投票 this.recvQueue = new ArrayBlockingQueue<Message>(RECV_CAPACITY); //根据SID分组,为其他每台节点单独创建发送队列 this.queueSendMap = new ConcurrentHashMap<Long, ArrayBlockingQueue<ByteBuffer>>(); //保存最近发送的一次投票 this.lastMessageSent = new ConcurrentHashMap<Long, ByteBuffer>(); String cnxToValue = System.getProperty("zookeeper.cnxTimeout"); if(cnxToValue != null){ this.cnxTO = Integer.parseInt(cnxToValue); } //服务器id this.mySid = mySid; this.socketTimeout = socketTimeout; this.view = view; // 这个地方非常重要,这是一个监听器线程,监听选举端口,开启后会监听来自其他服务的连接请求 listener = new Listener(); }
最上面的属性解释,在这一步就得到了验证,这里就不再多赘述,代码注释也写的很清楚。在这里重点解释一下这个Listener对象。它是QuorumCnxManager内部的一个监听器线程,用来监听选举端口(默认监听3888)。
protected Election createElectionAlgorithm(int electionAlgorithm){ Election le=null; //构建QuorumCnxManager对象,初始化相关参数 qcm = createCnxnManager(); QuorumCnxManager.Listener listener = qcm.listener; if(listener != null){ //开启选举监听,之后会执行run方法,接收来自其他服务器的连接请求 listener.start(); le = new FastLeaderElection(this, qcm); } return le; }
此处的IO通讯还有一个规则,那就是myid小的一方如果连到了大的一方,那么此时会关闭当前的socket连接,然后大的一方会主动向小的一方发起连接请求,创建一个新的连接。如果连接创建后,此时会创建消息接收器和消息发送器,循环接收和发送投票。
public void run() { int numRetries = 0; InetSocketAddress addr; while((!shutdown) && (numRetries < 3)){ try { //建立ServerSocket,监听选举端口 ss = new ServerSocket(); ss.setReuseAddress(true); if (listenOnAllIPs) { int port = view.get(QuorumCnxManager.this.mySid).electionAddr.getPort(); addr = new InetSocketAddress(port); } else { addr = view.get(QuorumCnxManager.this.mySid).electionAddr; } setName(view.get(QuorumCnxManager.this.mySid).electionAddr.toString()); //绑定监听地址 ss.bind(addr); while (!shutdown) { Socket client = ss.accept(); setSockOpts(client); if (quorumSaslAuthEnabled) { receiveConnectionAsync(client); } else { //接收连接请求 receiveConnection(client); } numRetries = 0; } } catch (IOException e) { //...省略无关代码 } } } private void handleConnection(Socket sock, DataInputStream din) throws IOException { Long sid = null; sid = din.readLong(); if (sid < 0) { } if (sid == QuorumPeer.OBSERVER_ID) { } if (sid < this.mySid) { //发起连接的机器id小于当前机器id,关闭连接 closeSocket(sock); //大的一端主动向小的一端发起连接请求 connectOne(sid); } else { //创建消息发送器 SendWorker sw = new SendWorker(sock, sid); //创建消息接收器 RecvWorker rw = new RecvWorker(sock, din, sid, sw); sw.setRecv(rw); SendWorker vsw = senderWorkerMap.get(sid); if(vsw != null) vsw.finish(); senderWorkerMap.put(sid, sw); queueSendMap.putIfAbsent(sid, new ArrayBlockingQueue<ByteBuffer>(SEND_CAPACITY)); //启动线程,开始接收和发送消息 sw.start(); rw.start(); } }
最后,会初始化FastLeaderElection对象,我们来看下具体都做了些什么?
public FastLeaderElection(QuorumPeer self, QuorumCnxManager manager){ this.stop = false; this.manager = manager; starter(self, manager); } private void starter(QuorumPeer self, QuorumCnxManager manager) { this.self = self; proposedLeader = -1; proposedZxid = -1; //初始化发送队列 sendqueue = new LinkedBlockingQueue<ToSend>(); //初始化接收队列 recvqueue = new LinkedBlockingQueue<Notification>(); //初始化消息接收器和消息发送器 this.messenger = new Messenger(manager); } Messenger(QuorumCnxManager manager) { //创建消息发送器 this.ws = new WorkerSender(manager); Thread t = new Thread(this.ws, "WorkerSender[myid=" + self.getId() + "]"); t.setDaemon(true); t.start(); //创建消息接收器 this.wr = new WorkerReceiver(manager); t = new Thread(this.wr, "WorkerReceiver[myid=" + self.getId() + "]"); t.setDaemon(true); t.start(); }
这一步就是初始化FastLeaderElection层面的选票接收队列和选票发送队列了。让我们来看看这两个工人是如何干活的,顺便印证最开始的参数解读。
//WorkerSender的run方法---FastLeaderElection层面 public void run() { while (!stop) { try { //从sendqueue队列中不断的来获取选票 ToSend m = sendqueue.poll(3000, TimeUnit.MILLISECONDS); if(m == null) continue; //处理选票,无非就是将内部选票加入sendQueue中 process(m); } catch (InterruptedException e) { break; } } LOG.info("WorkerSender is down"); } //WorkerReceiver的run方法---FastLeaderElection层面 public void run() { Message response; while (!stop) { // Sleeps on receive try{ //不断从recvQueue队列中获取来自其他节点的选票 response = manager.pollRecvQueue(3000, TimeUnit.MILLISECONDS); if(response == null) continue; //省略了中间的判断和封装细节,封装好后将选票放入recvqueue队列中 recvqueue.offer(n); } catch (InterruptedException e) { System.out.println("Interrupted Exception while waiting for new message" + e.toString()); } } LOG.info("WorkerReceiver is down"); }
第四步:调用线程的QuorumPeer中的start方法,最终会进入当前线程的run方法执行主逻辑。
public void run() { while(running){ //获取当前的状态 switch (getPeerState()) { case LOOKING: try { setBCVote(null); //进入最重要的方法lookForLeader(),真正的选举流程 setCurrentVote(makeLEStrategy().lookForLeader()); } catch (Exception e) { LOG.warn("Unexpected exception", e); setPeerState(ServerState.LOOKING); } break; //.....省略了其他的判断分支,这里我们重点讲解leader选举 } } } }
接下来进入我们今天最核心的代码lookForLeader方法,点进去看看,研究下具体的实现逻辑。
public Vote lookForLeader() throws InterruptedException { try { //保存接收到的选票,便于后面过半机制选出leader HashMap<Long, Vote> recvset = new HashMap<Long, Vote>(); HashMap<Long, Vote> outofelection = new HashMap<Long, Vote>(); int notTimeout = finalizeWait; //选举开始前 synchronized(this){ //当前的选举轮次+1 logicalclock.incrementAndGet(); //更新提议,先选举自己为leader updateProposal(getInitId(), getInitLastLoggedZxid(), getPeerEpoch()); } //将当前的选票放入sendqueue中,由选票发送器发送给其他服务器 sendNotifications(); //当前状态为looking并且服务正常运行 while ((self.getPeerState() == ServerState.LOOKING) && (!stop)){ //从recvqueue队列中获取来自其他节点的选票 Notification n = recvqueue.poll(notTimeout, TimeUnit.MILLISECONDS); //如果为空 if(n == null){ //判断当前发送队列是否为空来确保所有的选票已经被发送 if(manager.haveDelivered()){ //为空则再发送一遍 sendNotifications(); } else { //不为空则重新建立连接 manager.connectAll(); } } else if(self.getVotingView().containsKey(n.sid)) { // 投票者集合中包含接收到消息中的服务器id switch (n.state) { case LOOKING: //其选举周期大于逻辑时钟,说明自己的选举轮次已经落后 if (n.electionEpoch > logicalclock.get()) { //立即将自己的逻辑时钟更新为最新 logicalclock.set(n.electionEpoch); //清空投票箱 recvset.clear(); //外部选票和内部选票进行PK if(totalOrderPredicate(n.leader, n.zxid, n.peerEpoch, getInitId(), getInitLastLoggedZxid(), getPeerEpoch())) { //外部选票胜,则更新自己内部的提议,把票投给外部 updateProposal(n.leader, n.zxid, n.peerEpoch); } else { //内部选票胜,也进行更新,主要是更新peerEpoch为最新 updateProposal(getInitId(),getInitLastLoggedZxid(),getPeerEpoch()); } //将当前的选票放入sendqueue中,由选票发送器发送给其他服务器 sendNotifications(); } else if (n.electionEpoch < logicalclock.get()) { //其选举周期小于逻辑时钟,说明自己的选举轮次新,此时不予理睬,直接忽略 break; } else if (totalOrderPredicate(n.leader, n.zxid, n.peerEpoch, proposedLeader, proposedZxid, proposedEpoch)) { //两者在同一届选举中,PK后外部更优,更新提议,选举外部为leader updateProposal(n.leader, n.zxid, n.peerEpoch); //将当前的选票放入sendqueue中,由选票发送器发送给其他服务器 sendNotifications(); } //保存外部的选票 recvset.put(n.sid, new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch)); //此时试图去判断是否已经有超过半数的选票投给了当前提议中的leader if (termPredicate(recvset, new Vote(proposedLeader, proposedZxid,logicalclock.get(), proposedEpoch))) { //如果已经有过半投票,此时再从队列中获取选票看是否有更优秀的节点 while((n = recvqueue.poll(finalizeWait, TimeUnit.MILLISECONDS)) != null){ if(totalOrderPredicate(n.leader, n.zxid, n.peerEpoch, proposedLeader, proposedZxid, proposedEpoch)){ //拉取到选票后进行PK,如果外部胜利则将该选票放入队列中,继续进行选举 recvqueue.put(n); break; } } //如果没有选票,则进行状态的变更, if (n == null) { 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: 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; } } 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: break; } } } return null; } }
这段代码比较长,但是注释我也写的比较清楚,如果配合选举规则来看,稍加阅读都能懂。接下来我讲解下其中的个别细节吧,选举前需要更新自己的逻辑时钟,并推选自己为leader,为何要这么做??
//选举开始前 synchronized(this){ //当前的选举轮次+1 logicalclock.incrementAndGet(); //更新提议,先选举自己为leader updateProposal(getInitId(), getInitLastLoggedZxid(), getPeerEpoch()); } //更新选举提议,推选新的leader synchronized void updateProposal(long leader, long zxid, long epoch){ //被推选为leader的id proposedLeader = leader; //被推选为leader的zxid proposedZxid = zxid; //被推选为leader的epoch proposedEpoch = epoch; }
逻辑时钟是在zkServer端维护的一个自增序列,每开启新一轮的选举都会自增,这样做是为了确保集群中参与选举的节点都在同一个轮次中,而为什么在最开始会选择自己做leader呢(人嘛都会有私心,哈哈)?这个只是最初的选票,因为此时没有参考物,每个节点都认为自己最优,随着收到外部的投票后,经过PK才会选出更优,被推举为leader。接下来来看下发送选票方法sendNotifications的具体实现细节。
private void sendNotifications() { for (QuorumServer server : self.getVotingView().values()) { long sid = server.id; ToSend notmsg = new ToSend(ToSend.mType.notification,proposedLeader,proposedZxid, logicalclock.get(),QuorumPeer.ServerState.LOOKING,sid,proposedEpoch); //将组装好的选票放入待发送的队列 sendqueue.offer(notmsg); } }
逻辑比较简单,循环遍历,将选票放入队列中,至于以后我不管,交由选票发送器WorkSender来处理,所谓各司其职,在这里运用的很好啊!而在选票的过程中,这里有三个判断,对这三种情况都做了不同的处理。
//其选举周期大于逻辑时钟,说明自己的选举轮次已经落后 if (n.electionEpoch > logicalclock.get()) { //立即将自己的逻辑时钟更新为最新 logicalclock.set(n.electionEpoch); //清空投票箱 recvset.clear(); //外部选票和内部选票进行PK if(totalOrderPredicate(n.leader, n.zxid, n.peerEpoch, getInitId(), getInitLastLoggedZxid(), getPeerEpoch())) { //外部选票胜,则更新自己内部的提议,把票投给外部 updateProposal(n.leader, n.zxid, n.peerEpoch); } else { //内部选票胜,也进行更新,主要是更新peerEpoch为最新 updateProposal(getInitId(),getInitLastLoggedZxid(),getPeerEpoch()); } //将当前的选票放入sendqueue中,由选票发送器发送给其他服务器 sendNotifications(); } else if (n.electionEpoch < logicalclock.get()) { //其选举周期小于逻辑时钟,说明自己的选举轮次新,此时不予理睬,直接忽略 break; } else if (totalOrderPredicate(n.leader, n.zxid, n.peerEpoch, proposedLeader, proposedZxid, proposedEpoch)) { //两者在同一届选举中,PK后外部更优,更新提议,选举外部为leader updateProposal(n.leader, n.zxid, n.peerEpoch); //将当前的选票放入sendqueue中,由选票发送器发送给其他服务器 sendNotifications(); } //选票进行PK protected boolean totalOrderPredicate(long newId, long newZxid, long newEpoch, long curId, long curZxid, long curEpoch) { /* * We return true if one of the following three cases hold: * 1- epoch越大越优秀 * 2- epoch相等的情况下,如果zxid越大越优秀 * 3- zxid相等的情况下,如果id越大越优秀 */ return ((newEpoch > curEpoch) || ((newEpoch == curEpoch) && ((newZxid > curZxid) || ((newZxid == curZxid) && (newId > curId))))); }
PK规则请看代码注释,写的很清楚,接下来继续往下看。保留外部的选票到集合中,此时试图选举出leader,对,这个时候只是尝试选出,没有的话还得继续循环。怎么才算选出呢?
//保存外部的选票 recvset.put(n.sid, new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch)); //此时试图去判断是否已经有超过半数的选票投给了当前提议中的leader if (termPredicate(recvset, new Vote(proposedLeader, proposedZxid,logicalclock.get(), proposedEpoch))) { //如果已经有过半投票,此时再从队列中获取选票看是否有更优秀的节点 while((n = recvqueue.poll(finalizeWait, TimeUnit.MILLISECONDS)) != null){ if(totalOrderPredicate(n.leader, n.zxid, n.peerEpoch, proposedLeader, proposedZxid, proposedEpoch)){ //拉取到选票后进行PK,如果外部胜利则将该选票放入队列中,继续进行选举 recvqueue.put(n); break; } } //如果没有选票,则进行状态的变更, if (n == null) { self.setPeerState((proposedLeader == self.getId()) ? ServerState.LEADING: learningState()); Vote endVote = new Vote(proposedLeader,proposedZxid,logicalclock.get(),proposedEpoch); leaveInstance(endVote); return endVote; } } protected boolean termPredicate( HashMap<Long, Vote> votes, Vote vote) { HashSet<Long> set = new HashSet<Long>(); //循环遍历,选出当前被推举为leader的选票,并将sid加入set中 for (Map.Entry<Long,Vote> entry : votes.entrySet()) { if (vote.equals(entry.getValue())){ set.add(entry.getKey()); } } //此时去判断时候已有过半节点选择了当前被推举的leader节点 return self.getQuorumVerifier().containsQuorum(set); } public boolean containsQuorum(HashSet<Long> set){ //判断是否当前set的大小已经超过了集群节点的半数 return (set.size() > half); }
这就是Zookeeper中鼎鼎有名的过半机制,我不需要你所有人都选我,只要有一半以上的节点选了我,那我就是leader,毕竟少数服从多数嘛!这个时候如果有一半以上的节点选择了当前被选举的leader节点,那么这时Zookeeper说,先不急,我再看看我剩余的收票的队列里还有没有票,有的话我再跟你进行一个PK,如果还有没有更牛的,有的话继续循环进行选举。没有的话,那这个时候leader已经选出,改变当前节点的服务器状态,leading或者following。然后将选票发送给其他节点,告诉别人已经选出了leader了。leader选出之后,会调用shutdown方法来终止本次选举。可以正常的向外提供服务。
四.总结
本文从名词解释,设计内部流程到最后的源码分析,篇幅较长,但干货满满,希望看到这篇文章的读者都能有所收获!码字不易,愿读者能给个赞或者关注支持下,以后会持续定期更新更多技术分享!