【Zookeeper源码阅读】从源码角度分析watcher实现原理

521 阅读7分钟

大家都知道Zookeeper有个非常重要的机制-Watcher机制。总的来说,Watcher机制可以分为三个过程:客户端注册Watcher、服务器处理Watcher、客户端保存路劲与watcher处理逻辑之间的映射关系和服务端事件触发。客户端注册 watcher 有 3 种方式,getDataexistsgetChildren。本文将会从源码角度分析watcher实现原理。

一个能触发watcher的代码示例

// 实例化ZooKeeper客户端 传入watcher
ZooKeeper zookeeper = new ZooKeeper("127.0.0.1:2181", 4000,
        event -> System.out.println("event.type" + event.getType()));
//创建节点
zookeeper.create("/watch","0".getBytes(), ZooDefs.Ids. OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
//注册监听
zookeeper.exists("/watch",true);
Thread.sleep(1000);
//修改节点的值触发监听
zookeeper.setData("/watch", "1".getBytes(),-1) ;
System.in.read();

接下来,我们就按上述示例代码来分析watcher实现原理。

ZooKeeper API 的初始化过程

在创建一个ZooKeeper客户端对象实例时,我们通过new Watcher()向构造方法中传入一个默认的Watcher, 这个Watcher将作为整个ZooKeeper会话期间的默认Watcher,会一直被保存在客户端ZKWatchManagerdefaultWatcher中。代码如下:

image.png

ClientCnxn:是Zookeeper客户端和Zookeeper服务器端进行通信和事件通知处理的主要类,它内部包含两个类:

  1. SendThread:负责客户端和服务器端的数据通信, 也包括事件信息的传输。
  2. EventThread:主要在客户端回调注册的Watchers进行通知处理。

ClientCnxn 初始化

image.png

服务端接收请求处理流程

服务端有一个NIOServerCnxn类,用来处理客户端发送过来的请求。

QuorumPeer.start()

image.png

NIOServerCnxnFactory.run()

启动一个NIO socket监听客户端请求。 image.png

NIOServerCnxn.doIO(SelectionKey k)

收到客户端请求会调用这个方法。 image.png

NIOServerCnxn.readPayload()

image.png

NIOServerCnxn.readRequest()

调用了zkServer.processPacket()处理客户端传送过来的数据包 image.png

zkServer.processPacket()

负责在服务端提交当前请求 image.png

firstProcessor的请求链组成

firstProcessor的初始化是在ZookeeperServer的setupRequestProcessor中完成的,代码如下: image.png

从上面我们可以看到firstProcessor的实例是一个PrepRequestProcessor,而这个构造方法中又传递了一个Processor构成了一个调用链。

RequestProcessor syncProcessor = new SyncRequestProcessor(this, finalProcessor);

syncProcessor的构造方法传递的又是一个 Processor,对应的是 FinalRequestProcessor; 所以整个调用链是 PrepRequestProcessor -> SyncRequestProcessor - >FinalRequestProcessor

PredRequestProcessor.processRequest(si);

通过上面了解到调用链关系以后,我们继续再看firstProcessor.processRequest(si)会调用到 PrepRequestProcessorimage.png

processRequest只是把request添加到submittedRequests中,根据前面的经验,很自然的想到这里又是一个异步操作。而subittedRequests又是一个阻塞队列。

LinkedBlockingQueue submittedRequests = new LinkedBlockingQueue();

PrepRequestProcessor个类又继承了线程类,因此我们直接找到当前类中的run()方法如下: image.png

PrepRequestProcessor.pRequest(request)

预处理这块的代码太长,就不好贴了。前面的N行代码都是根据当前的OP类型 进行判断和做相应的处理,在这个方法中的最后一行中,我们会看到如下代码:

nextProcessor.processRequest(request);

SyncRequestProcessor.processRequest(request)

image.png

这个方法的代码也是一样,基于异步化的操作,把请求添加到queuedRequets中, 那么我们继续在当前类找到 run()方法:

image.png image.png

FinalRequestProcessor. processRequest()

FinalRequestProcessor.processRequest 方法并根据Request对象中的操作更新内存中Session信息或者znode数据。这块代码有小300多行,就不全部贴出来了,我们直接定位到关键代码,根据客户端的OP类型找到如下的代码:

image.png

客户端接收服务端处理完成的响应

ClientCnxnSocketNIO.doIO()

服务端处理完成以后,会通过 NIOServerCnxn.sendResponse()发送返回的响应信息, 客户端会在 ClientCnxnSocketNIO.doIO() 接收服务端的返回,注意一下SendThread.readResponse(),接收服务端的信息进行读取: image.png

SendThread.readResponse()

这个方法里面主要的流程如下: 首先读取header

  • 如果其xid == -2,表明是一个pingresponse,return
  • 如果xid == -4 ,表明是一个 AuthPacketresponse,return
  • 如果 xid == -1,表明是一个notification,此时要继续读取并构造一个event,通过 EventThread.queueEvent 发送,return;
  • 其它情况下: 从pendingQueue拿出一个Packet,校验后更新packet信息。

image.png image.png

finishPacket()

主要功能是把从Packet中取出对应的Watcher并注册到ZKWatchManager中去。

image.png

watchRegistration,熟悉吗?在组装请求的时候,我们初始化了这个对象。 把watchRegistration子类里面的Watcher实例放到ZKWatchManagerexistsWatches 中存储起来。

WatchRegistration.register

image.png

下面这段代码是客户端存储 watcher 的几个map集合,分别对应三种注册监听事件:

image.png

总的来说,当使用 ZooKeeper构造方法或者使用getDataexistsgetChildren三个接口来向 ZooKeeper 服务器注册Watcher的时候,首先将此消息传递给服务端,传递成功后,服务端会通知客户端,然后客户端将该路径和 Watcher 对应关系存储起来备用

EventThread.queuePacket()

finishPacket()方法最终会调用eventThread.queuePacket(), 讲当前的数据包添加到等待事件通知的队列中: image.png

事件触发

前面这么长的说明,只是为了说明事件的注册流程,最终的触发,还得需要通过事务型操作来完成。在我们最开始的案例中,通过如下代码去完成了事件的触发:

zookeeper.setData(“/watch”, “1”.getByte(),-1) ; //修改节点的值触发监听

前面的客户端和服务端对接的流程就不再重复讲解了,交互流程是一样的,唯一的差别在于事件触发了。

服务端的事件响应 DataTree.setData()

image.png

WatcherManager. triggerWatch()

image.png

Watcher.process(e);

还记得我们在服务端绑定事件的时候,watcher绑定是是什么?是ServerCnxn, 所以w.process(e),其实调用的应该是ServerCnxnprocess()方法。而servercnxn又是一个抽象方法,有两个实现类,分别是:NIOServerCnxnNettyServerCnxn。那接下来我们扒开NIOServerCnxn这个类的process()方法看看究竟。 image.png

那接下里,客户端会收到这个response,触发SendThread.readResponse()方法。

客户端处理事件响应

SendThread.readResponse()

这块代码上面已经贴过了,所以我们只挑选当前流程的代码进行讲解,按照前面我们将到过的,notifacation通知消息的xid 为-1,意味着直接找到-1的判断进行分析。

image.png

eventThread.queueEvent()

SendThread接收到服务端的通知事件后,会通过调用EventThread类的queueEvent方法将事件传给 EventThread线程,queueEvent方法根据该通知事件, 从ZKWatchManager中取出所有相关的 Watcher,如果获取到相应的Watcher,就会让Watcher移除失效。

image.png

Watcher.Meterialize()

通过dataWatches或者existWatches或者childWatchesremove取出对应的watch表明客户端 watch 也是注册一次就移除

同时需要根据keeperStateeventTypepath返回应该被通知的Watcher集合。

waitingEvents.add()

waitingEventsEventThread这个线程中的阻塞队列,很明显,又是在我们第一步操作的时候实例化的一个线程。从名字可以指导,waitingEvents是一个待处理 Watcher 的队列,EventThreadrun()方法会不断从队列中取数据,交由processEvent方法处理:

EventThread.ProcessEvent()

由于这块的代码太长,我只把核心的代码贴出来,这里就是处理事件触发的核心代码。 image.png

总结

watcher机制的实现流程可分为以下四个步骤:

  1. 客户端注册watcher:通过getDataexistsgetChildren,可以向服务端注册watcher。这个步骤中,客户端需要告诉服务端哪个路径path下是否需要注册watcher。因此发送给服务端的指令中包括路径path、是否需要注册watcher。
  2. 服务端处理watcher:服务端接收到客户端的指令之后,需要将包含watcher的相关数据保存起来。具体来说,要保存三个信息,即哪个客户端的那个路径注册了watcher。
  3. 客户端保存路径与watcher处理逻辑之间的映射关系:在服务端处理完客户端注册watcher指令之后,客户端会将路径和对应的watcher处理逻辑的映射关系保存起来,为回调处理做准备。
  4. 服务端事件触发:在注册了watcher的路径中,如果数据发生变化就会触发相应的watcher。具体来说,服务端会找到path对应的watcher,调用执行方法。在服务端,这个watcher的实现就是一个发送notification的通知。客户端收到该通知后,会遍历该路径下所有的watcher,并调用执行方法,此时调用的就是真实的处理逻辑。

原创不易,觉得文章写得不错的小伙伴,点个赞👍 鼓励一下吧~

欢迎关注我的开源项目:一款适用于SpringBoot的轻量级HTTP调用框架