Zookeeper 源码解读系列,集群模式(七)

前情提要

上一篇博客我们提到SendWorkerRecvWorker这两个线程形成了两个链条,在选举过程中处于收发数据的重要位置。但是我们留了两个问题:1. queueSendMap这里的数据从何而来,2. recvQueue收到的数据是谁来使用的?本篇主要就是来处理这两个问题。本篇也会被收录到【Zookeeper 源码解读系列目录】中。

FastLeaderElection

上篇我们讲解过SendWorkerRecvWorker两个线程的作用。而这两个线程是在Listener这个线程中运行的,所以我们还是要一步一步的返回去。这两个线程走完以后只有一个return;,那么就是说Listener这个线程的run()方法已经走完了。所以我们回到QuorumPeer. createElectionAlgorithm()方法中,接着listener.start()继续看。

case 3:
    qcm = createCnxnManager();//初始化负责各个服务器之间的底层leader选举过程中的网络通信
    QuorumCnxManager.Listener listener = qcm.listener; //取出listener
    if(listener != null){
        listener.start(); //启动listener线程
        le = new FastLeaderElection(this, qcm); //硕果仅存的算法类
    } else {
        LOG.error("Null listener when initializing cnx manager");
    }
    break;

既然只剩下硕果仅存的算法类可以作为线索,我们也只能通过解析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);
}

进入构造方法,就有看到两个queue:sendqueuerevcqueue,以及一个Messenger类实例对象的构造。那我们点进Message类:

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();
}

WorkerSender线程

发现里面又启动了两个线程WorkerSenderWorkerReceiver。我们还是一个一个来看,既然启动了,呢吗先点进去WorkerSender.run()看里面跑了什么东西:

public void run() {
    while (!stop) {
        try {
            ToSend m = sendqueue.poll(3000, TimeUnit.MILLISECONDS);
            if(m == null) continue;
            process(m);
        } catch (InterruptedException e) {
            break;
        }
    }
    LOG.info("WorkerSender is down");
}

看到几乎没有什么代码,在这里取出了sendqueue里面的内容ToSend m = sendqueue.poll(3000, TimeUnit.MILLISECONDS)),然后调用了process(m)方法处理:

void process(ToSend m) {
    ByteBuffer requestBuffer = buildMsg(m.state.ordinal(), 
                                            m.leader,
                                            m.zxid, 
                                            m.electionEpoch, 
                                            m.peerEpoch);
    manager.toSend(m.sid, requestBuffer);
}

这里面更简单,只有一行manager.toSend(m.sid, requestBuffer)。这个manager又是什么呢,找到声明发现是QuorumCnxManager这个类的对象,那我们继续进入toSend(m.sid, requestBuffer)方法:

public void toSend(Long sid, ByteBuffer b) {
    //如果是发给自己的,则不需要经过网络,自己转成Message,并加入到recvQueue即可
    if (this.mySid == sid) {
         b.position(0);
         addToRecvQueue(new Message(b.duplicate(), sid));
    } else {
         //如果不是发给自己的,发送给其他服务器,加入到服务器所对应的发送队列中
         ArrayBlockingQueue<ByteBuffer> bq = new ArrayBlockingQueue<ByteBuffer>(SEND_CAPACITY);
         ArrayBlockingQueue<ByteBuffer> bqExisting = queueSendMap.putIfAbsent(sid, bq);
         if (bqExisting != null) {
             addToSendQueue(bqExisting, b);
         } else {
             addToSendQueue(bq, b);
         }
         //如果还没有连接到这个服务器,就去连接一下
         connectOne(sid);
    }
}

方法里面只有一个逻辑判断if (this.mySid == sid)这里是说如果是发给自己的投票,则不需要经过网络,自己转成Message,加入到recvQueue即可。接着看else的逻辑:如果不是发给自己的,是发送给其他服务器的。就应该加入到服务器所对应的发送队列中,这里就发现了queueSendMap。这里逻辑也很简单,其实就是把要发的数据放入queueSendMap中。那么到这里我们的第一个问题就回答出来了:queueSendMap中的数据就是来自sendqueue,被WorkerSender线程从sendqueue读出来放进去的。

WorkerReceiver线程

说完WorkerSender线程,我们回头看WorkerReceiver线程,进入run()方法:

public void run() {
    Message response;
    while (!stop) {
        // Sleeps on receive
        try{
            response = manager.pollRecvQueue(3000, TimeUnit.MILLISECONDS);
            if(response == null) continue;
            /**暂时略,代码很多,后面讲**/
        } catch (****Exception e) {
           /**Exceptions**/
        }
    }
    LOG.info("WorkerReceiver is down");
}

