【zookeeper】客户端 底层实现

首先看下我们是怎么用zk客户端的。我们一般都会采用如下的代码:

ZooKeeper zk = new ZooKeeper("127.0.0.1:2183", 5000, new DefaultHandler());
然后创建的ZooKeeper实例是我们与服务端通讯的接口。所以从这个类开始看。

    public ZooKeeper(String connectString, int sessionTimeout, Watcher watcher)
        throws IOException
    {
        LOG.info("Initiating client connection, connectString=" + connectString
                + " sessionTimeout=" + sessionTimeout + " watcher=" + watcher);

        watchManager.defaultWatcher = watcher;
        cnxn = new ClientCnxn(connectString, sessionTimeout, this, watchManager);
        cnxn.start();
    }

这是我们使用使用的构造方法,有几个重载版本。这里需要传入三个参数:服务器的ip端口,等待timeout和一个默认的回调接口。zk是有事件机制的,关于事件会在另一篇中研究,这里只需要知道在zk构造方法中传入的watch是一个默认的watch,watchManger是zk客户端管理watch的类。默认的watch也可以在后续的其他事件注册中使用,把相应的boolean参数设置为true即可。

接下来有一个cnxn实例,从start方法可以看到这是一个线程。这个是zk客户端很重要的一个组件,用于处理zk的io,默认是一个java nio的实现。所以这里着重要分析的是zk的io处理实现。

所以,纵观ZooKeeper类,只是实现了客户端的业务相关的api,而底层的io是交给cnxn这个类来处理的。下面就挑一个zk客户端方法来看,比如最常见的getData方法,这个方法有同步异步两个版本,这里就看同步的:

    public byte[] getData(final String path, Watcher watcher, Stat stat)
        throws KeeperException, InterruptedException
     {
        final String clientPath = path;
        PathUtils.validatePath(clientPath);

        // the watch contains the un-chroot path
        WatchRegistration wcb = null;
        if (watcher != null) {
            wcb = new DataWatchRegistration(watcher, clientPath);
        }

        final String serverPath = prependChroot(clientPath);

        RequestHeader h = new RequestHeader();
        h.setType(ZooDefs.OpCode.getData);
        GetDataRequest request = new GetDataRequest();
        request.setPath(serverPath);
        request.setWatch(watcher != null);
        GetDataResponse response = new GetDataResponse();
        ReplyHeader r = cnxn.submitRequest(h, request, response, wcb);
        if (r.getErr() != 0) {
            throw KeeperException.create(KeeperException.Code.get(r.getErr()),
                    clientPath);
        }
        if (stat != null) {
            DataTree.copyStat(response.getStat(), stat);
        }
        return response.getData();
    }
首先根据watcher参数来注册watch,然后就构造了一个request对象,封装了getData请求,接着就调用了cnxn类的submitRequest方法来发送这个请求,最后可以发现,这个方法是阻塞的,会等待服务端发送回结果,最后返回请求的data。基本上所有的zk客户端的api实现逻辑都是这个模板。所以我们需要研究的是cnxn这个实例的类。

这个类名是ClientCnxn,专门用于客户端的io逻辑。

这个类里面有两个重要的内部类,一个是SendThread一个EventThread。看名字也可以清楚它们是两个实现了runnable接口的类。其中SendThread用于处理io,使用的是nio。EventThread用于处理收到的事件。还有两个重要的变量outgoingQueue和pendingQueue,outgoingQueue用于存放待发送的packet,一旦一个packet被发送,就会从outgoingQueue中移除,加入到pendingQueue中,等待服务端的返回。这里的顺序是可以保证的,先发送的那么就先进入pending,tcp的顺序可以保证每一次从服务端拿到的请求响应一定是pending中第一个packet的相应,这样完美解决了顺序和匹配的问题。

