Java面试

zookeeper源码分析之集群模式服务端(上)

2018-11-03  本文已影响24人  1d96ba4c1912

最近较忙,在做服务网格的一些东西,很久没有更新文章了,其实一直惦记着这个事,这次接着之前的话题,把zookeeper的东西补完。

整体来说ZK集群模式运行时分为两个阶段,一个是选举,一个是处理请求。
本来是要一起写的,但由于内容较多,因此分成上下两篇来说,本篇文章我们先看看当ZK集群选举成功,达到一个稳定状态时各个不同类型的节点在处理请求时各自的流程。

由于集群模式跟单机模式有很多相同点,因此建议读本文前先看看我之前的一篇文章zookeeper源码分析之单机模式服务端

ZK集群的节点类型有三种,Leader,Follower,Observer。从大方向看,各个节点各自处理客户端发给我自己的读请求,对于写请求则统一交给Leader节点处理,Leader会先把写请求包装成proposal发给自己跟参与投票的节点,即Follower节点,等到收到多数节点的ACK消息以后则提交该请求并且想Follower节点发送commit通知以及向Observer节点发送inform通知。

下面我们就一个一个的进行源码解读。

Leader节点

Leader节点所在类:LeaderZooKeeperServer

与单机版一样,所有的请求都是交给一个处理链进行的,因此我们先来看看Leader节点的处理链路由哪些处理器构成,如下:

protected void setupRequestProcessors() {
    RequestProcessor finalProcessor = new FinalRequestProcessor(this);
    RequestProcessor toBeAppliedProcessor = new Leader.ToBeAppliedRequestProcessor(finalProcessor, getLeader());
    commitProcessor = new CommitProcessor(toBeAppliedProcessor, Long.toString(getServerId()), false, getZooKeeperServerListener());
    //start表示是异步线程,需要重点关注其run方法
    commitProcessor.start();
    ProposalRequestProcessor proposalProcessor = new ProposalRequestProcessor(this, commitProcessor);
    //该方法重点关注,会启动内部的几个处理器
    proposalProcessor.initialize();
    prepRequestProcessor = new PrepRequestProcessor(this, proposalProcessor);
    //start表示是异步线程,需要重点关注其run方法
    prepRequestProcessor.start();
    //第一个处理器
    firstProcessor = new LeaderRequestProcessor(this, prepRequestProcessor);

    setupContainerManager();
}

按上面的代码,Leader节点请求首先交给firstProcessor,即我们的LeaderRequestProcessor,所有的处理器核心方法都是processRequest,那就从这里入手吧:

public void processRequest(Request request) throws RequestProcessorException {
    //这里省略掉对session的校验逻辑...
    //真正处理的请求
    nextProcessor.processRequest(request);
}

LeaderRequestProcessor除了校验session的逻辑以外,就是把请求转交给下一个处理器进行处理,即PrepRequestProcessor处理器,该处理器在ZK单机模式中已经分析过了,就是把请求包装一个事务对象,这里就不再赘述了。

与单机模式不同,PrepRequestProcessor紧接着会把请求转交给ProposalRequestProcessor进行处理,这个处理器比较复杂,首先看下构造方法:

//传入的nextProcessor就是CommitProcessor
public ProposalRequestProcessor(LeaderZooKeeperServer zks, RequestProcessor nextProcessor) {
    this.zks = zks;
    //把下一个处理器设置为CommitProcessor
    this.nextProcessor = nextProcessor;
    //自己内部又初始化了一个AckRequestProcessor处理器
    AckRequestProcessor ackProcessor = new AckRequestProcessor(zks.getLeader());
   //同时初始化SyncRequestProcessor处理器
    syncProcessor = new SyncRequestProcessor(zks, ackProcessor);
}

//顺带着一起看下SyncRequestProcessor的构造方法
public SyncRequestProcessor(ZooKeeperServer zks,
        RequestProcessor nextProcessor) {
    super("SyncThread:" + zks.getServerId(), zks
            .getZooKeeperServerListener());
    this.zks = zks;
    //nextProcessor就是AckRequestProcessor处理器
    this.nextProcessor = nextProcessor;
    running = true;
}

