Zookeeper 源码分析–Zookeeper启动流程(二)

Zookeeper 源码分析–Zookeeper启动流程(二)

上次说到zookeeper的启动过程,我们已经了解到zookeeper是如何找寻配置文件以及启动的,然而上次也遗留了一个问题,那么zookeeper是如何选举的呢?网上的博客确实很多,有些博客概括的内容也很不错,但是本着探究事物原理的执着,这次就接着上次探究下zookeeper是如何选举并完成最终的集群启动的。

Zookeeper的选举算法

这是上次我们在结尾处看到的选举算法,可以看到,一开始建立了一个新的Vote,其实就是zookeeper中的投票,在这里我们可以理解为在开始选举算法时,在这个同步的startLeaderElection方法中新生成了一个投票:
在这里插入图片描述
在之前的分析中,我们了解到,集群启动还需要进行选举,也就是我们必须要了解Vote的信息,而投票的初始化内容如下:,一开始服务的状态是looking,也就是正处于选举阶段,目前还没有leader和follower:
在这里插入图片描述
书接上回,上次说道调用了一个选举方法,里面根据case的不同,其实也就是electionType的不同会使用不同的选举方法,默认情况下zookeeper选择的是case 3的快速选举算法:
在这里插入图片描述
上次的分析已经了解到,zookeeper启动了listener监听连接请求,同时通过manager设置了链接相关的内容,如keepalive、timeout等等,那直接进入吧,回顾的也差不多了,那看下FastLeaderElection:
在这里插入图片描述
可以看到,这个需要保证每一个peer只创建一次,也就是在集群中每个参与选举的服务器都在同一个轮次、同一个服务实例中只允许被创建一次。同时可以看到,在FastLeaderElection创建的时候,还调用了starter方法:
在这里插入图片描述
可以看到,初始化了两个队列,一个send发送的队列,另一个是recv接受的队列,在这里可以理解为是发送投票和接受投票的队列,同时这里还新建了一个Messager对象,同样的看一眼是怎么回事:
在这里插入图片描述
可以看到,这里启动了一个新的名字为WorkerSender的守护线程,从worker可以看出其是用来执行实际消息发送的。同时也启动了一个workerReciver的守护线程,用来接受其他服务器传递来的消息,其实也如我们之前接触的信息一样,这里其实是:::接受投票信息,因为这个过程目前还是处在选举过程之中。这里可以看到FastLeaderElection内部也是开启了两个线程负责读写,这里需要跟前面Listener的逻辑结合考虑。Listener开启的线程一个负责读取数据放入队列,一个负责把队列中的数据发出去,但读取的数据给谁用呢?发送的数据是哪来的呢?FastLeaderElection里的两线程就是跟它们交互的。

选票的交互:

在这里插入图片描述
可看到与执行逻辑最相关的就是run方法中的process方法,这里也看到了我们经常用的一种停止线程的方式,使用voilate关键字,然后通过这个boolean变量来查看和改变线程的状态。这也为平时工作中如有相关的工作内容,那就提供了一个参考。可以发现,一直到执行到run方法,本质上sendQueueu里面并没有ToSend类型的消息,因为我们没看到过由push/add之类的操作。假设sendqueue队列中有了对应的消息,那看下process方法:
在这里插入图片描述
其实也就是调用了一步步传递过来的manager的toSend方法,消息则是由前面提到的状态,leader以及选举轮次等。这些信息的载体是ToSend的对象,所以ToSend类应该定义了这些状态信息,同时也应该注明消息是属于哪一类型的消息,说白了,本质上process方法就是一个序列化消息+发送数据的过程:
在这里插入图片描述
不过需要注意的是这个类是个内部静态类,本质上这些字段是保存在FastElection类的相应的字段中,其作用就是一个类似于消息载体,其内部也没有定义太多的方法和属性。下面为其新增的属性,可以发现这些新增的与选举有关,同样在发送过程中也发送了当前的状态以及选举相关的内容,如轮次等。进入到toSend方法一看究竟:
在这里插入图片描述
本质上FastLeaderElection中进行选举广播投票信息时,将投票信息写入到对端服务器大致流程如下:

  • 第一步,将数据封装成ToSend格式放入到sendqueue,这个队列是sendworker维护的,而sendworker线程则存在于sid, sendworker键值对的queueSendMap中;
  • WorkerSender线程会一直轮询提取sendqueue中的数据,其实本质上是将消息包装成ToSend类型的消息,workerSender提取到ToSend数据后,会获取到集群中所有参与Leader选举节点,这些节点其实是来源于在zoo.cfg中配置的,由QuorumPeerConfig类进行传递的,(除Observer节点外的节点)的sid,如果sid即为本机节点,则转成Notification直接放入到recvqueue中,因为本机不再需要走网络IO,这就验证了zookeeper在选举投票过程中第一步就是先给自己投票这个过程;若这个发送的节点是集群中的其他节点,也就是sid是其他的服务器id,则放入到queueSendMap中维护的阻塞队列中,队列中的保存的数据结构为<sid, bytebuffer>的形式的内容,key是要发送给哪个服务器节点的sid,ByteBuffer即为ToSend的内容,queueSendMap维护的着当前节点要发送的网络数据信息,为了应对发送到同一个sid服务器可能存在多条数据的场景,其实每个queueSendMap的value是一个queue类型,其实等待发送的消息的容器是一个队列queue;
  • QuorumCnxManager中的SendWorker线程不停轮询queueSendMap,确认其中是否存在自己要发送的数据,因为存在与sendMap中的每个SendWorkder线程都会绑定一个sid,所以SendWorkder线程可以和对应sid的端服务器进行通信,因此,queueSendMap.get(sid)即可获取该线程要发送数据的queue,也就是上面提到的发送消息的queue,然后通过queue.poll()即可提取该线程要发送的数据内容,成功发送后将此消息放入最近成功的已提交事务的队列,毕竟这些都是基于事务请求完成的。然后通过调用SendWorkder内部维护的socket输出流即可将数据写入到对端服务器。

