zookeeper源码解析-客户端请求流程

本文详细解析了ZooKeeper客户端处理请求的过程,包括SendThread和EventThread的工作原理,客户端如何封装请求并发送到服务端,以及服务端响应的处理流程。

1.引入

前面介绍了客户端创建流程,最终调用了ClientCnxn.start方法启动了两个重要的线程SendThread和eventThread,这两个就是客户端处理请求的关键所在,所以在本篇中会逐一介绍这两个线程的作用。
为了方便介绍客户端请求处理过程,我们接着前面介绍的cli解析分析,当前以delete操作为例,最终介绍到了

        DeleteRequest request = new DeleteRequest();
        request.setPath(serverPath);
        request.setVersion(version);
        ReplyHeader r = cnxn.submitRequest(h, request, null, null);

其中cnxn.submitRequest就会通过客户端的请求机制访问zk服务端。

2.客户端请求

提交请求的处理流程:

  1. 将请求封装成packet等待线程处理(后面会介绍)
  2. 如果通过requestTimeout变量控制是否等待处理结果,如果小于0表示一直等到处理结束。
  3. 处理请求异常,主要是清除socket连接,清除处理队列等。
ClientCnxn.java
-------------------


    public ReplyHeader submitRequest(RequestHeader h, Record request,
            Record response, WatchRegistration watchRegistration,
            WatchDeregistration watchDeregistration)
            throws InterruptedException {
        ReplyHeader r = new ReplyHeader();
        // 将packet放入队列中
        Packet packet = queuePacket(h, r, request, response, null, null, null,
                null, watchRegistration, watchDeregistration);
        synchronized (packet) {
            // 如果reequestTimeout<=0表示一直等待处理结束(同步)
            if (requestTimeout > 0) {
                // 在超时时间内等待处理结果
                waitForPacketFinish(r, packet);
            } else {
                // Wait for request completion infinitely
                while (!packet.finished) {
                    packet.wait();
                }
            }
        }
        // 如果处理超时
        if (r.getErr() == Code.REQUESTTIMEOUT.intValue()) {
            sendThread.cleanAndNotifyState();
        }
        return r;
    }

客户端将请求封装成packet之后,如果校验状态没有问题之后,就会将packet添加到outgoingQueue队列中。

ClientCnxn.java
------------------

  public Packet queuePacket(RequestHeader h, ReplyHeader r, Record request,
            Record response, AsyncCallback cb, String clientPath,
            String serverPath, Object ctx, WatchRegistration watchRegistration,
            WatchDeregistration watchDeregistration) {
        Packet packet = null;
        // 组装packet对象
        packet = new Packet(h, r, request, response, watchRegistration);
        packet.cb = cb;
        packet.ctx = ctx;
        packet.clientPath = clientPath;
        packet.serverPath = serverPath;
        packet.watchDeregistration = watchDeregistration;
        synchronized (state) {
            // 服务状态不可用
            if (!state.isAlive() || closing) {
                conLossPacket(packet);
            } else {
                if (h.getType() == OpCode.closeSession) {
                    closing = true;
                }
                // 将有效的请求(packet)放入到outgoingQueue队列中
                outgoingQueue.add(packet);
            }
        }
        // 通知有新的请求,对于NIO连接来说,唤醒阻塞在selector.select上的线程
        sendThread.getClientCnxnSocket().packetAdded();
        return packet;
    }

从上面可以看出,一个正常的请求最终会放到outgoingQueue队列中,然后就不进行任何操作,那它是如何偷偷地发送到服务端地呢?这个就要看前面介绍的SendThread(ClientCnxn内部类)了,我们来看看这个线程究竟在干什么吧。

  1. 判断当前客户端和zk服务端是否建立连接,如果还没有建立连接,就创建
  2. 如果是安全连接,就先校验客户端连接
  3. 如果客户端请求超时,抛出异常
  4. 客户端和服务端连接正常,需要定期发送心跳信息到服务端,并更新客户端连接状态信息
  5. 如果连接的服务端处理仅读模式,需要重新寻找一个可以支持读写的服务端连接,并更新服务连接信息
  6. 处理客户端请求(核心逻辑)
