Zookeeper源码分析之彻底弄懂leader选举底层原理

摘要:上一篇文章,我着重介绍了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方法来终止本次选举。可以正常的向外提供服务。

四.总结

本文从名词解释,设计内部流程到最后的源码分析,篇幅较长,但干货满满,希望看到这篇文章的读者都能有所收获!码字不易,愿读者能给个赞或者关注支持下,以后会持续定期更新更多技术分享!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值