这里主要关注点在于除了初始化的时候调用构造方法传入CommitProcessor作为它之后的处理器以外,它内部创建了AckRequestProcessor 跟SyncRequestProcessor两个处理器,并且把前者作为后者的nextProcessor。

搞清楚关系以后,先来看下ProposalRequestProcessor的处理逻辑:

public void processRequest(Request request) throws RequestProcessorException {
    //如果请求是由ZK集群中其他节点发过来的,则由各自的handler处理
    if (request instanceof LearnerSyncRequest){
        zks.getLeader().processSync((LearnerSyncRequest)request);
    } else {
        //普通请求直接调用下一个处理器,这里的下一个处理器是CommitProcessor
        //注意这里提交给CommitProcessor的既有读请求也有写请求
        nextProcessor.processRequest(request);
        //header不为null说明是写请求
        if (request.getHdr() != null) {
            try {
                ////写请求发起proposal
                zks.getLeader().propose(request);
            } catch (XidRolloverException e) {
                throw new RequestProcessorException(e.getMessage(), e);
            }
            //请求同步磁盘
            syncProcessor.processRequest(request);
        }
    }
}

我们顺着思路来看,首先该处理内部直接把请求转交给了CommitProcessor,上代码:

public void processRequest(Request request) {
    if (stopped) {
        return;
    }
    queuedRequests.add(request);
    wakeup();
}

很清楚的看到该方法就是把请求放进队列就返回了,首先我们要注意的是这里提交给CommitProcessor处理的请求既有读请求也有写请求,如果是写请求,我们可以想到该处理器会定到多数派节点都响应以后再执行commit操作,而读请求的处理逻辑我们后面跟着代码一起看。

回到上面ProposalRequestProcessor的代码,对于写请求,它在提交给CommitProcessor之后马上就发起了propose,所以我们先来看下这段代码:

public Proposal propose(Request request) throws XidRolloverException {
    //zxid已经用完,触发一次重新选举
    if ((request.zxid & 0xffffffffL) == 0xffffffffL) {
        String msg = "zxid lower 32 bits have rolled over, forcing re-election, and therefore new epoch start";
        //关闭当前服务
        shutdown(msg);
        throw new XidRolloverException(msg);
    }

    //封装发送的数据包
    byte[] data = SerializeUtils.serializeRequest(request);
    proposalStats.setLastProposalSize(data.length);
    QuorumPacket pp = new QuorumPacket(Leader.PROPOSAL, request.zxid, data, null);

    Proposal p = new Proposal();
    p.packet = pp;
    p.request = request;                
    
    synchronized(this) {
        //简单起见,这里省略掉部分逻辑...       

        lastProposed = p.packet.getZxid();
        //保存正在处理的Proposal到内存
        outstandingProposals.put(lastProposed, p);
        //发送数据包给各个节点
        sendPacket(pp);
    }
    return p;
}

上面把请求发送给集群中其他节点以后就开始把请求交给SyncRequestProcessor处理了,该处理器的逻辑之前单机版的文章中也已经分析过了,不同点在于这里它对应的nextProcessor变成了AckRequestProcessor,也就是说对于当前节点来说,当请求成功落盘以后,也要作为多数派中的一员响应ACK,那么接下来就来看下AckRequestProcessor的处理逻辑:

public void processRequest(Request request) {
    QuorumPeer self = leader.self;
    if(self != null)
        //ACK的参数是比较少的
        leader.processAck(self.getId(), request.zxid, null);
    else
        LOG.error("Null QuorumPeer");
}

