深入了解ZooKeeper的Watcher机制的工作机制

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/carson0408/article/details/84142079

        Zookeeper提供了分布式数据的发布/订阅功能,多个订阅者同时监听某一个主题对象,当这个主题对象自身状态变化时,会通知所有订阅者,使它们作出相应的处理,而ZooKeeper实现这一功能的根本就是Watcher机制。

        ZooKeeper的Watcher机制主要包括客户端线程、客户端WatchManager和ZooKeeper服务器三部分。具体的流程主要是客户端向ZooKeeper服务器注册Watcher的同时,会将Watcher对象存储在客户端的WatchManager中。当ZooKeeper服务器端触发Watcher事件后,会向客户端发送通知,客户端线程从WatchManager中取出对应的Watcher对象来执行回调逻辑。

1.客户端注册Watcher

        首先,注册一个Watcher对象可以通过创建一个ZooKeeper客户端对象实例时或者使用ZooKeeper客户端的getData、getChildren和exist三个接口来向ZooKeeper服务器注册Watcher。在注册Wacher后,客户端首先会将当前客户端请求request进行标记,将其设置为Watcher监听,同时会封装一个Watcher的注册信息WatchRegistration对象,用于暂时保存数据节点路径和Watcher的对应关系。

        在ZooKeeper中,Packet可以看作一个最小通信协议单元,用于进行客户端与服务端之间的网络传输,任何需要传输的对象都需要包装成一个Packet对象。在ZooKeeper源码中,当使用异步操作时,都会出现一行代码:

cnxn.queuePacket(h, new ReplyHeader(), request, null, cb, clientPath,
                serverPath, ctx, null);

这说明操作结束之后会调用cnxn对象的queuePacket方法:

Packet queuePacket(RequestHeader h, ReplyHeader r, Record request,
            Record response, AsyncCallback cb, String clientPath,
            String serverPath, Object ctx, WatchRegistration watchRegistration)
    {
        Packet packet = null;
        synchronized (outgoingQueue) {
            if (h.getType() != OpCode.ping && h.getType() != OpCode.auth) {
                h.setXid(getXid());
            }
            packet = new Packet(h, r, request, response, null,
                    watchRegistration);
            packet.cb = cb;
            packet.ctx = ctx;
            packet.clientPath = clientPath;
            packet.serverPath = serverPath;
            if (!zooKeeper.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.wakeup();
        return packet;
    }

        从以上可以看出通过这个方法,WatchRegistration会被封装到Packet中,然后放入发送队列中等待客户端发送。随后,ZooKeeper客户端就会向服务端发送这个请求,同时等待请求的返回。这时,激活了客户端的sendThreand线程,该线程的readResponse方法负责接收来自服务端的响应,然后将Watcher注册到ZKWatchManager中进行管理。这里需要注意的是,在底层实际的网络传输序列化过程中,并没有将WatchRegistration对象完全地序列化到底层字节数组中去。这样可以减轻服务端资源开销以及网络传输开销。

2.服务端处理Watcher

Watcher触发

1.封装WatchedEvent

首先将通知状态、事件类型以及节点路径封装成一个WatchedEvent。

2.查询Watcher

根据数据节点的节点路径从watchTable中取出对应的Watcher。如果没有找到Watcher,说明没有任何客户端在该数据节点上注册过Watcher,直接退出。如果找到了Watcher,将其提取出来,同时会直接从watchTable和watch2Paths中将其删除。

注意:WatchManager是ZooKeeper服务端Watcher的管理者,其内部管理的watchTable和watch2Paths两个存储结构,其中watchTable是从数据节点路径的粒度来托管Watcher。watch2Paths是从Watcher的粒度来控制事件触发需要触发的数据节点。

3.调用process方法来触发Watcher

依次调用步骤2中找出的所有Watcher的process方法,这是ServerCnxn对应的process方法,而该process方法中,主要工作如下:

a.在请求头中标记"-1",表明当前是一个通知。

b.将WatchedEvent包装成WatcherEvent,以便进行网络传输序列化。

c.向客户端发送该通知。

因此可以看出,该方法并不是处理客户端Watcher真正的业务逻辑,而是借助当前客户端连接的ServerCnxn对象来实现对客户端的WatchedEvent传递,真正的客户端Watcher回调与业务逻辑执行都在客户端。

注意:以上服务端只是给客户端发送一个通知,内容仅包括说明这是什么引起的Watcher。而WatchedEvent会被包装成可序列化的WatcherEvent,可以在网络中进行传输。

3.客户端回调Watcher

SendThread接收事件通知

void readResponse() throws IOException {
            ByteBufferInputStream bbis = new ByteBufferInputStream(
                    incomingBuffer);
            BinaryInputArchive bbia = BinaryInputArchive.getArchive(bbis);
            ReplyHeader replyHdr = new ReplyHeader();

            replyHdr.deserialize(bbia, "header");
            if (replyHdr.getXid() == -2) {
                // -2 is the xid for pings
                if (LOG.isDebugEnabled()) {
                    LOG.debug("Got ping response for sessionid: 0x"
                            + Long.toHexString(sessionId)
                            + " after "
                            + ((System.nanoTime() - lastPingSentNs) / 1000000)
                            + "ms");
                }
                return;
            }
            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;
            }
            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;
            }
            if (pendingQueue.size() == 0) {
                throw new IOException("Nothing in the queue, but got "
                        + replyHdr.getXid());
            }
            Packet packet = null;
            synchronized (pendingQueue) {
                packet = pendingQueue.remove();
            }
            /*
             * Since requests are processed in order, we better get a response
             * to the first request!
             */
            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);
            }
        }

