Zookeeper Zab协议

539 阅读14分钟

1. Zab协议总流程

当我们运行Quorum模式的时候,实际上建立了一个Quorum的网络通信,在leader选举结束后,leader节点会和所有的follower节点建立一个单独的网络通信,通过socket新建一个监听端口来和所有follower进行zab协议的流程。 zab协议的作用其实就是为了保证写数据的一致性,所以你会发现读请求都是各个节点直接自己处理了,只有写请求才会发起zab。

  • 假设,某一个follower节点接收到了一个写请求,那么他会现在自己的processor pipeline中进行处理,再此过程中,会将这个请求发送到leader节点,发起zab协议,并将这个请求在自己的处理器中等待。
  • 然后leader通过监听接收到请求,这时候leader节点会发送proposal数据包,给每一个follower。
  • 当其他follower接收到proposal后,会响应给ledaer节点ack报,通知ledaer节点自己可以处理这个请求。
  • 当leader节点收到过半数的ack包后,会发送commit包,告诉各个follower节点将写数据提交。至此流程结束。

上图是leader和follower节点交互流程,其实有一些不准确,因为有些具体的交互流程并不是在processor中实现的,不过是在processor调用前后,此图可以作为参考。

下面来看一下源码的实现。

2.Follower请求接受流程

2.1 请求接受

我们知道,集群模式的入口是QuorumPeerMain类中的main方法,然后依次调用main()->initializeAndRun()->runFromConfig(),在runFromConfig中,进行配置了QuoromPeer类进行了主要的leader选举zab协议处理等内容,那么再次之前,runFromConfig中,其实创建了一个Netty服务端实例,来处理监听客户端请求。 这里的createFactory()方法,通过反射创建了NettyServerCnxnFactory的实例,这个类并没有实际启动Netty,只做了基本的配置。

在runFromConfig方法中,配置好QuorumPeer类后,会start运行这个类,因为这个类其实实现了Thread,是一个可执行的类,在start方法中,真正启动了Netty的线程组,进行请求的监听。

至此,开始启动Netty服务端,监听请求,我们上面提到了Netty监听客户端的实际的处理器是CnxnChannelHandler类,当有请求数据进来的时候,Netty会回调ChannelRead接口,进行数据的接受,接受完数据后,会发送个Processor进行请求的处理。

receiveMessage()中会处理接收到的pecket processPacket中进行一系列验证过后,会将请求推入的缓冲队列中。

追踪一下这个队列的使用的地方,找到了take()方法,在这里请求拿出来并且发送到了第一个处理器。

2.1.1 构造处理器链

下面再来看看这个第一个处理是什么,follower整个处理器链是在ledaer选举后才初始化完成的。因为我们的在leder选举完成后,会创建对应的节点类型,比如leder节点就创建LeaderZookeeperServer,follower节点就创建FollowerZookeeperServer。我们来看follower的zkserver。

FollowerZookeeperServer继承Zookeeper,Zookeeper中的startup()方法,调用了FollowerZookeeperServer实例的重写方法setupRequestProcessors()创建了处理器链。

这里创建了两个处理器分支,晚点儿说这个。 但是你会发现他创建FollowerZookeeper实例的时候并没有直接实例话这个处理链,而是在follwer.followleader中才初始化了,调用syncWithLeader中,同步和leader的事务,然后调用startUp()初始化了处理器链。

在构造完处理器链后我们发现,处理器链其实一条是followerprocessor->commitProcessor->finalprocessor;另外一条是syncRequestprocessor->sendAckRequestProcessor。

2.2 请求处理

通过上面的源码分析,已经确定了第一个请求处理器是followerprocessor,下面看看后续的处理逻辑。 FollowerRequestProcessor中第一步就是压入队列,进行缓存解耦。

FollowerRequestProcessor其实也是一条可运行的线程,在run()方法中,取出了队列中的请求,做了两件事,一件事是发送到committedRequestProcessor中进行后续等待提交的处理;另一件事,是发起zab协议,将请求数据发送给leader,进行zab协议流程。