先看SendThread。

 class SendThread extends Thread {
        SelectionKey sockKey;

        private final Selector selector = Selector.open();

        final ByteBuffer lenBuffer = ByteBuffer.allocateDirect(4);

        ByteBuffer incomingBuffer = lenBuffer;

        boolean initialized;

        private long lastPingSentNs;

        long sentCount = 0;
        long recvCount = 0;
这是它的成员变量,有一个selector,会周期性地处理客户端端口的io事件。

我们只贴出关键性部分,也就是select以后得到了key的部分:

 selector.select(to);
                    Set<SelectionKey> selected;
                    synchronized (this) {
                        selected = selector.selectedKeys();
                    }
                    // Everything below and until we get back to the select is
                    // non blocking, so time is effectively a constant. That is
                    // Why we just have to do this once, here
                    now = System.currentTimeMillis();
                    for (SelectionKey k : selected) {
                        SocketChannel sc = ((SocketChannel) k.channel());
                        if ((k.readyOps() & SelectionKey.OP_CONNECT) != 0) {
                            if (sc.finishConnect()) {
                                lastHeard = now;
                                lastSend = now;
                                primeConnection(k);
                            }
                        } else if ((k.readyOps() & (SelectionKey.OP_READ | SelectionKey.OP_WRITE)) != 0) {
                            if (outgoingQueue.size() > 0) {
                                // We have something to send so it's the same
                                // as if we do the send now.
                                lastSend = now;
                            }
                            if (doIO()) {
                                lastHeard = now;
                            }
                        }
                    }
for循环会处理一次select到的全部key。基本上是判断事件类型,如果是连接成功,就调用primeConnection方法来处理连接事件。否则如果是read和write事件,那么就调用doIO方法来处理io事件。这里我们只看io部分。

doIO方法也很长。
内部分为了主要的两个case,一个是读事件,一个是写事件。

先看读事件:

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++;
                        readLength();
                    } else if (!initialized) {
                        readConnectResult();
                        enableRead();
                        if (!outgoingQueue.isEmpty()) {
                            enableWrite();
                        }
                        lenBuffer.clear();
                        incomingBuffer = lenBuffer;
                        packetReceived = true;
                        initialized = true;
                    } else {
                        readResponse();
                        lenBuffer.clear();
                        incomingBuffer = lenBuffer;
                        packetReceived = true;
                    }
                }
            }
发生读事件意味着服务端返回了一个数据,应该是客户端之前发送的请求。在这个处理读事件的case里面,首先调用了read方法,把字节流读到了成员变量buffer里面。然后就根据当前的状态判断如何处理独到的数据。如果说当前的客户端还没有初始化,那么说明什么,说明这些返回的数据是与客户端连接有关的,调用readConnectResult方法来处理连接过程。否则,这就是客户端请求返回的结果,调用readResponse方法处理。

再来看写事件:

 if (sockKey.isWritable()) {
                synchronized (outgoingQueue) {
                    if (!outgoingQueue.isEmpty()) {
                        ByteBuffer pbb = outgoingQueue.getFirst().bb;
                        sock.write(pbb);
                        if (!pbb.hasRemaining()) {
                            sentCount++;
                            Packet p = outgoingQueue.removeFirst();
                            if (p.header != null
                                    && p.header.getType() != OpCode.ping
                                    && p.header.getType() != OpCode.auth) {
                                pendingQueue.add(p);
                            }
                        }
                    }
                }
            }
如果是写事件,那么就意味着可以发送一次客户端的请求,也就是从outgoingQueue中取出一个packet,发送即可,再加入到pedingQueue。
            if (outgoingQueue.isEmpty()) {
                disableWrite();
            } else {
                enableWrite();
            }
最后修改select监听的事件,主要看两个队列是否有元素。

