【zookeeper】客户端 底层实现

来源:互联网 发布:数据库数据存储方式 编辑:程序博客网 时间:2024/06/03 11:17

首先看下我们是怎么用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,顺序就是接受顺序,然后处理完以后会按照接受顺序返回结果。不过还没看服务端的代码。




原创粉丝点击