CommittedRequestProcessor,其实是一个相对复杂的处理器,他的最大的作用就是,确定请求什么时候落库提交,放到最后来说,因为leader和follower都涉及到这个过程。下面先说一下follower发送给leader写请求之后,接受proposal和commit的过程。

2.2.1 proposal

我们之前提到了,对于follower节点来说,选举结束之后,会创建follower实例,并调用followledaer()方法来进行实际的监听,这里会监听来自leader节点的proposal,commit等类型的请求。

我们先来说一下proposal请求,在处理proposal请求过程中会调用logRequest(),这个方法很简单就是将请求放到了SyncRequestProcessor处理器的处理队列中。

在SyncRequestProcessor的run()方法中,将缓存队列中的请求取出来处理,取出来后,发送给了SendAckProcessor。

其实里SendAckProcessor的处理逻辑也比较简单,其实就是包装一个Ack包返回给leader。

2.2.2 commit

上面已经处理了和leader的proposal的Ack响应,当leader收到超过半数的Ack后,就会发送Commit给各个follower节点。follower接受commit,也是在follower.followleader()中监听的。

这里的处理比较简单,其实就是直接提交给了CommittedProcessor去处理了。

3.Leader Zab协议流程

3.1 Processor责任链建立

当leader选举结束后,如果当前节点被选为leader节点,那么将会创建leader实例来行使ledaer的职责。 我们先来看一下leader的处理器链是怎么建立的。 和follower类似,我们可以在leader类中的lead()->startZkServer()->zk.startup()->setupRequestProcessors()方法中看到处理器责任链的建立。

我们可以看到,建立的处理器链是LeaderRequestProcessor->PrepRequestProcessor->ProposalrequestProcessor->CommittedProcessor->ToBeAppliedProcessor->FinalProcessor.

3.2 监听follower请求

创建leader节点中,会单独创建一个监听端口来处理zab协议follower发过来的额请求,连接到各个follower。

创建好leader实例后,调用lead()方法,在lead()方法中,创建了LearnerCnxAcceptor,这个类也继承了Thread,调用start()方法来运行线程,调用run(),来处理和follower的交互。

LearnerCnxAcceptorHandler其实只是while循环接受请求,请求数据读取完成后,其实也是开启了一个线程交给了另一个handler去处理。

在LearnerHandler中,具体根据请求的类型处理了请求。

3.3 处理zab协议

在LearnerHandler的run()方法中可以看到,根据类型进行了处理,先来看下对follower发过来的写请求的REQUEST类型的处理

3.3.1 REQUEST

这里接收到请求后,其实一路发送到了第一个处理器也就是prepRequestprocessor中。

然后prepRequestProcessor经过一系列验证等处理,发送到了下一个处理器中,经过上面分析的处理器链,可以知道他实际是发送给了proposalRequestProcessor。

proposalRequestProcessor的processRequest方法中,其实干了三件事,一是发送给下一个处理器CommittedProcessor进行等待提交的操作;第二个是,判断是写请求,那么发送proposal给follower节点;第三一个是,调用SyncRequestProcessor,将请求尝试commit到本地。

CommittedProcessor我们放到后面讲,留个坑。先来说下proposal发送给follower节点。

再来说说SyncRequestProcessor,这个处理器其实也是从队列中拿出来任务,然后处理发送给了下一个处理器,那其实SyncRequestProcessor的处理器链,是在ProposalProcessor里面去创建的,也就是说我们构造leader节点处理链,创建ProposalProcessor的时候,也构造了另一条处理链。

在SyncRequestProcessor中,其实也调用了nextProcessor.processRequest方法。从上面可以看到SyncRequestProcessor的下一个处理器是AckRequestProcessor。来看下这个processor干了点儿啥。

processAck这个方法其实主要就是做了用一个map将Ack根据zxid分组,然后如果一个事务(zxid)接收到超过半数的请求,那么就尝试提交这个事务发送commit请求到各个follower节点,然后也给自己发一个commit。给follower发送的请求就加入到队列中了,给自己发送的请求就直接调用了CommittedProcessor中的commit方法来处理了。