synchronized public void processAck(long sid, long zxid, SocketAddress followerAddr) {        
    if (!allowedToCommit) return; 
    //如果是一个特殊的ACK信息(NEWLEADER)则忽略
    if ((zxid & 0xffffffffL) == 0) {
        return;
    }
    //Proposal会被加入这里
    if (outstandingProposals.size() == 0) {
        return;
    }
    //已经提交过,忽略
    if (lastCommitted >= zxid) {
        return;
    }
    Proposal p = outstandingProposals.get(zxid);
    if (p == null) {
        return;
    }

    //把响应节点的serverId加入到该Proposal中
    p.addAck(sid);        

    //每次收到ACK都会尝试commit
    boolean hasCommitted = tryToCommit(p, zxid, followerAddr);

    //简单起见这里省略掉对于reconfig指令的处理
}

好了,接下来的重点应该是上面的tryToCommit方法了,一起看下:

synchronized public boolean tryToCommit(Proposal p, long zxid, SocketAddress followerAddr) {       
       //保证按顺序commit
       if (outstandingProposals.containsKey(zxid - 1)) return false;
        //是否满足提交要求,即多数派达成一致
        //由于上面是只要收到ACK就会调用该方法
        //因此这里要判断是否响应过半,否则不执行真正的commit
        if (!p.hasAllQuorums()) {
           return false;
        }
        //从待处理的Proposal中删除
        outstandingProposals.remove(zxid);
        if (p.request != null) {
             //加入到待执行列表中
             toBeApplied.add(p);
        }

        if (p.request == null) {
            //忽略
        } else if (p.request.getHdr().getType() == OpCode.reconfig) {                                   
            //继续忽略reconfig指令
        } else {
            //发送commit通知给follower
            commit(zxid);
            //发送inform通知给observer
            inform(p);
        }
        //本地先commit该事务
        zk.commitProcessor.commit(p.request);
        //如果正在等待的同步请求包含该请求,则执行发送同步消息
        if(pendingSyncs.containsKey(zxid)){
            for(LearnerSyncRequest r: pendingSyncs.remove(zxid)) {
                sendSync(r);
            }               
        } 
        return  true;   
    }

上面执行commit的操作主要两个逻辑,一个是把该proposal加入到toBeApplied列表中,一个是调用commitProcessor的commit方法。结合上面的逻辑这里可以想到一开始ZK仅仅把请求放在commitProcessor队列中,明显是需要阻塞等待ACK的,而这里再调用commit应该是要执行真正的commit逻辑了,一起来看下:

public void commit(Request request) {
    if (stopped || request == null) {
        return;
    }
    //这里是把请求保存在commit队列中
    committedRequests.add(request);
    wakeup();
}

到这里总结一下,最一开始执行的commitProcessor中的processRequest方法是把请求放在queuedRequests队列中,这里放的请求既有读也有写,而这里通过commit方法放在committedRequests队列中的请求只有写。commitProcessor处理器是一个线程,下面重点看下它的run方法:

public void run() {
    try {
        int requestsToProcess = 0;
        boolean commitIsWaiting = false;
        do {
            //有待commit的请求(只有写)
            commitIsWaiting = !committedRequests.isEmpty();
            //有待处理的请求(读写都有)
            requestsToProcess =  queuedRequests.size();
            //如果当前没有任何待处理的请求则阻塞等待
            if (requestsToProcess == 0 && !commitIsWaiting){
                synchronized (this) {
                    while (!stopped && requestsToProcess == 0 && !commitIsWaiting) {
                        wait();
                        commitIsWaiting = !committedRequests.isEmpty();
                        requestsToProcess = queuedRequests.size();
                    }
                }
            }
            //首先开始待处理的读写请求
            Request request = null;
            while (!stopped && requestsToProcess > 0 && (request = queuedRequests.poll()) != null) {
                requestsToProcess--;
                //这里的第一个条件是写请求一律加入该客户端的队列,按照顺序执行
                //这里的第二个条件是要保证如果一个客户端先发写请求再发读请求,那么需要读到写之后的值
                if (needCommit(request) || pendingRequests.containsKey(request.sessionId)) {
                    LinkedList<Request> requests = pendingRequests.get(request.sessionId);
                    if (requests == null) {
                        requests = new LinkedList<Request>();
                        pendingRequests.put(request.sessionId, requests);
                    }
                    requests.addLast(request);
                }
                else {
                    //不是写请求且该客户端没有正在处理的请求则直接跳转下一个处理器ToBeAppliedRequestProcessor
                    sendToNextProcessor(request);
                }
                //从上面可以看到只有写请求才会加入pendingRequests队列
                //因此这里的意思就是如果有待处理的写请求且已经有已提交的写请求需要处理则跳出该循环
                if (!pendingRequests.isEmpty() && !committedRequests.isEmpty()){
                    commitIsWaiting = true;
                    break;
                }
            }
            if (commitIsWaiting && !stopped){
                waitForEmptyPool();
                if (stopped){
                    return;
                }
                //处理已提交的请求
                if ((request = committedRequests.poll()) == null) {
                    throw new IOException("Error: committed head is null");
                }
                LinkedList<Request> sessionQueue = pendingRequests.get(request.sessionId);
                if (sessionQueue != null) {
                    Request topPending = sessionQueue.poll();
                    if (request.cxid != topPending.cxid) {
                        sessionQueue.addFirst(topPending);
                    } else {
                        topPending.setHdr(request.getHdr());
                        topPending.setTxn(request.getTxn());
                        topPending.zxid = request.zxid;
                        request = topPending;
                    }
                }

                //调用下一个处理器,即ToBeAppliedRequestProcessor
                sendToNextProcessor(request);
                waitForEmptyPool();
                if (sessionQueue != null) {
                    //一旦处理完该客户端的一个写请求,则把它后面的读请求一次性处理完
                    //因为前面说了,为了保证同一客户端能读取到最新值,需要保证读在写之后
                    while (!stopped && !sessionQueue.isEmpty()
                            && !needCommit(sessionQueue.peek())) {
                        sendToNextProcessor(sessionQueue.poll());
                    }
                    if (sessionQueue.isEmpty()) {
                        pendingRequests.remove(request.sessionId);
                    }
                }
            }
        } while (!stoppedMainLoop);
    } catch (Throwable e) {
        handleException(this.getName(), e);
    }
}

commit之后下一个处理器ToBeAppliedRequestProcessor逻辑很简单:

public void processRequest(Request request) throws RequestProcessorException {
            //next指向最后一个处理器
            next.processRequest(request);
            if (request.getHdr() != null) {
                long zxid = request.getHdr().getZxid();
                Iterator<Proposal> iter = leader.toBeApplied.iterator();
                if (iter.hasNext()) {
                    Proposal p = iter.next();
                    if (p.request != null && p.request.zxid == zxid) {
                        iter.remove();
                        return;
                    }
                }
                LOG.error("Committed request not found on toBeApplied: " + request);
            }
        }

就是调用最终处理器FinalRequestProcessor以及把请求从toBeApplied列表中删除。
关于FinalRequestProcessor同样在之前的单机版文章中已经分析过了就是真正的修改内存中的数据,保存commitLog以及返回客户端response。

Follower节点

首先接着上面的Leader节点逻辑说,Leader会把写请求包装成proposal发送给各个follower节点处理,这个请求跟普通的连接到follower节点的客户端的请求不同,它是通过一个专门的端口来接受leader的消息的,这个在选举完成以后就建立了。
先来看下处理器链路的建立:

protected void setupRequestProcessors() {
    RequestProcessor finalProcessor = new FinalRequestProcessor(this);
    //与Leader节点相比commitProcessor后面的处理器删除了ToBeAppliedRequestProcessor,直接变为FinalRequestProcessor
    commitProcessor = new CommitProcessor(finalProcessor, Long.toString(getServerId()), true, getZooKeeperServerListener());
    commitProcessor.start();
    //第一个处理器改为FollowerRequestProcessor
    firstProcessor = new FollowerRequestProcessor(this, commitProcessor);
    ((FollowerRequestProcessor) firstProcessor).start();
    //SyncRequestProcessor之后跟着的是SendAckRequestProcessor,而不是AckRequestProcessor
    syncProcessor = new SyncRequestProcessor(this, new SendAckRequestProcessor((Learner)getFollower()));
    syncProcessor.start();
}