那再接着看下WorkerReciver的run方法(部分参考https://zhuanlan.zhihu.com/p/141522620):

public void run() {
    Message response;
    while (!stop) {
        try {
            //这里本质上是从recvQueue里取出数据
            response = manager.pollRecvQueue(3000, TimeUnit.MILLISECONDS);
            //没有数据则继续等待
            if(response == null) continue;
			...
            int rstate = response.buffer.getInt();
            long rleader = response.buffer.getLong();
            long rzxid = response.buffer.getLong();
            long relectionEpoch = response.buffer.getLong();
            long rpeerepoch;
            QuorumVerifier rqv = null;
            //如果不是一个有投票权的节点,例如Observer节点
            if(!validVoter(response.sid)) {
                //直接把自己的投票信息返回
                Vote current = self.getCurrentVote();
                QuorumVerifier qv = self.getQuorumVerifier();
                ToSend notmsg = new ToSend(ToSend.mType.notification,
                        current.getId(),
                        current.getZxid(),
                        logicalclock.get(),
                        self.getPeerState(),
                        response.sid,
                        current.getPeerEpoch(),
                        qv.toString().getBytes());
                sendqueue.offer(notmsg);
            } else {
                //获取发消息的节点的状态
                QuorumPeer.ServerState ackstate = QuorumPeer.ServerState.LOOKING;
                switch (rstate) {
                case 0:
                    ackstate = QuorumPeer.ServerState.LOOKING;
                    break;
                case 1:
                    ackstate = QuorumPeer.ServerState.FOLLOWING;
                    break;
                case 2:
                    ackstate = QuorumPeer.ServerState.LEADING;
                    break;
                case 3:
                    ackstate = QuorumPeer.ServerState.OBSERVING;
                    break;
                default:
                    continue;
                }

                //赋值Notification
                n.leader = rleader;
                n.zxid = rzxid;
                n.electionEpoch = relectionEpoch;
                n.state = ackstate;
                n.sid = response.sid;
                n.peerEpoch = rpeerepoch;
                n.version = version;
                n.qv = rqv;
                //如果当前节点正在寻找Leader
                if(self.getPeerState() == QuorumPeer.ServerState.LOOKING){
                    //把收到的消息加入队列
                    recvqueue.offer(n);
                    //如果对方节点也是LOOKING状态,且周期小于自己,则把自己投票信息发回去
                    if((ackstate == QuorumPeer.ServerState.LOOKING) && (n.electionEpoch < logicalclock.get())){
                        Vote v = getVote();
                        QuorumVerifier qv = self.getQuorumVerifier();
                        ToSend notmsg = new ToSend(ToSend.mType.notification,
                                v.getId(),
                                v.getZxid(),
                                logicalclock.get(),
                                self.getPeerState(),
                                response.sid,
                                v.getPeerEpoch(),
                                qv.toString().getBytes());
                        sendqueue.offer(notmsg);
                    }
                } else {
                    //如果当前节点不是LOOKING状态,那么它已经知道谁是Leader了
                    Vote current = self.getCurrentVote();
                    //如果对方是LOOKING状态,那么就把自己认为的Leader信息返给对方
                    if(ackstate == QuorumPeer.ServerState.LOOKING){
                        QuorumVerifier qv = self.getQuorumVerifier();
                        ToSend notmsg = new ToSend(
                                ToSend.mType.notification,
                                current.getId(),
                                current.getZxid(),
                                current.getElectionEpoch(),
                                self.getPeerState(),
                                response.sid,
                                current.getPeerEpoch(),
                                qv.toString().getBytes());
                        sendqueue.offer(notmsg);
                    }
                }
            }
        } catch (InterruptedException e) {
        }
    }
}

从上面代码可看出,FastLeaderElection中进行选举广播投票信息时,从对端服务器读取投票信息的大致流程如下:

  • QuorumCnxManager中的RecvWorker线程会一直从Socket的输入流中读取数据,当读取到对端发送过来的数据时,转成Message格式并放入到recvQueue中;
  • FastLeaderElection.WorkerReceiver线程会轮询方式从recvQueue提取数据并转成Notification格式放入到recvqueue中;
  • FastLeaderElection从recvqueue提取所有的投票信息进行比较 最终选出一个Leader。

Leader的选举:

到上面,我们了解了投票信息的接受与发送,但还未有leader选举的过程,那么这个过程在那里呢?其实,在QuorumPeer线程中会有一个Loop循环,获取serverState状态后进入不同分支,当分支退出后继续下次循环,FastLeaderElection选举策略调用就是发生在检测到serverState状态为LOOKING时进入到LOOKING分支中调用的。

case LOOKING:
	LOG.info("LOOKING");
	ServerMetrics.getMetrics().LOOKING_COUNT.add(1);

	if (Boolean.getBoolean("readonlymode.enabled")) {
		LOG.info("Attempting to start ReadOnlyZooKeeperServer");

		// Create read-only server but don't start it immediately
		final ReadOnlyZooKeeperServer roZk = new ReadOnlyZooKeeperServer(logFactory, this, this.zkDb);

		// Instead of starting roZk immediately, wait some grace
		// period before we decide we're partitioned.
		//
		// Thread is used here because otherwise it would require
		// changes in each of election strategy classes which is
		// unnecessary code coupling.
		Thread roZkMgr = new Thread() {
			public void run() {
				try {
					// lower-bound grace period to 2 secs
					sleep(Math.max(2000, tickTime));
					if (ServerState.LOOKING.equals(getPeerState())) {
						roZk.startup();
					}
				} catch (InterruptedException e) {
					LOG.info("Interrupted while attempting to start ReadOnlyZooKeeperServer, not started");
				} catch (Exception e) {
					LOG.error("FAILED to start ReadOnlyZooKeeperServer", e);
				}
			}
		};
		try {
			roZkMgr.start();
			reconfigFlagClear();
			if (shuttingDownLE) {
				shuttingDownLE = false;
				startLeaderElection();
			}
			setCurrentVote(makeLEStrategy().lookForLeader());
		} catch (Exception e) {
			LOG.warn("Unexpected exception", e);
			setPeerState(ServerState.LOOKING);
		} finally {
			// If the thread is in the the grace period, interrupt
			// to come out of waiting.
			roZkMgr.interrupt();
			roZk.shutdown();
		}
	} else {
		try {
			reconfigFlagClear();
			if (shuttingDownLE) {
				shuttingDownLE = false;
				***startLeaderElection();***
			}
			setCurrentVote(makeLEStrategy().lookForLeader());
		} catch (Exception e) {
			LOG.warn("Unexpected exception", e);
			setPeerState(ServerState.LOOKING);
		}
	}
	break;

从上面代码可以看出,Leader选举策略入口方法为:FastLeaderElection.lookForLeader()方法。当QuorumPeer.serverState变成LOOKING时,该方法会被调用,表示执行新一轮Leader选举。下面来看下lookForLeader方法的大致实现逻辑:

1、更新自己期望投票信息,即自己期望选哪个服务器作为Leader(用sid代替期望服务器节点)以及该服务器zxid、epoch等信息,第一次投票默认都是投自己当选Leader,然后调用sendNotifications方法广播该投票到集群中所有可以参与投票服务器,代码如下:

synchronized (this) {
	logicalclock.incrementAndGet();
    // 可以看到,在这里提出了propsoal,这也就对应了zookeeper选举过程中的提出提议proposal
	updateProposal(getInitId(), getInitLastLoggedZxid(), getPeerEpoch());
}

LOG.info(
	"New election. My id = {}, proposed zxid=0x{}",
	self.getId(),
	Long.toHexString(proposedZxid));
// 发送提示消息
sendNotifications();

logicalclock维护electionEpoch,即选举轮次。在进行投票结果赛选的时候需要保证大家在一个投票轮次 updateProposal()方法有三个参数:a.期望投票给哪个服务器(sid)、b.该服务器的zxid、c.该服务器的epoch,在后面会看到这三个参数是选举Leader时的核心指标,其实这也验证了我们了解的zookeeper选举过程中所需要的三个信息,sid:目标服务器,轮次和事务zxid,在上述代码中,getInitId()用于获取当前myid,getInitLastLoggedZxid()提取lastProcessedZxid值,lastProcessedZxid是最后一次commit的事务请求的zxid,getPeerEpoch():获取epoch值,每个leader任期内都要有一个epoch代表该Leader轮次,同时把该epoch同步到集群送的所有其它节点,并会被保存到本地硬盘dataLogDir目录下currentEpoch文件中,这里的getPeerEpoch()就是获取最近一次Leader的epoch,如果是第一次部署启动则默认从0开始。其实这里就可以和一开始启动的loadDataBase联系起来了,因为在start方法中,选取开始之前有一步loadDataBase,这一步load将持久化在dataLog中epoch,可以和当前的currentEpoch进行比较,查看当前epoch和读取的epoch的关系,发送给集群中所有可参与投票节点,注意也包括自身节点:

  • 将proposedLeader、proposedZxid、electionEpoch、peerEpoch、sid(要发送给哪个节点的sid)等信息封装为一个ToSend对象(可以查看上文中展示的ToSend对象),并放入到LinkedBlockingQueue sendqueue队列中,注意遍历集群中所有参与投票节点的sid,为每个sid封装成一个ToSend
  • WorkerSender线程将会从sendqueue队列中获取要发送消息根据sid发送给集群中指定的节点

2、然后就开始等待其它服务器发送给自己的投票信息

3、将接收到投票的state进行判断确定执行哪个分支逻辑,其实本质上是基于传递过来的ToSend消息中的ServerState来判定该作出什么样的处理:**(1)如果是FOLLOWING或LEADING,则说明对端已选举出Leader,这时只需要验证下这个Leader是否有效即可,有效则代表选举结束,否则继续接收投票信息。(2)如果是OBSERVING:忽略该投票信息,因为Observer不能参与投票。(3)如果是LOOKING:则表示对端也还处于Leader选举状态。**LOOKING状态,换句话来说就是统计有效的票数,通过判断轮次以及事务zxid等来判断收到的票是否是有效的,然后根据判断结果作出不同的处理:

case LOOKING:
	if (getInitLastLoggedZxid() == -1) {
		LOG.debug("Ignoring notification as our zxid is -1");
		break;
	}
	if (n.zxid == -1) {
		LOG.debug("Ignoring notification from member with -1 zxid {}", n.sid);
		break;
	}
	// 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()) {
			LOG.debug(
				"Notification election epoch is smaller than logicalclock. n.electionEpoch = 0x{}, logicalclock=0x{}",
				Long.toHexString(n.electionEpoch),
				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();
	}

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

	// don't care about the version if it's in LOOKING state
	recvset.put(n.sid, new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch));

	voteSet = getVoteTracker(recvset, new Vote(proposedLeader, proposedZxid, logicalclock.get(), proposedEpoch));

	if (voteSet.hasAllQuorums()) {

		// 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) {
			setPeerState(proposedLeader, voteSet);
			Vote endVote = new Vote(proposedLeader, proposedZxid, logicalclock.get(), proposedEpoch);
			leaveInstance(endVote);
			return endVote;
		}
	}
	break;