3.3.2 Ack

我们再来看一下当接受到follower节点返回的Ack数据是怎么处理的,还是返回到LearnerHandler的run()监听方法中来,这里监听了Ack类型的请求。

可以看到这里也还是直接调用了上面说的processAck()方法,完成了计算是不是超过了半数的Ack,如果超过了,那就发送commit给follower并且这个请求也提交到自己的Committed的commit方法中。

至此说明完了Zab协议的交互流程。

4. CommittedProcessor

Committed的入口其实有这么几个。

一,follower节点从FollowerProcessor提交的本节点接受到的请求。

二,leader节点发送给follower,并且也发送给自己节点的CommittedProcessor的commit()方法。

CommittedProcessor里面有几个比较重要的缓存队列,决定了什么时候提交事务。

  • queuedRequests:这个队列缓存了读请求和写请求,在CommittedProcessor的run方法中会poll出来,在一次循环中必定会poll出来。
  • queueWriteRequests:这个队列其实存放的是自己节点提交的写请求,用来后续对比确认处理的请求是不是自己节点提交的。
  • committedRequests:这个队列用来存放已经可以提交的请求,不论是leader还是follower节点,都会将自己或者zab协议接收到的请求存放到这个队列,对于leader节点,当接收到过半数的Ack请求后会把请求放到这个队列中,对于follower节点,在接收到leader发送过来的commit请求后,会把commit请求放到这个队列中。这个队列其实和queuewriteRequests队列中有一部分是一样的,比如follower节点的写请求,首先会放到queuewriteRequests中,在接收到leader的commit后,会放入committedRequests中,后续如果发现这个请求两个队列中都存在,那么可以确定这个请求是自己节点接收到的并且是可以提交的。
  • pendingRequests:他其实是一个map,key是客户端会话id,value是一个队列。他的作用其实主要是为了实现对同一客户端的数据一致性,其实是一把读写锁,queueRequests队列中poll出来的请求,如果是写请求就会加入到pendingRequests中,后面如果有同一个客户端的读请求或者写请求发来的时候,都要加入key为客户端会话id的队列中,这样保证客户端前一刻发送的写请求能被正确返回,不会造成写请求还没处理我去读发现刚刚写进去的东西不存在,这又点儿类似于mysql的读写锁的意思。