Follower节点监听Leader节点数据包的逻辑在Follower类中,我们只以proposal跟commit为例说明,如下:

protected void processPacket(QuorumPacket qp) throws Exception{
    switch (qp.getType()) {
    case Leader.PROPOSAL:           
        TxnHeader hdr = new TxnHeader();
        Record txn = SerializeUtils.deserializeTxn(qp.getData(), hdr);
        lastQueued = hdr.getZxid();
        //依旧忽略reconfig指令的处理...
        
        //处理proposal请求
        fzk.logRequest(hdr, txn);
        break;
    case Leader.COMMIT:
        //处理commit请求
        fzk.commit(qp.getZxid());
        break;
    }
}

从上面的代码片段看出,重点就是两个方法,logRequest跟commit,我们挨个来看下:

public void logRequest(TxnHeader hdr, Record txn) {
    Request request = new Request(hdr.getClientId(), hdr.getCxid(), hdr.getType(), hdr, txn, hdr.getZxid());
    if ((request.zxid & 0xffffffffL) != 0) {
        //加入处理中事务队列
        pendingTxns.add(request);
    }
    //请求落盘
    syncProcessor.processRequest(request);
}

通过syncProcessor处理器落盘的逻辑我们就不用多说了,从上面Follower节点初始化处理链路的代码我们知道落盘以后会把请求交给SendAckRequestProcessor来处理,一起看下:

public void processRequest(Request si) {
    if(si.type != OpCode.sync){
        //封装ACK包
        QuorumPacket qp = new QuorumPacket(Leader.ACK, si.getHdr().getZxid(), null, null);
        try {
            //发送给Leader
            learner.writePacket(qp, false);
        } catch (IOException e) {
            //忽略
        }
    }
}

逻辑很简单,我们接着看commit方法:

public void commit(long zxid) {
    if (pendingTxns.size() == 0) {
        return;
    }
    long firstElementZxid = pendingTxns.element().zxid;
    if (firstElementZxid != zxid) {
        System.exit(12);
    }
    Request request = pendingTxns.remove();
    //交给commitProcessor进行处理
    commitProcessor.commit(request);
}

逻辑很简单,这里就不多说了。
看完了Follower处理Leader的逻辑,我们最后来看一下Follower处理连接它的客户端发来的请求的逻辑,这显然要回到FollowerRequestProcessor类中,如下:

    public void processRequest(Request request) {
        if (!finished) {
            //首先跟Leader节点一样,依旧是校验session的逻辑,这里忽略...

            //这里是把请求加入到队列中
            queuedRequests.add(request);
        }
    }

由于FollowerRequestProcessor是一个异步线程,那还是来看下它的run方法:

public void run() {
    try {
        while (!finished) {
            Request request = queuedRequests.take();
            //如果是服务关闭的请求则退出循环
            if (request == Request.requestOfDeath) {
                break;
            }
            //无论读写请求,直接交给下一个处理器,即CommitProcessor
            nextProcessor.processRequest(request);
            switch (request.type) {
            case OpCode.sync:
                //如果客户端发来的是同步请求,则加入pendingSyncs队列,并想leader发送sync指令
                zks.pendingSyncs.add(request);
                zks.getFollower().request(request);
                break;
            case OpCode.create:
            case OpCode.create2:
            case OpCode.createTTL:
            case OpCode.createContainer:
            case OpCode.delete:
            case OpCode.deleteContainer:
            case OpCode.setData:
            case OpCode.reconfig:
            case OpCode.setACL:
            case OpCode.multi:
            case OpCode.check:
                //这里是写请求转发给leader进行处理
                zks.getFollower().request(request);
                break;
            case OpCode.createSession:
            case OpCode.closeSession:
                // Don't forward local sessions to the leader.
                //如果是session操作且不是本地session 则转发给Leader
                if (!request.isLocalSession()) {
                    zks.getFollower().request(request);
                }
                break;
            }
        }
    } catch (Exception e) {
        handleException(this.getName(), e);
    }
}

