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

前情提要

上篇我们说到leader的处理器链的逻辑,当leader发送了一个事务的提议以后需要等待followerack响应才可以继续走下去,否则就会在CommitProcessor这里阻塞住,无法提交这个事务。所以本篇就接着上一篇博客继续探究,当follower端收到这个提议proposal以后,会走什么样的流程。本篇也会被收录到【Zookeeper 源码解读系列目录】中。

Follower接收Proposal

SyncRequestProcessor & SendAckRequestProcessor

我们在【集群模式三】的最后说follower会在自己的线程里用一个while (this.isRunning())不断地接收来自leader的数据,当时笔者贴的代码如下:

syncWithLeader(newEpochZxid);//同步数据
QuorumPacket qp = new QuorumPacket();
while (this.isRunning()) {
    readPacket(qp);//监听leader给我的信息
    processPacket(qp);//处理这个信息
}

其实,上一篇所说的发送给follower的提议就是在这里被接收的。那么这一段代码在哪大家还记得吗?是在Follower.followLeader()里面,记不得的同学可以回去看一下。那么我们要去的方法就一定是processPacket(qp)这个里面了:

protected void processPacket(QuorumPacket qp) throws IOException{
    switch (qp.getType()) {
    case Leader.PING:            
        ping(qp);            
        break;
    case Leader.PROPOSAL:            //接收提议
        TxnHeader hdr = new TxnHeader();
        Record txn = SerializeUtils.deserializeTxn(qp.getData(), hdr);//取出数据
        if (hdr.getZxid() != lastQueued + 1) {
           /**Log4j**/
        }
        lastQueued = hdr.getZxid();
        fzk.logRequest(hdr, txn); //重点
        break;
    case Leader.COMMIT:
        fzk.commit(qp.getZxid());
        break;
    case Leader.UPTODATE:
        LOG.error("Received an UPTODATE message after Follower started");
        break;
    case Leader.REVALIDATE:
        revalidate(qp);
        break;
    case Leader.SYNC:
        fzk.sync();
        break;
    default:
        LOG.error("Invalid packet type: {} received by Observer", qp.getType());
    }
}

我们找到case Leader.PROPOSAL这个分支,首先反序列化取出数据txn,然后调用fzk.logRequest(hdr, txn);这个方法处理数据,所以进入这个方法:

public void logRequest(TxnHeader hdr, Record txn) {
    Request request = new Request(null, hdr.getClientId(), hdr.getCxid(),
            hdr.getType(), null, null);
    request.hdr = hdr;
    request.txn = txn;
    request.zxid = hdr.getZxid();
    if ((request.zxid & 0xffffffffL) != 0) {
        pendingTxns.add(request);
    }
    syncProcessor.processRequest(request);//也是调用了持久化
}

进入以后,这里也没有做什么内容,先把txn这个事务转变成了一个request,然后直接调用持久化的处理器SyncRequestProcessor。我们上一篇说过,在follower端如果SyncRequestProcessor调用成功,也就是持久化操作没问题,接下来的处理器就是SendAckRequestProcessor。沿着链条我们找到这个发送ack的方法,即SendAckRequestProcessor.processRequest(request)

    public void processRequest(Request si) {
        if(si.type != OpCode.sync){
            QuorumPacket qp = new QuorumPacket(Leader.ACK, si.hdr.getZxid(), null, null);
            try {
                learner.writePacket(qp, false);
            } catch (IOException e) {
                LOG.warn("Closing connection to leader, exception during packet send", e);
                try {
                    if (!learner.sock.isClosed()) {
                        learner.sock.close();
                    }
                } catch (IOException e1) {
                    LOG.debug("Ignoring error closing the connection", e1);
                }
            }
        }
    }

进入以后,果然首先构造一个QuorumPacket qp,但是这个qp的操作码是Leader.ACK,然后在调用socket方法learner.writePacket(qp, false);发送出去。这个处理器做的任务可以说的非常简单,就是发送一个ackleader端。

那么这里是什么意思呢,总结上面所讲,其实就是在说明一件事:首先leader接收到一个事务,然后leader进行持久化,leader持久化以后发送一个提议给followerfollower怎么判断这个提议能不能成功呢?也是持久化,follower接到这个提议以后,直接就去持久化。如果follower的持久化成功了,就会调用发送ackprocess,进入SendAckRequestProcessor.processRequest(request);发送一个ack告诉leader"我"持久化没问题可以做这个事务,"我"同意这个提议。