进入后看到,首先又是response = manager.pollRecvQueue(3000, TimeUnit.MILLISECONDS);拿数据。如果拿出来的数据response == null继续循环continue;,如果不为null又有一大堆的逻辑。到这里就很明显就是处理消息的逻辑了,因为数据拿出来肯定就是要处理的嘛,这里的代码具体什么用的先不解释。我们先看从哪里拿的数据,进入pollRecvQueue(3000, TimeUnit.MILLISECONDS);这个方法:

public Message pollRecvQueue(long timeout, TimeUnit unit) throws InterruptedException {
   return recvQueue.poll(timeout, unit);
}

一进去就发现了我们的recvQueue.poll(timeout, unit),很明显WorkerReceiver这个线程就是取数据的线程,而且是从recvQueue里面取出来数据的。到这里我们的第二个问题也有答案了:WorkerReceiver就是消费recvQueue内容的线程。注意这两个queue,都是小写的,和刚才首字母大写的用处是不一样的,一定要区别开。这里先剧透一下,WorkerReceiver线程会把消息根据逻辑的不同一部分放到sendqueue一部分放到revcqueue里,这部分就是在我略去的代码里,一会儿我们看。那么我们到目前可以有一个合理的推断:这两个队列sendqueuerevcqueue的后面,就是领导者选举的算法的核心部分,真正的算法部分。总上所述,我们可以有一个这样的概念图:
在这里插入图片描述

记着这个图,我们接下来还会多次引用。领导者选举算法只要把要发送的投票发送到sendqueue这个队列里可以了,后面就不需要在管了。而取投票只要去revcqueue里面取就代表了本机从别人那里取到了投票。

QuorumPeer.LOOKING

到这里我们就可以往上返回了,返回到最初我们进入的方法startLeaderElection()这里继续往下走。大家还记得要返回到哪里吗?问个更远的问题,大家还记得我们是从哪里进来的吗?看懵了的同学基本上就找不到了,笔者看源码的时候也是一样,不用担心,提醒一下:

----入口----> QuorumPeerMain.main();
----转到----> QuorumPeerMain.initializeAndRun(args);
----转到----> QuorumPeerMain.runFromConfig(config);
----转到----> quorumPeer.start();

我们要返回去的方法就是QuorumPeer.start()方法:

public synchronized void start() {
    loadDataBase();
    cnxnFactory.start(); 
    startLeaderElection(); //跳出来,继续走
    super.start(); 
}

下一步就是super.start();,意思就是说QuorumPeer这个线程类的主线程终于要开始动了。其实这个线程也是我们的老朋友了,基本上已经快讲一个遍了。那么我们现在去QuorumPeer.run()方法里看最后一个还没有说的分支Looking分支。在之前的博客里,我们说了LOOKING状态,代表还没有确定好角色,就是正在进行领导者选举,所以领导者选举的方法应该就是在这里:

public void run() {
	/**已经讲过,略**/
    try {
        while (running) {
            switch (getPeerState()) {
            case LOOKING:
                LOG.info("LOOKING");
                if (Boolean.getBoolean("readonlymode.enabled")) {
                    /**只读模式,无关紧要,略**/
                } else {
                    try {
                        setBCVote(null);
                        //真正关心的方法在lookForLeader()
                        setCurrentVote(makeLEStrategy().lookForLeader());
                    } catch (Exception e) {
                        LOG.warn("Unexpected exception", e);
                        setPeerState(ServerState.LOOKING);
                    }
                }
                break;
            case OBSERVING:  /**已经讲过,略**/ break;
            case FOLLOWING: /**已经讲过,略**/  break;
            case LEADING: /**已经讲过,略**/ break;
            }
        }
    } finally {
        /**已经讲过,略**/
    }
}

投票给自己

在集群刚刚启动的时候,由于getPeerState()中的status默认的就是LOOKING状态,所以一台机器启动的时候就会被switch语句分配进入case LOOKING:里去。跳过只读模式部分的内容,真正关心的方法就是lookForLeader()。点进去发现这是个接口,找到FastLeaderElection类对这个方法的实实现方法,代码很多我们还是分段讲解:

public Vote lookForLeader() throws InterruptedException {
    /**初始化,略**/
    try {
        HashMap<Long, Vote> recvset = new HashMap<Long, Vote>(); //投票箱map
        HashMap<Long, Vote> outofelection = new HashMap<Long, Vote>();
        int notTimeout = finalizeWait;
        //启动时先投票给自己,然后发送出去
        synchronized(this){
            logicalclock.incrementAndGet();//时钟+1,记录自己经历了几轮投票
            //更新提议,包含(myid,lastZxid,epoch),更新成为自己
            updateProposal(getInitId(), getInitLastLoggedZxid(), getPeerEpoch());
        }
        LOG.info("New election. My id =  " + self.getId() + ", proposed zxid=0x" + Long.toHexString(proposedZxid));
        sendNotifications();   //发送自己的投票
        while ((self.getPeerState() == ServerState.LOOKING) && ......
        /**暂时略,后面讲**/
}

跳过一些初始化,找到recvset = new HashMap<Long, Vote>(),这个map就是我们提到的投票箱的那个箱子,Key(Long)代表其他服务器,Value(Vote)代表该服务器投的票。如果有更新,按照key来更新就好了。往下我们看到一个synchronized包围的代码,这里其实就是我们说的一台机器首先投票给自己的逻辑,logicalclock.incrementAndGet()用来记录自己投票轮数然后加1(首次启动,初始化为1)。紧接着updateProposal(getInitId(), getInitLastLoggedZxid(), getPeerEpoch())更新投票的提议,包含的内容有myid(自己的机器代号)、lastZxid(自己机器的最新事务id)、epoch(自己这里是第几届),其实也就是说设置投的票为自己的内容。这里只是设置了内容,后面发送自己的投票sendNotifications()会用到。所以我们进入进入sendNotifications()方法:

private void sendNotifications() {
    for (QuorumServer server : self.getVotingView().values()) {
        long sid = server.id;
        //new一个发送的内容
        ToSend notmsg = new ToSend(ToSend.mType.notification,
                proposedLeader,
                proposedZxid,
                logicalclock.get(),
                QuorumPeer.ServerState.LOOKING,
                sid,
                proposedEpoch);
        /**Log4j**/
        sendqueue.offer(notmsg); //放入发送队列
    }
}

进入方法以后,立即就看到了一个for循环,而且是循环QuorumServer的。很明显这个循环就是发现那些能够投票的机器的。然后new了一个ToSend再放入发送队列sendqueue.offer(notmsg)。那么根据上面的分析,这个循环会不断地生成投给自己的投票,生成的数目就是配置的有选举权的机器数目,当然也包括自己。也就是说要给所有的机器发送投给自己的票。第一个步骤投给自己的逻辑就走完了。

接收集群中其他机器的投票

跳出sendNotifications()走到下面的while ((self.getPeerState() == ServerState.LOOKING) && (!stop))里面:

public Vote lookForLeader() throws InterruptedException {
    /**初始化,略**/
    try {
        /**已经讲过,略**/
        sendNotifications(); //发送自己的投票
		//while会不停的拿别人发来的票
        while ((self.getPeerState() == ServerState.LOOKING) &&(!stop)){
            Notification n = recvqueue.poll(notTimeout,TimeUnit.MILLISECONDS); //获取其他服务器的投票
            if(n == null){//获取的选票是空的,说明没有收到别的服务器发来的数据
                if(manager.haveDelivered()){//看下是否有未发送的数据
                    sendNotifications();//重新发送选票
                } else {
                    manager.connectAll(); //重新连接
                }
                int tmpTimeOut = notTimeout*2;
                notTimeout = (tmpTimeOut < maxNotificationInterval? tmpTimeOut : maxNotificationInterval);
                LOG.info("Notification time out: " + notTimeout);
            }
            else if(validVoter(n.sid) && validVoter(n.leader)) {......
            /**暂时略,后面讲**/
}

如果服务器还在运行,且状态是Looking就会再往下走。就看到了一个很熟悉的队列recvqueue,并且从里面取出了Notification n = recvqueue.poll((notTimeout, TimeUnit.MILLISECONDS)其他服务器的投票。

但是要注意一点:首次启动的时候这里并不会接到其他的服务器发来的数据。但是其实也还会有数据的,因为这个数据就是自己投票给自己的数据。不需要经过网络,WorkSender这个线程就直接调用方法把自己的票放到了recvQueue中。然后(概念图)就又流回到了recvqueue里,这里还要注意WorkSender也会把要发往别的服务器的投票从sendqueue取出放到queueSendMap中,这里放的当然就是配置中的需要经过网络的要到其他服务器的投票。

对选票的验证

当然这里的Notification n就是取到的结果,也就是我们说的别的服务器发来的选票。紧接着是一个判断if(n == null),获取的选票是空的。说明没有收到别的服务器发来的数据。如果没有收到别的服务器发来的票,那么基本上有两种情况:第一是不是我的票没有发出去;第二发出去了但是没有收到回复。manager.haveDelivered()就是判断这一点的:

boolean haveDelivered() {
    //循环每个需要发送给所有服务器的队列,如果发现里面有数据,说明投票没有发出去,那么就返回false
    for (ArrayBlockingQueue<ByteBuffer> queue : queueSendMap.values()) {
        LOG.debug("Queue size: " + queue.size());
        if (queue.size() == 0) {
            return true;
        }
    }
    return false;
}

这个方法里会循环queueSendMap。如果发现里面有数据,说明投票没有发出去,那么就返回false。如果发现里面有queue没有数据queue.size()==0说明,我的这个票发成功了。但是只要进入这个if (queue.size() == 0)就说明,在外面我拿到的选票n一定是null,意味着我肯定没有得到回复,返回true,再次发送这个数据。

所以当if(manager.haveDelivered())true的时候,就调用sendNotifications()重新发送选票。因为既没有别人来的选票,而我发的选票也没有得到其他机器的回复,那么我就把我配置的选票再发送一次,再次等待各个目标机的回复。如果进入了else又是什么情况呢?说明queueSendMap这个map里面有数据,意味着我的票没有发出去。那么可能的原因就是还没有建立连接,调用manager.connectAll()连接所有配置的服务器:

public void connectAll(){
    long sid;
    //找到没有发出去的数据
    for(Enumeration<Long> en = queueSendMap.keys(); en.hasMoreElements();){
        sid = en.nextElement();
        connectOne(sid);//连接
    }      
}

进入这里以后,可以看到服务器的id是从queueSendMap取出来的。取得是queueSendMap.keys(),就是说哪台的没有发出去,就去连接哪台connectOne(sid),我们再深一步进入:

synchronized public void connectOne(long sid){
    if (!connectedToPeer(sid)){//判断如果真的没有连接,才回去重新连接
        InetSocketAddress electionAddr;
        if (view.containsKey(sid)) {
            electionAddr = view.get(sid).electionAddr; //获取选举地址
        } else {
            LOG.warn("Invalid server id: " + sid);
            return;
        }
        try {
            LOG.debug("Opening channel to server " + sid);
            Socket sock = new Socket();  //建立socket
            setSockOpts(sock);
            sock.connect(view.get(sid).electionAddr, cnxTO);
            LOG.debug("Connected to server " + sid);
            if (quorumSaslAuthEnabled) {
                initiateConnectionAsync(sock, sid);
            } else {
                initiateConnection(sock, sid);   //连接好了以后初始化
            }
        } catch (***Exception e) {
        	/**Exceptions**/
        } 
    } else {
        LOG.debug("There is a connection already for server " + sid);
    }
}

进入以后就看见一个判断if (!connectedToPeer(sid)) 如果真的没有连接,才回去重新连接。然后获取地址建立socket,初始化initiateConnection(sock, sid),点进去:

public void initiateConnection(final Socket sock, final Long sid) {
    try {
        //开始链接
        startConnection(sock, sid);
    } catch (IOException e) {
        LOG.error("Exception while connecting, id: {}, addr: {}, closing learner connection",
                 new Object[] { sid, sock.getRemoteSocketAddress() }, e);
        closeSocket(sock);
        return;
    }
}

只有一个开始连接的方法startConnection(sock, sid),这里面就是一样的逻辑了if (sid > this.mySid)连接的控制,新建SendWorkerRecvWorker然后启动等等,这样就完成了connectAll()

所以我们就清楚了,if(n == null)这一块的逻辑其实就是一个保险机制,用来检验投票的发送和连接成功与否用的。那么验证过了n!=null就是选举的核心逻辑了,到这里说明收到了外来的选票而且合法。服务器就可以放心大胆的进行领导者选举,选出一个合格的Leader了。

总结

到这里,我们先停一停,后面还有不少的逻辑,放到一篇里内容太过冗长了。那么我们总结下目前Zookeeper准备选举前干了什么事情。首先当然还是要开启socket,其次启动了SendReceive线程构成了两个投票收发链,然后投票给自己,接着验证选票。如果发现没有收到选票,就重新发送。如果发现选票没有发出去,就检查连接,重新尝试连接集群里的各个机器。那么本章就到此结束,我们下一篇接着探索选举的过程,谢谢大家。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值