ClientCnxn.java
-----------------

    public void run() {
        ..........
        while (state.isAlive()) {
                try {
                    if (!clientCnxnSocket.isConnected()) {
                        if (closing) {
                            break;
                        }
                        if (rwServerAddress != null) {
                            serverAddress = rwServerAddress;
                            rwServerAddress = null;
                        } else {
                            serverAddress = hostProvider.next(1000);
                        }
                        // 如果没有连接服务端,重新连接
                        startConnect(serverAddress);
                        clientCnxnSocket.updateLastSendAndHeard();
                    }

                    if (state.isConnected()) {
                            // 安全连接配置...省略
                            // 发送认证事件
                            if (sendAuthEvent) {
                                eventThread.queueEvent(new WatchedEvent(
                                      Watcher.Event.EventType.None,
                                      authState,null));
                                if (state == States.AUTH_FAILED) {
                                  eventThread.queueEventOfDeath();
                                }
                            }
                        }
                        to = readTimeout - clientCnxnSocket.getIdleRecv();
                    } else {
                        to = connectTimeout - clientCnxnSocket.getIdleRecv();
                    }

                    // 如果session已经超时
                    if (to <= 0) {
                        String warnInfo;
                        warnInfo = "Client session timed out, have not heard from server in "
                            + clientCnxnSocket.getIdleRecv()
                            + "ms"
                            + " for sessionid 0x"
                            + Long.toHexString(sessionId);
                        LOG.warn(warnInfo);
                        throw new SessionTimeoutException(warnInfo);
                    }

                    // ------------------------发送心跳------------------------
                    if (state.isConnected()) {
                        int timeToNextPing = readTimeout / 2 - clientCnxnSocket.getIdleSend() - 
                        		((clientCnxnSocket.getIdleSend() > 1000) ? 1000 : 0);
                        if (timeToNextPing <= 0 || clientCnxnSocket.getIdleSend() > MAX_SEND_PING_INTERVAL) {
                            sendPing();
                            clientCnxnSocket.updateLastSend();
                        } else {
                            if (timeToNextPing < to) {
                                to = timeToNextPing;
                            }
                        }
                    }
                    // ------------------------发送心跳------------------------


                    // 如果是仅读模式,寻找读写服务器
                    if (state == States.CONNECTEDREADONLY) {
                        long now = Time.currentElapsedTime();
                        int idlePingRwServer = (int) (now - lastPingRwServer);
                        if (idlePingRwServer >= pingRwTimeout) {
                            lastPingRwServer = now;
                            idlePingRwServer = 0;
                            pingRwTimeout =
                                Math.min(2*pingRwTimeout, maxPingRwTimeout);
                            pingRwServer();
                        }
                        to = Math.min(to, pingRwTimeout - idlePingRwServer);
                    }

                    // 处理请求的关键逻辑,详情看实现类。
                    clientCnxnSocket.doTransport(to, pendingQueue, ClientCnxn.this);  

clientCnxnSocket.doTransport将真正看到客户端是如何处理请求的,由于它有NIO和Netty两种实现方式,这里介绍默认的实现方式,也就是NIO(对应ClientCnxnSocketNIO)。

  1. 如果是客户端和服务端连接行为,就添加认证信息,watcher等信息到outgoingQueue队列队首等待处理。
  2. 如果是读写行为,调用doIO方法处理。
ClientCnxnSocketNIO.java
----------------------

 @Override
    void doTransport(int waitTimeOut, List<Packet> pendingQueue, ClientCnxn cnxn)
            throws IOException, InterruptedException {
        selector.select(waitTimeOut);
        Set<SelectionKey> selected;
        synchronized (this) {
            selected = selector.selectedKeys();
        }
        for (SelectionKey k : selected) {
            SocketChannel sc = ((SocketChannel) k.channel());
            if ((k.readyOps() & SelectionKey.OP_CONNECT) != 0) {
                // 处理客户端连接行为
                if (sc.finishConnect()) {
                    updateLastSendAndHeard();
                    updateSocketAddresses();
                    sendThread.primeConnection();
                }
            } else if ((k.readyOps() & (SelectionKey.OP_READ | SelectionKey.OP_WRITE)) != 0) {
                // 处理读写行为(核心)
                doIO(pendingQueue, cnxn);
            }
        }
        .....
    }

doIO这里我们分成写流程和读流程。

客户端请求流程(写流程):

  1. 取出outgoingQueue队首的packet,并序列化
  2. 向zk服务端发送序列化之后的packet
  3. outgoingQueue移除该packet,pendingQueue添加该packet表明这个packet等待处理结束。
 if (sockKey.isWritable()) {
            // 从outgoingQueue中获取队列的第一个元素
            Packet p = findSendablePacket(outgoingQueue,
                    sendThread.tunnelAuthInProgress());

            if (p != null) {
                updateLastSend();
                if (p.bb == null) {
                    if ((p.requestHeader != null) &&
                            (p.requestHeader.getType() != OpCode.ping) &&
                            (p.requestHeader.getType() != OpCode.auth)) {
                        // 设置客户端请求序号
                        p.requestHeader.setXid(cnxn.getXid());
                    }
                    // 设置packet的序列化请求对象
                    p.createBB();
                }
                // 向zookeeper的服务端发送请求
                sock.write(p.bb);
                if (!p.bb.hasRemaining()) {
                    sentCount.getAndIncrement();
                    // 移除packet
                    outgoingQueue.removeFirstOccurrence(p);
                    if (p.requestHeader != null
                            && p.requestHeader.getType() != OpCode.ping
                            && p.requestHeader.getType() != OpCode.auth) {
                        synchronized (pendingQueue) {
                            // 等待队列中添加packet
                            pendingQueue.add(p);
                        }
                    }
                }
            }

客户端读流程(服务端响应),主要通过sendThread.readResponse实现,由于实现代码比较多直接,在这里直接描述实现过程。

  1. 解析响应的header信息,反序列化成ReplyHeader对象(该对象主要需要关注err属性,err=0表示响应没有出现问题)
  2. 处理replyHeader中的xid属性:
  • -2: 客户端ping响应结果,直接忽略
  • -4: 服务端认证请求失败,将调用eventThread.queueEvent添加一个客户端需要处理的认证失败的WatcherEvent对象
  • -1: 说明根据客户端的请求路径,触发了watcher需要将响应反序列成WatcherEvent对象,等待本客户端处理。
 if (sockKey.isReadable()) {
            // 读取服务端返回结果
            int rc = sock.read(incomingBuffer);
            if (rc < 0) {
                throw new EndOfStreamException(
                        "Unable to read additional data from server sessionid 0x"
                                + Long.toHexString(sessionId)
                                + ", likely server has closed socket");
            }
            if (!incomingBuffer.hasRemaining()) {
                incomingBuffer.flip();
                if (incomingBuffer == lenBuffer) {
                    recvCount.getAndIncrement();
                    readLength();
                } else if (!initialized) {
                    // 读取初始化结果
                    readConnectResult();
                    enableRead();
                    if (findSendablePacket(outgoingQueue,
                            sendThread.tunnelAuthInProgress()) != null) {
                        enableWrite();
                    }
                    lenBuffer.clear();
                    incomingBuffer = lenBuffer;
                    updateLastHeard();
                    initialized = true;
                } else {
                    sendThread.readResponse(incomingBuffer);
                    lenBuffer.clear();
                    incomingBuffer = lenBuffer;
                    updateLastHeard();
                }
            }
        }

在上面介绍了,处理服务端的响应时主要是会调用eventThread.queueEvent方法,最终将客户端需要触发的watcher列表和该watcherEvent对象添加到waitingEvents,即客户端等待处理响应的队列中。那这个队列的数据是如何进行消费的呢?这个时候就需要看看EventThread线程执行过程了。

   
   // eventThread线程执行
    public void run() {
           try {
              isRunning = true;
              while (true) {
                  // 从等待线程中获取事件(先进先出)
                 Object event = waitingEvents.take();
                 if (event == eventOfDeath) {
                    wasKilled = true;
                 } else {
                     // 如果不是结束事件,处理事件
                    processEvent(event);
                 }
                 if (wasKilled)
                    synchronized (waitingEvents) {
                       if (waitingEvents.isEmpty()) {
                          isRunning = false;
                          break;
                       }
                    }
              }
       ......       
           
        // 处理事件的核心逻辑   
       private void processEvent(Object event) {
          try {
              if (event instanceof WatcherSetEventPair) {
                  // 存在多个watcher
                  // each watcher will process the event
                  WatcherSetEventPair pair = (WatcherSetEventPair) event;
                  for (Watcher watcher : pair.watchers) {
                      try {
                          // 多个watcher处理该事件
                          watcher.process(pair.event);
                      } catch (Throwable t) {
                          LOG.error("Error while calling watcher ", t);
                      }
                  }

所以eventThread主要就是做了两件事情:

  1. 监听waitingEvents队列,并取出等待客户端处理的事件
  2. 如果存在事件,会调用对应的watcher执行(客户端在请求服务端时会添加一个watcher对象同时指定watcher的处理方式)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值