Leader接收提议的ACK

那么到了leader这里,谁去处理的这个消息呢?到这里我们就又得返回LearnerHandle.run()方法里找到while(true)里的ACK分支,要记住我们线程一直再跑。在follower端的while (this.isRunning())leader端的while(true)是一直监听互相的消息的,我们以前说过是配对用的。那么我们去看:

public void run() {
    try {
        /**已经讲过,略**/
        queuedPackets.add(new QuorumPacket(Leader.UPTODATE, -1, null, null));//数据同步结束
        while (true) {
            /**拿数据,略**/
            switch (qp.getType()) {
            case Leader.ACK:
                if (this.learnerType == LearnerType.OBSERVER) {
                    /**Log4j**/
                }
                syncLimitCheck.updateAck(qp.getZxid());
                leader.processAck(this.sid, qp.getZxid(), sock.getLocalSocketAddress());
                break;
            case Leader.PING:
                /**不相关,略**/
                break;
            case Leader.REVALIDATE:
                /**不相关,略**/
                break;
            case Leader.REQUEST:  //接收Follower转发
                /**不相关,略**/
                break;
            default:
                LOG.warn("unexpected quorum packet, type: {}", packetToString(qp));
                break;
            }
        }
    } catch (***Exception e) {
        /**Exceptions**/
    } finally {
        /**Log4j**/
        shutdown();
    }
}

那么我们找到case Leader.ACK:,拿到ack以后就会处理leader.processAck(this.sid, qp.getZxid(), sock.getLocalSocketAddress())。继续进入看:

synchronized public void processAck(long sid, long zxid, SocketAddress followerAddr) {
	/**不相关,略**/
	Proposal p = outstandingProposals.get(zxid); //构造提议
	/**不相关,略**/
    p.ackSet.add(sid);//添加合法的sid
    /**Log4j**/
    if (self.getQuorumVerifier().containsQuorum(p.ackSet)){  //验证过半
        if (zxid != lastCommitted+1) {
            /**Log4j**/
        }
        outstandingProposals.remove(zxid);
        if (p.request != null) {
            toBeApplied.add(p);
        }
        if (p.request == null) {
            LOG.warn("Going to commmit null request for proposal: {}", p);
        }
        commit(zxid);//通知follower提交
        inform(p);//通知observer
        zk.commitProcessor.commit(p.request);
        if(pendingSyncs.containsKey(zxid)){
            for(LearnerSyncRequest r: pendingSyncs.remove(zxid)) {
                sendSync(r);
            }
        }
    }
}

进入以后,略去一些检查的代码。先把事务zxid对应的提议从提议的队列里面初始化出来Proposal p = outstandingProposals.get(zxid);。然后再把follower的服务器id加入进来p.ackSet.add(sid);。也就是说ackSet就是在存储发送ack过来的followersid的,记住这里。我们说过,凡是follower发送ack到leader端都会走到这里,也就是说ackSet会不断的把成功的follower加入进来,为什么要加入呢?当然是为了通过过半机制。我们之前说过提议就是要通过过半验证才能提交。果然紧接着就又发现了过半验证的判断if (self.getQuorumVerifier().containsQuorum(p.ackSet)),验证是不是有过半的follower发回了ack,一旦验证过半通过了就进入if,然后提交commit(zxid)这个事务。

public void commit(long zxid) {
    synchronized(this){
        lastCommitted = zxid;
    }
    QuorumPacket qp = new QuorumPacket(Leader.COMMIT, zxid, null, null); //提交COMMIT
    sendPacket(qp);  //发给follower
}

进入,里面就只有创建打包为一个packet,然后sendPacket(qp)给所有的follower。往下走还有一个方法inform(p);这里就是发送一个INFORM的操作码给对应的observer

public void inform(Proposal proposal) {   
    QuorumPacket qp = new QuorumPacket(Leader.INFORM, proposal.request.zxid, 
                                        proposal.packet.getData(), null); //通知INFORM
    sendObserverPacket(qp); //发给Observer,这里调用的方法都是不同的
}

发送以后,我们的follower自然就可以在while(isrunning)这个循环中监控到,那么再次提醒下这个循环的位置Follower.followerLeader().while(isrunning).processPacket(qp)