SendThread的response()方法负责接收这个客户端事件通知。首先会对replyHdr中xid进行判断,如果是-1,则说明是一个通知类型的响应。作以下处理:

1.反序列化:ZooKeeper客户端接到请求后,首先将字节流转换成WatcherEvent对象。

2.处理chrootPath:如果客户端设置了chrootPath属性,那么需要对服务端传过来的完整的节点路径进行chrootPath处理,生成客户端的一个相对节点路径。

3.还原WatchedEvent:将WatcherEvent对象转换成WatchedEvent。

4.回调Watcher:将WatchedEvent对象交给EventThread线程,在下一个轮询周期中进行Watcher回调。

EventThread处理事件通知

SendThread通过调用EventThread.queueEvent方法将事件传给EventThread线程。queueEvent方法会根据通知事件,从ZKWatchManager中取出所有相关的watcher,并将其放入waitingEvents队列中,进行串行同步处理。

4.总结

        了解了Watcher机制的工作机制后,有以下几点总结比较重要。

1.Watcher具有一次性,无论是服务端还是客户端,一旦一个Watcher被触发,ZooKeeper都会将其从相应的存储中移除。因此Watcher需要反复注册。

2.客户端串行执行:最终Watcher会被放入一个队列中串行执行。

3.WatchedEvent是ZooKeeper整个Watcher通知机制的最小单元,结构只包含:通知状态、事件类型和节点路径。Watcher只会通知客户端发生的事件,不会说明事件的具体内容。具体内容的逻辑都在客户端进行回调获取。同时,客户端向服务端注册Watcher的时候,并不会把真实的Watcher对象传递给服务端,仅仅只是在客户端请求中使用boolean类型属性进行了标记,同时服务端也仅仅保存了当前连接的ServerCnxn对象。这样设计大大减轻了网络开销和服务端内存开销。

4.WatchedEvent需要先转换为WatcherEvent序列化进行网络传输,然后客户端获取之后要反序列化然后再转换成WatchedEvent进行后续逻辑操作。

猜你喜欢

转载自blog.csdn.net/carson0408/article/details/84142079
今日推荐