可以看到,对于Follower节点来说,连接它的客户端发来的请求,如果是读请求则直接进行处理,如果是写请求则转发给Leader节点处理,Leader节点又会按照上面的流程,先发给Follower节点proposal请求,再发commit请求。

Observer节点

基于上面的Leader节点跟Follower节点的分析,Observer节点应该是最简单的,对于Leader节点发来的请求它不参与投票,直接给一个等Leader的通知消息处理就好;对于连接它自己的客户端请求跟Follower是一样,一起看下。

首先是Observer节点处理链路的构建:

protected void setupRequestProcessors() {      
    //FinalRequestProcessor不变
    RequestProcessor finalProcessor = new FinalRequestProcessor(this);
    //CommitProcessor不变
    commitProcessor = new CommitProcessor(finalProcessor, Long.toString(getServerId()), true, getZooKeeperServerListener());
    commitProcessor.start();
    //第一个处理器变为ObserverRequestProcessor
    firstProcessor = new ObserverRequestProcessor(this, commitProcessor);
    ((ObserverRequestProcessor) firstProcessor).start();
}

跟Follower节点一样,我们先来看下它跟Leader节点的交互逻辑:

protected void processPacket(QuorumPacket qp) throws Exception{
    switch (qp.getType()) {
    case Leader.PROPOSAL:
        LOG.warn("Ignoring proposal");
        break;
    case Leader.COMMIT:
        LOG.warn("Ignoring commit");
        break;
    case Leader.INFORM:
        TxnHeader hdr = new TxnHeader();
        Record txn = SerializeUtils.deserializeTxn(qp.getData(), hdr);
        Request request = new Request (hdr.getClientId(),  hdr.getCxid(), hdr.getType(), hdr, txn, 0);
        ObserverZooKeeperServer obs = (ObserverZooKeeperServer)zk;
        //直接提交请求
        obs.commitRequest(request);
        break;
    }
}

可以看到它忽略了PROPOSAL跟COMMIT数据包,而当收到INFORM数据包以后直接进行commit,一起看下:

public void commitRequest(Request request) {     
    //直接提交
    commitProcessor.commit(request);        
}

最后来看下它处理客户端请求的逻辑,即ObserverRequestProcessor,如下:

public void processRequest(Request request) {
    if (!finished) {
        //同样忽略校验session的逻辑

        //把请求放队列
        queuedRequests.add(request);
    }
}
//核心逻辑依旧是在run方法中
public void run() {
    try {
        while (!finished) {
            Request request = queuedRequests.take();
            //如果服务关闭则退出循环
            if (request == Request.requestOfDeath) {
                break;
            }
            //交给commitProcessor处理
            nextProcessor.processRequest(request);
            switch (request.type) {
            case OpCode.sync:
                zks.pendingSyncs.add(request);
                zks.getObserver().request(request);
                break;
            case OpCode.create:
            case OpCode.create2:
            case OpCode.createTTL:
            case OpCode.createContainer:
            case OpCode.delete:
            case OpCode.deleteContainer:
            case OpCode.setData:
            case OpCode.reconfig:
            case OpCode.setACL:
            case OpCode.multi:
            case OpCode.check:
                //写请求转发给Leader
                zks.getObserver().request(request);
                break;
            case OpCode.createSession:
            case OpCode.closeSession:
                //非本地会话转发给leader处理
                if (!request.isLocalSession()) {
                    zks.getObserver().request(request);
                }
                break;
            }
        }
    } catch (Exception e) {
        handleException(this.getName(), e);
    }
}
上一篇下一篇

猜你喜欢

热点阅读