zookeeper源码(五)——客户端源码之客户端与服务端的通信过程二

上一篇说道了server端有链式调用来处理客户端请求,链式调用主要有三个处理器,分别是preRequestProcessor、syncRequestProcessor和filnalRequestProcessor,下面来看这三个处理器的内容。

首先是preRequestProcessor,可以看到根据请求头的请求类型的不同,进入不同的CASE,这意味着命令是可定制化的。再讲述server之前,我们知道客户端已经和服务端建链,客户端此时的状态时是一直在sendThread的线程循环中,等待可发送的包来消费,并且每隔一段时间会发送ping-pong请求,来保持服务端和客户端链路正常。

protected void pRequest(Request request) throws RequestProcessorException {
         LOG.info("Prep>>> cxid = " + request.cxid + " type = " +
        request.type + " id = 0x" + Long.toHexString(request.sessionId));
        request.setHdr(null);
        request.setTxn(null);

        try {
            //识别命令
            switch (request.type) {
            case OpCode.createContainer:
            case OpCode.create:
            case OpCode.create2:
                CreateRequest create2Request = new CreateRequest();
                pRequest2Txn(request.type, zks.getNextZxid(), request, create2Request, true);
                break;
            case OpCode.createTTL:
                CreateTTLRequest createTtlRequest = new CreateTTLRequest();
                pRequest2Txn(request.type, zks.getNextZxid(), request, createTtlRequest, true);
                break;
            case OpCode.deleteContainer:
            case OpCode.delete:
                DeleteRequest deleteRequest = new DeleteRequest();
                pRequest2Txn(request.type, zks.getNextZxid(), request, deleteRequest, true);
                break;
            case OpCode.setData:
                SetDataRequest setDataRequest = new SetDataRequest();
                pRequest2Txn(request.type, zks.getNextZxid(), request, setDataRequest, true);
                break;
            case OpCode.reconfig:
                ReconfigRequest reconfigRequest = new ReconfigRequest();
                ByteBufferInputStream.byteBuffer2Record(request.request, reconfigRequest);
                pRequest2Txn(request.type, zks.getNextZxid(), request, reconfigRequest, true);
                break;
            case OpCode.setACL:
                SetACLRequest setAclRequest = new SetACLRequest();
                pRequest2Txn(request.type, zks.getNextZxid(), request, setAclRequest, true);
                break;
            case OpCode.check:
                CheckVersionRequest checkRequest = new CheckVersionRequest();
                pRequest2Txn(request.type, zks.getNextZxid(), request, checkRequest, true);
                break;
            case OpCode.multi:
             .

            //All the rest don't need to create a Txn - just verify session
            case OpCode.sync:
            case OpCode.exists:
            case OpCode.getData:
            case OpCode.getACL:
            case OpCode.getChildren:
            case OpCode.getAllChildrenNumber:
            case OpCode.getChildren2:
            case OpCode.ping:
            case OpCode.setWatches:
            case OpCode.setWatches2:
            case OpCode.checkWatches:
            case OpCode.removeWatches:
            case OpCode.getEphemerals:
            case OpCode.multiRead:
            case OpCode.addWatch:
              ....
        nextProcessor.processRequest(request);
    }

为了更有条理的梳理请求,我们先从简答的客户端增删查改节点的路径开始,从下面增加节点值开始。 三个参数,第一个zk.setData是设置根节点的值,三个参数分别是路径、节点值和版本号。形成请求后提交到客户端的发送队列。

public Stat setData(final String path, byte[] data, int version) throws KeeperException, InterruptedException {
        final String clientPath = path;
        PathUtils.validatePath(clientPath);

        final String serverPath = prependChroot(clientPath);

        RequestHeader h = new RequestHeader();
        h.setType(ZooDefs.OpCode.setData);
        SetDataRequest request = new SetDataRequest();
        request.setPath(serverPath);
        request.setData(data);
        request.setVersion(version);
        SetDataResponse response = new SetDataResponse();
        ReplyHeader r = cnxn.submitRequest(h, request, response, null);
        if (r.getErr() != 0) {
            throw KeeperException.create(KeeperException.Code.get(r.getErr()), clientPath);
        }
        return response.getStat();
    }
synchronized (state) { //锁住state状态,相当于是队列是同步的,保证包的顺序性
            if (!state.isAlive() || closing) {
                conLossPacket(packet);
            } else {
                // If the client is asking to close the session then
                // mark as closing
                if (h.getType() == OpCode.closeSession) {
                    closing = true;
                }
                outgoingQueue.add(packet); 
            }
        }
        sendThread.getClientCnxnSocket().packetAdded(); //唤醒selector

看完客户端处理,看看服务端是如何处理set 命令的请求的。首先进入前置处理器,PrerequestProcessor,首先初始化请求,然后进入请求处理函数pRequest2Txn。

 LOG.info("Prep>>> cxid = " + request.cxid + " type = " +
        request.type + " id = 0x" + Long.toHexString(request.sessionId));
        request.setHdr(null); //初始化请求
        request.setTxn(null);

...

  case OpCode.setData:
                SetDataRequest setDataRequest = new SetDataRequest();//构造数据对象
                //自增zkserver hzxid,改参数是一个 AtomicLong 类型,线程安全
                pRequest2Txn(request.type, zks.getNextZxid(), request, setDataRequest, true);
                break;

 这个函数初始化一个txnheader,记录请求的sessionid,serverid和zxid(hzxid),以及时间和请求类型,方便后续请求写入日志和数据入snapshot。然后进入到setData case分支。该分支主要处理记录要变更的节点数据、zxid、版本以及权限信息,并放入处理队列,以及更新部分请求信息。

protected void pRequest2Txn(int type, long zxid, Request request, Record record, boolean deserialize) throws KeeperException, IOException, RequestProcessorException {
        if (request.getHdr() == null) {
            request.setHdr(new TxnHeader(request.sessionId, request.cxid, zxid,
                    Time.currentWallTime(), type)); //sessionID在请求过程初始随机化了,cxid指sid,zxid数据计数器
       }
...


  case OpCode.setData:
            zks.sessionTracker.checkSession(request.sessionId, request.getOwner());//服务端也会检查客户端的session是否超时,要求进行重连
            SetDataRequest setDataRequest = (SetDataRequest) record;
            if (deserialize) {
                ByteBufferInputStream.byteBuffer2Record(request.request, setDataRequest);//复制请求数据buffer到setDataRequest
            }
            path = setDataRequest.getPath();
            validatePath(path, request.sessionId); //验证节点路径是否有效
            nodeRecord = getRecordForPath(path);  //加队列锁从outstandingChanges队列中读取上一次节点修改信息以及加node锁从zkdatabase获取该路径的节点信息
            zks.checkACL(request.cnxn, nodeRecord.acl, ZooDefs.Perms.WRITE, request.authInfo, path, null); //验证节点acl,验证不通过抛异常
            int newVersion = checkAndIncVersion(nodeRecord.stat.getVersion(), setDataRequest.getVersion(), path); //验证修改的节点最新的版本和当前的版本是否一致,若不一直抛出异常,-1的话不验证版本
            request.setTxn(new SetDataTxn(path, setDataRequest.getData(), newVersion)); //设置入数据库的节点名称和值以及版本号
            nodeRecord = nodeRecord.duplicate(request.getHdr().getZxid());
            nodeRecord.stat.setVersion(newVersion); // version+1
            nodeRecord.stat.setMtime(request.getHdr().getTime());//记录操作时间
            nodeRecord.stat.setMzxid(zxid);//zid+1
            nodeRecord.data = setDataRequest.getData();
            nodeRecord.precalculatedDigest = precalculateDigest( 
                    DigestOpCode.UPDATE, path, nodeRecord.data, nodeRecord.stat);
            setTxnDigest(request, nodeRecord.precalculatedDigest);
            addChangeRecord(nodeRecord); //将NodeRecoord记录到zkserver的队列
            break;

....

前置处理器部分处理完后,进入同步处理器,该处理器也是在一个线程中进行阻塞消费队列,该处理器在源码中标记了集群中不同角色的作用, 如果角色是Leader,则同步请求并落盘,给你发ack,若是follwer,则同样数据落盘,但是通过SendAckRequestProcessor发送给leader,若是Observer,则单纯数据落盘。

因为这个是单机的zk,所以这个会进行数据落盘,然后直接进入最终处理器。下面是该处理器处理过程。先借鉴一下别人的分析,帮助我们理事物日志txnlog和数据存储文件snapshot,正好找到这段分析就是zk数据读写的源码分析总结,太巧了~~~

 ZK的数据存储在磁盘上分为快照和事务日志两种:

        1.对于客户端的写请求zk会将其写入到transaction log中

        2.如果事务日志达到一定数量,则产生一个新日志,并启动一个线程进行进行takeSnapshot,把当前的内存DataTree和会话信息写入到snapshot file中,snapshot的文件名以datatree的最后处理的transactionId为结尾

        3.ZK在启动的时候首先将snapshot从磁盘加载到内存的DataTree中,然后根据dataTree.lastProcessedZxid+1找到需要处理的transaction log记录,一一附加到datatree上。

        Zookeeper高效原因之一就是对请求的处理主要是操作内存结构. 然而对于每个写请求都需要写事务日志,为了提高append日志效率,zk使用了group commit机制,也就是并非每条日志都直接flush到磁盘上,而是等到多个请求产生的日志缓存起来,到达指定条数后,一次性flush到磁盘上

        此外zk为了在运行过程中迅速地向事务日志文件中append记录,为每个txn log文件预先分配了指定大小的磁盘块(通过preAllocSize来设置),默认是64M,接近这个值的时候(少于4k)便会再次分配64M.这样做的原因是如果没有预先分配,那么写完一个块的话就要重新分配,会带来额外的磁盘寻道。

下面是这个同步请求落盘的同步处理器代码,基本上和上面的分析能对上,现在我们明白,zk是通过事物日志和zxid来更新内存数据。

@Override
    public void run() {
        try {
            // we do this in an attempt to ensure that not all of the servers
            // in the ensemble take a snapshot at the same time
            resetSnapshotStats();
            lastFlushTime = Time.currentElapsedTime();
            while (true) {
                ServerMetrics.getMetrics().SYNC_PROCESSOR_QUEUE_SIZE.add(queuedRequests.size());

                long pollTime = Math.min(zks.getMaxWriteQueuePollTime(), getRemainingDelay());
                Request si = queuedRequests.poll(pollTime, TimeUnit.MILLISECONDS);//先处理第队列的剩余元素

                if (si == null) {
                    /* We timed out looking for more writes to batch, go ahead and flush immediately */
                    flush();
                    si = queuedRequests.take();
                }

                if (si == REQUEST_OF_DEATH) {
                    break;
                }
                LOG.info("*^^*进入中间处理器{}", si);
                long startProcessTime = Time.currentElapsedTime();
                ServerMetrics.getMetrics().SYNC_PROCESSOR_QUEUE_TIME.add(startProcessTime - si.syncQueueStartTime);

                // track the number of records written to the log
                if (zks.getZKDatabase().append(si)) { //将此次请求请写入txn事物日志 新建Log.1,并且会预留磁盘通过preAllocSize来设置),默认是64M
                    if (shouldSnapshot()) { //根据日志数量和日志大小判断是否数据入库
                        resetSnapshotStats();
                        // roll the log
                        zks.getZKDatabase().rollLog();
                        // take a snapshot
                        if (!snapThreadMutex.tryAcquire()) { //等待上一个snapshot写完
                            LOG.warn("Too busy to snap, skipping");
                        } else {
                            new ZooKeeperThread("Snapshot Thread") {
                                public void run() {
                                    try {
                                        zks.takeSnapshot();//保存data session到新的snap中,zkserver启动的时候会重新刷一次
                                    } catch (Exception e) {
                                        LOG.warn("Unexpected exception", e);
                                    } finally {
                                        snapThreadMutex.release();
                                    }
                                }
                            }.start();
                        }
                    }
                } else if (toFlush.isEmpty()) { //没有可写的包,处理本次请求
                    // optimization for read heavy workloads
                    // iff this is a read, and there are no pending
                    // flushes (writes), then just pass this to the next
                    // processor
                    if (nextProcessor != null) {
                        nextProcessor.processRequest(si);//进入后置处理
                        if (nextProcessor instanceof Flushable) {
                            ((Flushable) nextProcessor).flush();
                        }
                    }
                    continue;
                }
                toFlush.add(si);
                if (shouldFlush()) { //设置批量写入日志,批量更新
                    flush();
                }
                ServerMetrics.getMetrics().SYNC_PROCESS_TIME.add(Time.currentElapsedTime() - startProcessTime);
            }
        } catch (Throwable t) {
            handleException(this.getName(), t);
        }
        LOG.info("SyncRequestProcessor exited!");
    }

结束完上面的处理,可以进入最后一个处理器,finalprocessprocessor ,后置处理器逻辑主要是更新内存中的zktree,更新zxid cversion等,然后返回结果给客户端。主要逻辑集中在这个函数。可以看到最后面如果是个分布式请求,会将结果发送给一个ack队列中,然后给follwer同步,后续看zk集群一些源码也会有涉及。

// entry point for FinalRequestProcessor.java
    public ProcessTxnResult processTxn(Request request) {
        TxnHeader hdr = request.getHdr();
        processTxnForSessionEvents(request, hdr, request.getTxn()); //处理创建和关闭session,对于某些特定请求

        final boolean writeRequest = (hdr != null); //写请求,带有Hdr
        final boolean quorumRequest = request.isQuorum(); //分布式请求,带有修改的请求

        // return fast w/o synchronization when we get a read
        if (!writeRequest && !quorumRequest) { //读请求
            return new ProcessTxnResult();
        }
        synchronized (outstandingChanges) { //用来更新队列的块以及更新zxdb的数据
            //对比权限和更改内存数据 以及更新server的zxid cversion
            ProcessTxnResult rc = processTxnInDB(hdr, request.getTxn(), request.getTxnDigest());

            // request.hdr is set for write requests, which are the only ones
            // that add to outstandingChanges.
            if (writeRequest) {
                long zxid = hdr.getZxid();
                while (!outstandingChanges.isEmpty()
                        && outstandingChanges.peek().zxid <= zxid) { //已经更新过的请求队列可以出列了
                    ChangeRecord cr = outstandingChanges.remove();
                    ServerMetrics.getMetrics().OUTSTANDING_CHANGES_REMOVED.add(1);
                    if (cr.zxid < zxid) {
                        LOG.warn(
                            "Zxid outstanding 0x{} is less than current 0x{}",
                            Long.toHexString(cr.zxid),
                            Long.toHexString(zxid));
                    }
                    if (outstandingChangesForPath.get(cr.path) == cr) {
                        outstandingChangesForPath.remove(cr.path);
                    }
                }
            }

            // do not add non quorum packets to the queue.
            if (quorumRequest) {
                getZKDatabase().addCommittedProposal(request); //加入commit队列相当于ack了,发送到follower
            }
            return rc;
        }
    }

客户端会阻塞等到主线程,直达结果又返回,然后获取结果。

public ReplyHeader submitRequest(
        RequestHeader h,
        Record request,
        Record response,
        WatchRegistration watchRegistration,
        WatchDeregistration watchDeregistration) throws InterruptedException {
        ReplyHeader r = new ReplyHeader();
        Packet packet = queuePacket(
            h,
            r,
            request,
            response,
            null,
            null,
            null,
            null,
            watchRegistration,
            watchDeregistration);
        synchronized (packet) {  //加锁同步请求获取
            if (requestTimeout > 0) {
                // Wait for request completion with timeout
                waitForPacketFinish(r, packet); //加入requestTimeout超时等待
            } else {
                // Wait for request completion infinitely
                while (!packet.finished) {
                    packet.wait();//释放锁,等待,加同步
                }
            }
        }
        if (r.getErr() == Code.REQUESTTIMEOUT.intValue()) {
            sendThread.cleanAndNotifyState();
        }
        return r;
    }

 

看完整个客户端set操作流程,了解了zk客户端与服务端通信过程,能够更加了解zk的客户端和服务端网络模型,zk数据结构,数据的读写与保存,以及在分布式集群中,不同角色的处理逻辑。对整个zk的有了一个整体的概念。例如这时候让你想一下zk watch的原理,其实比较简单了,就是客户端创建一个监听事件,会在服务端保存一个cnxserver实例,该实例实际上是维持客户端与服务端的连接对象,然后zkserver会维持一个watchmanger来管理watch,当节点发生改变时,就会触发watch,最终利用cnxserver实例来通知客户端,让客户端知道节点的发生了什么事件。

这个是zk的单机模式,有了这些基础,后面取了解zk的集群模式,并且了解选主算法,会有很大的帮助。后续也会探究zk集群模式下的一些源码。 

 

 

 

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值