protected void processPacket(QuorumPacket qp) throws IOException{
    switch (qp.getType()) {
    case Leader.PING:            
        ping(qp);            
        break;
    case Leader.PROPOSAL:    //接收提议
        /**已经讲过,略**/
        break;
    case Leader.COMMIT:  //提交提议
        fzk.commit(qp.getZxid());
        break;
    case Leader.UPTODATE:
        LOG.error("Received an UPTODATE message after Follower started");
        break;
    case Leader.REVALIDATE:
        revalidate(qp);
        break;
    case Leader.SYNC:
        fzk.sync();
        break;
    default:
        LOG.error("Invalid packet type: {} received by Observer", qp.getType());
    }
}

那么我们找到case Leader.COMMIT:这个分支里,处理这个事务的方法fzk.commit(qp.getZxid())

public void commit(long zxid) {
    if (pendingTxns.size() == 0) {
        /**Log4j**/
        return;
    }
    long firstElementZxid = pendingTxns.element().zxid;
    if (firstElementZxid != zxid) {
        /**Log4j**/
        System.exit(12);
    }
    Request request = pendingTxns.remove();
    commitProcessor.commit(request); //提交
}

进入后,发现这个提交用的就是commitProcessor.commit(request),这里是不是有点醒悟了,我们当初可是在CommitProcessor里面阻塞了对不对,那么这里是不是要唤醒了呢?我们进入看:

synchronized public void commit(Request request) {
    if (!finished) {
        if (request == null) {
            /**Log4j**/
            return;
        }
        /**Log4j**/
        committedRequests.add(request);//添加request
        notifyAll();//唤醒
    }
}

在这里面,我们就发现了两句话有用committedRequests.add(request)添加请求到队列里,以及唤醒notifyAll()。这时候在CommitProcessor里面就能够直接走到finalProcessor里面了,并且刚才阻塞的事务可以被提交了。所以CommitProcessor做了这样的逻辑:首先,不能提交的就先阻塞住等待ack,可以提交的就直接调用下一个process去提交更新内存。这里要特别说明一点,leaderfollower本质上是一样的,所以大家都有CommitProcessor这个类在执行。leader端的CommitProcessor也会在收到ack之后执行这个notifyAll();,这一点是一摸一样的。这里大家一定要想清楚,不然很容易就懵了。

总结一下到目前为止,Zookeeper都做了什么:
首先Client发了一个写请求给leaderleader首先会发送一个提议给followersfollowers会返回ackleader,然后leader会进行一个过半验证,验证过了会给followers发送提交认证,followers接收到以后就会直接调用finalprocessor更新内存,因为持久化已经在发送ack之前就做完了。所以leader的持久化已经做好了,提议也通过了,接下来leader就也走到了finalprocessor更新内存。

Follower转发写请求

那么本章我们就还剩下一个内容,如果client发给follower一个写请求会有什么不同:
假设一个客户端个follower发送了一个写请求,那么根据我们上面的分析,第一个接到写请求的应该是FollowerRequestProcessor,因为这里的逻辑也是放到一个队列里面,然后有一个线程去取出来。所以就有到了FollowerRequestProcessor.run()里面。

public void run() {
    try {
        while (!finished) {
            Request request = queuedRequests.take();
            if (LOG.isTraceEnabled()) {
                ZooTrace.logRequest(LOG, ZooTrace.CLIENT_REQUEST_TRACE_MASK,
                        'F', request, "");
            }
            if (request == Request.requestOfDeath) {
                break;
            } 
            nextProcessor.processRequest(request); //转发,不符合要求的就阻塞
            switch (request.type) {
            case OpCode.sync:
                zks.pendingSyncs.add(request);
                zks.getFollower().request(request);
                break;
            case OpCode.create:
            case OpCode.delete:
            case OpCode.setData:
            case OpCode.setACL:
            case OpCode.createSession:
            case OpCode.closeSession:
            case OpCode.multi:
                zks.getFollower().request(request);
                break;
            }
        }
    } catch (Exception e) {
        handleException(this.getName(), e);
    }
    LOG.info("FollowerRequestProcessor exited loop!");
}