下面贴出来代码分析

 public void run() {
        try {
            /*
             * In each iteration of the following loop we process at most
             * requestsToProcess requests of queuedRequests. We have to limit
             * the number of request we poll from queuedRequests, since it is
             * possible to endlessly poll read requests from queuedRequests, and
             * that will lead to a starvation of non-local committed requests.
             */
            int requestsToProcess = 0;
            boolean commitIsWaiting = false;
            do {
                /*
                 * Since requests are placed in the queue before being sent to
                 * the leader, if commitIsWaiting = true, the commit belongs to
                 * the first update operation in the queuedRequests or to a
                 * request from a client on another server (i.e., the order of
                 * the following two lines is important!).
                 */
                /**
                 * 这里这两个变量其实是用来限制不让他一直循环的,committed是leader提交commit添加到这个队列的,如果不为空那么需要执行写请求
                 */
                commitIsWaiting = !committedRequests.isEmpty();
                /**
                 * 这个队列是用来缓存自己节点收到的客户端请求,其中有读请求和写请求
                 */
                requestsToProcess = queuedRequests.size();
                // Avoid sync if we have something to do
                /**
                 * 这里就是说,leader发过来的commit是空,并且接收到的客户端的请求是空,那么就获取锁进行线程wait() 如果要wait必须获取对象锁
                 * 在commit方法中,会进行notify唤醒线程去处理已经commit的写请求,在收到读请求的时候也会进行notify告诉线程去处理
                 */
                if (requestsToProcess == 0 && !commitIsWaiting) {
                    // Waiting for requests to process
                    synchronized (this) {
                        while (!stopped && requestsToProcess == 0 && !commitIsWaiting) {
                            wait();
                            commitIsWaiting = !committedRequests.isEmpty();
                            requestsToProcess = queuedRequests.size();
                        }
                    }
                }

                ServerMetrics.getMetrics().READS_QUEUED_IN_COMMIT_PROCESSOR.add(numReadQueuedRequests.get());
                ServerMetrics.getMetrics().WRITES_QUEUED_IN_COMMIT_PROCESSOR.add(numWriteQueuedRequests.get());
                ServerMetrics.getMetrics().COMMITS_QUEUED_IN_COMMIT_PROCESSOR.add(committedRequests.size());

                long time = Time.currentElapsedTime();

                /*
                 * Processing up to requestsToProcess requests from the incoming
                 * queue (queuedRequests). If maxReadBatchSize is set then no
                 * commits will be processed until maxReadBatchSize number of
                 * reads are processed (or no more reads remain in the queue).
                 * After the loop a single committed request is processed if
                 * one is waiting (or a batch of commits if maxCommitBatchSize
                 * is set).
                 */
                Request request;
                int readsProcessed = 0;
                while (!stopped
                       && requestsToProcess > 0
                       && (maxReadBatchSize < 0 || readsProcessed <= maxReadBatchSize)
                       && (request = queuedRequests.poll()) != null) {
                    requestsToProcess--;
                    /**
                     * 这里首先判断现在从queuedRequest队列里面pop出来的请求是不是个写请求,并且判断pendingrequests里面存不存在
                     * 因为如果pendingrequets已经把这个客户端的会话存下来了,那么说明这个客户端发过来的写请求还没有被处理,那么同一个客户端的读请求
                     * 一定要在写请求后面排队,避免同一个客户端发送的请求处理的不一致,比如客户端已经明明写进去了,再次去查询却发现没有写进去,这个其实是
                     * 类似于一个读写锁的效果
                     */
                    if (needCommit(request) || pendingRequests.containsKey(request.sessionId)) {
                        // Add request to pending
                        Deque<Request> requests = pendingRequests.computeIfAbsent(request.sessionId, sid -> new ArrayDeque<>());
                        requests.addLast(request);
                        ServerMetrics.getMetrics().REQUESTS_IN_SESSION_QUEUE.add(requests.size());
                    } else {
                        /**
                         * 如果这里是一个读请求,并且同一个客户端会话没有缓存,那么直接交给finalprocessor进行读请求处理
                         * 返回给客户端数据
                         */
                        readsProcessed++;
                        numReadQueuedRequests.decrementAndGet();
                        sendToNextProcessor(request);
                    }
                    /*
                     * Stop feeding the pool if there is a local pending update
                     * and a committed request that is ready. Once we have a
                     * pending request with a waiting committed request, we know
                     * we can process the committed one. This is because commits
                     * for local requests arrive in the order they appeared in
                     * the queue, so if we have a pending request and a
                     * committed request, the committed request must be for that
                     * pending write or for a write originating at a different
                     * server. We skip this if maxReadBatchSize is set.
                     */
                    /**
                     * 这里判断如果pendingRequest本地收到的请求,和commitedRequest由leader节点发过来可以提交的请求都不为空的话
                     * 那么就跳出循环,这个循环其实主要就是用来处理读请求的
                     */
                    if (maxReadBatchSize < 0 && !pendingRequests.isEmpty() && !committedRequests.isEmpty()) {
                        commitIsWaiting = true;
                        break;
                    }
                }
                ServerMetrics.getMetrics().READS_ISSUED_IN_COMMIT_PROC.add(readsProcessed);

                if (!commitIsWaiting) {
                    commitIsWaiting = !committedRequests.isEmpty();
                }

                /*
                 * Handle commits, if any.
                 */
                if (commitIsWaiting && !stopped) {
                    /*
                     * Drain outstanding reads
                     */
                    /**
                     * 这里其实是等待工作线程池里的读请求全部处理完,因为如果不处理完就运行写数据的话,会造成数据的不一致,因为写请求是在读请求后面才提交的,
                     * 反而在读请求还没运行完的时候就能读取到后提交的写请求的数据了,其实是脏读了
                     */
                    waitForEmptyPool();

                    if (stopped) {
                        return;
                    }

                    int commitsToProcess = maxCommitBatchSize;

                    /*
                     * Loop through all the commits, and try to drain them.
                     */
                    /**
                     * 所有读请求处理完了之后开始处理写请求
                     */
                    Set<Long> queuesToDrain = new HashSet<>();
                    long startWriteTime = Time.currentElapsedTime();
                    int commitsProcessed = 0;
                    /**
                     * 这里这个while循环其实是要一直把所有的写请求处理完了才罢休,好不容易见缝插针所有的读请求都运行完了
                     * 肯定要把我现在的所有请求处理完了才行
                     */
                    while (commitIsWaiting && !stopped && commitsToProcess > 0) {

                        // Process committed head
                        /**
                         * 先从committedrequests里面拿出来一个看看是不是我自己提交的
                         * 因为其实leader也会运行到这个处理器的这段代码,也就是说committedreqeusts里面也有leader自己提交给自己的
                         * 请求或者其他follower节点通过zab协议发给leader的请求也会放到committedrequests里面
                         */
                        request = committedRequests.peek();

                        if (request.isThrottled()) {
                            LOG.error("Throttled request in committed pool: {}. Exiting.", request);
                            ServiceUtils.requestSystemExit(ExitCode.UNEXPECTED_ERROR.getValue());
                        }

                        /*
                         * Check if this is a local write request is pending,
                         * if so, update it with the committed info. If the commit matches
                         * the first write queued in the blockedRequestQueue, we know this is
                         * a commit for a local write, as commits are received in order. Else
                         * it must be a commit for a remote write.
                         */
                        /**
                         * 这里其实是判断queuewriterequest队头的请求是不是自己提交的
                         * 通过和commitedrequest的请求比较,看看,如果是sessionid和cxid相同的话,那其实就是leader节点自己给自己提交的任务
                         * 否的的话,当前的节点就是follower节点,committedrequest的peek出来的请求,就是leader发给follower节点的请求。
                         */
                        if (!queuedWriteRequests.isEmpty()
                            && queuedWriteRequests.peek().sessionId == request.sessionId
                            && queuedWriteRequests.peek().cxid == request.cxid) {
                            /*
                             * Commit matches the earliest write in our write queue.
                             */
                            Deque<Request> sessionQueue = pendingRequests.get(request.sessionId);
                            ServerMetrics.getMetrics().PENDING_SESSION_QUEUE_SIZE.add(pendingRequests.size());
                            if (sessionQueue == null || sessionQueue.isEmpty() || !needCommit(sessionQueue.peek())) {
                                /*
                                 * Can't process this write yet.
                                 * Either there are reads pending in this session, or we
                                 * haven't gotten to this write yet.
                                 */
                                break;
                            } else {
                                ServerMetrics.getMetrics().REQUESTS_IN_SESSION_QUEUE.add(sessionQueue.size());
                                // If session queue != null, then it is also not empty.
                                Request topPending = sessionQueue.poll();
                                /*
                                 * Generally, we want to send to the next processor our version of the request,
                                 * since it contains the session information that is needed for post update processing.
                                 * In more details, when a request is in the local queue, there is (or could be) a client
                                 * attached to this server waiting for a response, and there is other bookkeeping of
                                 * requests that are outstanding and have originated from this server
                                 * (e.g., for setting the max outstanding requests) - we need to update this info when an
                                 * outstanding request completes. Note that in the other case, the operation
                                 * originated from a different server and there is no local bookkeeping or a local client
                                 * session that needs to be notified.
                                 */
                                /**
                                 * 如果是leader节点自己的请求,那么就重新包装一下,把queuewriterequest出队
                                 * 或者如果本节点是follower节点,那么如果leader发送过来的commit请求是我之前提交过去的请求,那么也在
                                 * queuewriterequest中出队
                                 */
                                topPending.setHdr(request.getHdr());
                                topPending.setTxn(request.getTxn());
                                topPending.setTxnDigest(request.getTxnDigest());
                                topPending.zxid = request.zxid;
                                topPending.commitRecvTime = request.commitRecvTime;
                                request = topPending;
                                if (request.isThrottled()) {
                                    LOG.error("Throttled request in committed & pending pool: {}. Exiting.", request);
                                    ServiceUtils.requestSystemExit(ExitCode.UNEXPECTED_ERROR.getValue());
                                }
                                // Only decrement if we take a request off the queue.
                                numWriteQueuedRequests.decrementAndGet();
                                queuedWriteRequests.poll();
                                /**
                                 * 这里把pendingRequests里面sessionid放到queuestoDrain。
                                 * 其实相当于把的剩下的请求放到queuestodrain里面,
                                 * 这个队列里面会有读请求和写请求,会在队列里面排队
                                 */
                                queuesToDrain.add(request.sessionId);
                            }
                        }
                        /*
                         * Pull the request off the commit queue, now that we are going
                         * to process it.
                         */
                        /**
                         * 不管是leader自己运行到这里还是follower运行到这里都要处理commiteed里面的请求,因为里面其实都是已经可以提交的请求
                         */
                        committedRequests.remove();
                        commitsToProcess--;
                        commitsProcessed++;
                        /**
                         * 这里是提交到下一个finalprocessor,因为不管读请求写请求最终在datatree上的操作都是在finalprocessor里处理的
                         */
                        // Process the write inline.
                        processWrite(request);

                        commitIsWaiting = !committedRequests.isEmpty();
                    }
                    ServerMetrics.getMetrics().WRITE_BATCH_TIME_IN_COMMIT_PROCESSOR
                        .add(Time.currentElapsedTime() - startWriteTime);
                    ServerMetrics.getMetrics().WRITES_ISSUED_IN_COMMIT_PROC.add(commitsProcessed);

                    /*
                     * Process following reads if any, remove session queue(s) if
                     * empty.
                     */
                    readsProcessed = 0;
                    /**
                     * 上面指明了,如果是leader节点或follower节点,也就是自己处理自己的commit请求,那么会把所有
                     * 阻塞在自己队列中的读请求一股脑儿 的返回,因为所有的读请求其实这个时候是可以返回的,因为本节点的写请求已经提交了
                     * 保证了数据的一致性,然后其实队列里面也有写请求,那么遇到写请求会推出循环停止提交,继续进入下一个大循环,进行写请求的
                     * 处理
                     */
                    for (Long sessionId : queuesToDrain) {
                        Deque<Request> sessionQueue = pendingRequests.get(sessionId);
                        int readsAfterWrite = 0;
                        while (!stopped && !sessionQueue.isEmpty() && !needCommit(sessionQueue.peek())) {
                            numReadQueuedRequests.decrementAndGet();
                            sendToNextProcessor(sessionQueue.poll());
                            readsAfterWrite++;
                        }
                        ServerMetrics.getMetrics().READS_AFTER_WRITE_IN_SESSION_QUEUE.add(readsAfterWrite);
                        readsProcessed += readsAfterWrite;

                        // Remove empty queues
                        if (sessionQueue.isEmpty()) {
                            pendingRequests.remove(sessionId);
                        }
                    }
                    ServerMetrics.getMetrics().SESSION_QUEUES_DRAINED.add(queuesToDrain.size());
                    ServerMetrics.getMetrics().READ_ISSUED_FROM_SESSION_QUEUE.add(readsProcessed);
                }

                ServerMetrics.getMetrics().COMMIT_PROCESS_TIME.add(Time.currentElapsedTime() - time);
                endOfIteration();
            } while (!stoppedMainLoop);
        } catch (Throwable e) {
            handleException(this.getName(), e);
        }
        LOG.info("CommitProcessor exited loop!");
    }

最后,其实都是提交到worker pool里去调用finalprocessor进行data tree的数据更改。