接下来看readResponse方法,也很长,不过很清楚。首先是一些特殊的case处理。

 if (replyHdr.getXid() == -2) {
                // -2 is the xid for pings
第一个是对ping的处理。

 if (replyHdr.getXid() == -4) {
            	 // -4 is the xid for AuthPacket               
                if(replyHdr.getErr() == KeeperException.Code.AUTHFAILED.intValue()) {
                    zooKeeper.state = States.AUTH_FAILED;                    
                    eventThread.queueEvent( new WatchedEvent(Watcher.Event.EventType.None, 
                            Watcher.Event.KeeperState.AuthFailed, null) );            		            		
                }
                if (LOG.isDebugEnabled()) {
                    LOG.debug("Got auth sessionid:0x"
                            + Long.toHexString(sessionId));
                }
                return;
            }
第二个是对auth数据的处理。

           if (replyHdr.getXid() == -1) {
                // -1 means notification
                if (LOG.isDebugEnabled()) {
                    LOG.debug("Got notification sessionid:0x"
                        + Long.toHexString(sessionId));
                }
                WatcherEvent event = new WatcherEvent();
                event.deserialize(bbia, "response");

                // convert from a server path to a client path
                if (chrootPath != null) {
                    String serverPath = event.getPath();
                    if(serverPath.compareTo(chrootPath)==0)
                        event.setPath("/");
                    else
                        event.setPath(serverPath.substring(chrootPath.length()));
                }

                WatchedEvent we = new WatchedEvent(event);
                if (LOG.isDebugEnabled()) {
                    LOG.debug("Got " + we + " for sessionid 0x"
                            + Long.toHexString(sessionId));
                }

                eventThread.queueEvent( we );
                return;
            }

第三个是对事件的处理,意味着注册是事件发生了,这时服务端返回相应的事件,客户端所做的就是把事件放入到一个事件queue中,让EventThread来处理。以上三种特殊的case处理完以后,就是请求数据的返回。比如getData等。

if (pendingQueue.size() == 0) {
                throw new IOException("Nothing in the queue, but got "
                        + replyHdr.getXid());
            }
            Packet packet = null;
            synchronized (pendingQueue) {
                packet = pendingQueue.remove();
            }
首先从一个pendingQueue中拿出一个待处理的packet,那么这次服务器返回的数据就是这个packet想要的。

try {
                if (packet.header.getXid() != replyHdr.getXid()) {
                    packet.replyHeader.setErr(
                            KeeperException.Code.CONNECTIONLOSS.intValue());
                    throw new IOException("Xid out of order. Got "
                            + replyHdr.getXid() + " expected "
                            + packet.header.getXid());
                }

                packet.replyHeader.setXid(replyHdr.getXid());
                packet.replyHeader.setErr(replyHdr.getErr());
                packet.replyHeader.setZxid(replyHdr.getZxid());
                if (replyHdr.getZxid() > 0) {
                    lastZxid = replyHdr.getZxid();
                }
                if (packet.response != null && replyHdr.getErr() == 0) {
                    packet.response.deserialize(bbia, "response");
                }

                if (LOG.isDebugEnabled()) {
                    LOG.debug("Reading reply sessionid:0x"
                            + Long.toHexString(sessionId) + ", packet:: " + packet);
                }
            } finally {
                finishPacket(packet);
            }
然后就会把相应的结果设置到packet的response中。最后调用finishPacket方法来唤醒客户端。所以这个是阻塞的方法。

finishPacket方法中只是调用了一下notify方法:

    private void finishPacket(Packet p) {
        if (p.watchRegistration != null) {
            p.watchRegistration.register(p.replyHeader.getErr());
        }

        if (p.cb == null) {
            synchronized (p) {
                p.finished = true;
                p.notifyAll();
            }
        } else {
            p.finished = true;
            eventThread.queuePacket(p);
        }
    }

那么再看一下是zk是如何发送一个packet的。就像前面说的,ZooKeeper的几乎所有业务方法都是调用cnxn的submit方法来发送请求的:

public ReplyHeader submitRequest(RequestHeader h, Record request,
            Record response, WatchRegistration watchRegistration)
            throws InterruptedException {
        ReplyHeader r = new ReplyHeader();
        Packet packet = queuePacket(h, r, request, response, null, null, null,
                    null, watchRegistration);
        synchronized (packet) {
            while (!packet.finished) {
                packet.wait();
            }
        }
        return r;
    }

这个方法会构造一个packet,加入到cnxn的outgoingQueue中,然后调用一个wait方法阻塞在这里,知道被notify,再返回结果。到目前为止,应该明白了zk的客户端是如何处理请求响应了。也是zk客户端io的处理过程。


接下来是事件的处理,主要在EventThread类里。

有一个阻塞队列用于存放所有的在上述readResponse里收到的event:

class EventThread extends Thread {
        private final LinkedBlockingQueue<Object> waitingEvents =
            new LinkedBlockingQueue<Object>();

接下来看核心的run方法:

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;
                       }
                    }
              }
           } catch (InterruptedException e) {
              LOG.error("Event thread exiting due to interruption", e);
           }

            LOG.info("EventThread shut down");
        }
每一次从queue里面拿一个事件出来,然后调用processEvent函数处理。

processEvent方法很长,但是基本是很多的if case,用于处理不同的事件类型。因为在submit一个packet的时候,我们会注册回调接口。所以这个时候process就直接从packet里面拿出回调接口处理就好。


总结就是,ClientCnxn类里面会开两个线程一个处理io一个处理事件,io有两个队列,事件有一个队列。

这是按照这个思路写的一个客户端:http://blog.csdn.net/u010900754/article/details/78391710

关于顺序的思考,首先客户端的顺序肯定没有问题,因为进入outgoing的顺序就是整个包的顺序,每一次都是从outgoing里拿一个发送,然后就进了pending,所以这个顺序是没问题的,客户端会保持这个packet的顺序,一旦进入了outgoing。然后tcp会保证接受顺序,也就是客户端先发哪一个,服务端就会先接受哪一个。所以服务端接受的顺序也和进入outgoing的顺序一样。那么服务端返回结果的顺序就不一定了,因为有的请求可能需要的处理时间长,有的短,完全有可能后收到的请求先处理完,但是肯定还是按照接受顺序来返回的,因为如果不是这样,那么pedingQueue就混乱了。所以猜测服务端也会有一个queue,顺序就是接受顺序,然后处理完以后会按照接受顺序返回结果。不过还没看服务端的代码。




  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值