到这里面以后,直接转交给下一个处理器执行了nextProcessor.processRequest(request),也就是用到了CommitProcessor这个处理器。我们刚刚说过,这个处理器里面,如果发现不合标准的请求会直接阻塞,不多说,我们接着走到下面的switch里去。这里只要不是OpCode.sync的操作码的case,都会用zks.getFollower().request(request);这个方法去处理。自然我们要去看下里面写了什么:

    void request(Request request) throws IOException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        DataOutputStream oa = new DataOutputStream(baos);
        oa.writeLong(request.sessionId);
        oa.writeInt(request.cxid);
        oa.writeInt(request.type);
        if (request.request != null) {
            request.request.rewind();
            int len = request.request.remaining();
            byte b[] = new byte[len];
            request.request.get(b);
            request.request.rewind();
            oa.write(b);
        }
        oa.close();
        QuorumPacket qp = new QuorumPacket(Leader.REQUEST, -1, baos
                .toByteArray(), request.authInfo);  //包装Leader.REQUEST请求码和数据
        writePacket(qp, true); //写入socket
    }

进入看这个方法就是初始化了一些参数,然后直接包装成一个QuorumPacket qp写到socket里。所以这里就是followerleader转发的过程,我们看这里封装的就是Leader.REQUEST。这个就是之前我们提到的在LearnerHandler.run()里面的Leader.REQUEST部分的逻辑处理,所以我们转过去:

queuedPackets.add(new QuorumPacket(Leader.UPTODATE, -1, null, null)); //同步数据完成
while (true) {  //这里还是配对监听的while循环
    /**略**/
    switch (qp.getType()) {
    /**略**/
    case Leader.REQUEST:   //接收Follower转发
       bb = ByteBuffer.wrap(qp.getData());
       sessionId = bb.getLong();
       cxid = bb.getInt();
       type = bb.getInt();
       bb = bb.slice();
       Request si;
       if(type == OpCode.sync){
           si = new LearnerSyncRequest(this, sessionId, cxid, type, bb, qp.getAuthinfo());
       } else {
           si = new Request(null, sessionId, cxid, type, bb, qp.getAuthinfo());
       }
       si.setOwner(this);
       leader.zk.submitRequest(si);
       break;
    default:
        LOG.warn("unexpected quorum packet, type: {}", packetToString(qp));
        break;
    }
}

还是先拿到数据,然后构造了请求si,再往下就发现了请求提交leader.zk.submitRequest(si)的方法,继续进入:

public void submitRequest(Request si) {
    if (firstProcessor == null) {
        synchronized (this) {
            try {
                while (state == State.INITIAL) {
                    wait(1000);
                }
            } catch (InterruptedException e) {
                LOG.warn("Unexpected interruption", e);
            }
            if (firstProcessor == null || state != State.RUNNING) {
                throw new RuntimeException("Not started");
            }
        }
    }
    try {//到这里
        touch(si.cnxn);
        boolean validpacket = Request.isValid(si.type);
        if (validpacket) {
            firstProcessor.processRequest(si);
            if (si.cnxn != null) {
                incInProcess();
            }
        } else {
            LOG.warn("Received packet at server of unknown type " + si.type);
            new UnimplementedRequestProcessor().processRequest(si);
        }
    } catch (MissingSessionException e) {
        if (LOG.isDebugEnabled()) {
            LOG.debug("Dropping request: " + e.getMessage());
        }
    } catch (RequestProcessorException e) {
        LOG.error("Unable to process request:" + e.getMessage(), e);
    }
}

不知道这里大家还有没有印象,这里就是我们启动处理器链的地方对不对。但是我们要注意外面的zkLeaderZookeeperServer的实例对象,所以这个方法里的firstProcessor就是我们leader处理链中的PrepRequestProcessor。这样follower接收到的事物,就转发到了Leader上面,Observer也是同理,这样Zookeeper的一致性保证就结束了。

总结

那么我们用了两篇博客把Zookeeper的初始化也讲解完了。在这两篇里面,我们详细的介绍了集群模式的处理器链是怎么工作的,以及follower和leader的交互又是怎么监听的等等内容。到这里Zookeeper的启动等等内容基本上就告一段落了。但是我们还遗留了一个大坑,那就是Zookeeper的选举机制的源码讲解。这部分作为Zookeeper的精华只会多不会少,所以笔者预计将会用四到五篇博客去处理这部分。也可以看到这部分有多么的复杂。同样留几张图供大家更好的理解这几篇的内容:

Leader服务器接收到命令流程图 - Part 1

在这里插入图片描述

Leader服务器接收到命令流程图 - Part 2

在这里插入图片描述

Follower服务器接收到命令流程图

在这里插入图片描述

Observer服务器接收到命令流程

在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值