首先对之前提到的选举轮次electionEpoch进行判断,这里分为三种情况,三种情况分别对应了对于有效投票以及无效投票的的处理方式:

  • (1)只有对方发过来的投票的electionEpoch与当前节点处于同一轮的投票,换句话说,对方的electionEpoch和currentEpoch喜相等,这表示是同一轮投票,即投票有效,然后调用totalOrderPredicate()对投票进行处理,这时候,在totalOrderPredicate方法中处理的投票可理解为是有效的投票。若返回true代表发给自己投票的一端胜出,这也相应的纠正了一个问题即第一次投票是错误的(第一次都是投给自己)。此时,开始更新自己投票期望,将自己的投票投给发送方的服务器(对端)作为Leader,然后调用再次发用提示消息Notification类型的ToSend,并通过sendNotifications()将自己最新的投票广播出去。返回false则代表自己胜出,第一次投票没有问题,那就继续保持当前的保投票结果,将自己选择为leader。
  • (2)如果对端发过来的electionEpoch大于自己,则表明重置自己的electionEpoch,然后清空之前获取到的所有投票recvset,因为之前获取的投票轮次落后于当前,这其实说明了之前的投票已经无效了,因为已经处于不同的选举轮次了,就好比,当前是选特朗普和拜登,而我还停留在选奥巴马和希拉里,所以为了保持最新的投票过程,则需要进行上述的操作。然后调用totalOrderPredicate()将当前期望的投票和对端投票进行PK,用胜出者更新当前期望投票,然后调用sendNotifications()将自己期望投票广播出去。注意:这里不管哪一方胜出,都需要广播出去,而不是步骤a中己方胜出不需要广播,这是因为由于electionEpoch落后导致之前发出的所有投票都是无效的,所以这里需要重新发送。
  • (3)如果对端发过来的electionEpoch小于自己,则表示对方投票无效,直接忽略不进行处理。其实还是处在哪一届“美国大选”的问题。

选举投票的处理机制

选举投票的处理机制其实就是选票的处理过程,本质上就是选票的PK过程,下面来看下totalOrderPredicate方法:

protected boolean totalOrderPredicate(long newId, long newZxid, long newEpoch, long curId, long curZxid, long curEpoch) {
	LOG.debug(
		"id: {}, proposed id: {}, zxid: 0x{}, proposed zxid: 0x{}",
		newId,
		curId,
		Long.toHexString(newZxid),
		Long.toHexString(curZxid));

	if (self.getQuorumVerifier().getWeight(newId) == 0) {
		return false;
	}

	/*
	 * We return true if one of the following three cases hold:
	 * 1- New epoch is higher
	 * 2- New epoch is the same as current epoch, but new zxid is higher
	 * 3- New epoch is the same as current epoch, new zxid is the same
	 *  as current zxid, but server id is higher.
	 */

	return ((newEpoch > curEpoch)
			|| ((newEpoch == curEpoch)
				&& ((newZxid > curZxid)
					|| ((newZxid == curZxid)
						&& (newId > curId)))));
}

其实上面的逻辑和我们之前接触的zookeeper的投票的处理方法是一致的。这个PK逻辑原理(胜出一方代表更有希望成为Leader)如下:1、首先比较epoch,哪个epoch大哪个胜出,前面介绍过epoch代表了Leader的轮次,是一个递增的,epoch越大就意味着数据越新,Leader数据越新则可以减少后续数据同步的效率,当然应该优先选为Leader。

2、然后才是比较zxid,由于zxid=epoch+counter,第一步已经把epoch比较过了,其实这步骤只是相当于比较counter大小,counter越大则代表数据越新,优先选为Leader。注:其实第1和第2可以合并到一起,直接比较zxid即可,因为zxid=epoch+counter,第1比较显的有些多余。

3、如果前两个指标都没法比较出来,只能通过sid来确定,zxid相等说明两个服务器的数据是一致的,所以选哪个当Leader其实没有区别,这里就随机选择一个sid大的当Leader。

所以到此我们基本可以完成一次Main loop中的选举的流程了,应为在Main loop中是不断的去循环处理,所以随着多次的处理,集群中的服务器会不断的更新自己的状态,承担起自己的角色,如follower,leader等。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值