Zookeeper--Watcher详解

阿凡达2018-07-10 11:29

Zookeeper系统许多重要的功能都是基于Watcher机制实现的,比如发布订阅模型,分布式通知/协调模型等。本质上来说,它更像是一个分布式的观察者模式实现,对模式设计比较熟悉的同学会对此有更好的理解。本文分三个部分,第一部分详解Server端和Client端是通过什么数据结构对Watcher进行管理维护的;第二部分从注册Watcher和Watcher触发两个方面介绍下Zookeeper是如何工作的;第三部分结合代码对Watcher的常见问题进行一下解释


第一部分:Zookeeper对Watcher的管理维护数据结构

  • ZKClient端:ZKWatchManager类来对Watcher进行管理,其中重要的数据结构如下:
private final Map<String, Set<Watcher>> dataWatches =
            new HashMap<String, Set<Watcher>>();
        private final Map<String, Set<Watcher>> existWatches =
            new HashMap<String, Set<Watcher>>();
        private final Map<String, Set<Watcher>> childWatches =
            new HashMap<String, Set<Watcher>>();
可见,实际上它维护的是三个HashMap,map的key是节点路径,value是注册在这个节点上的所有Watcher。
  • ZKServer端:WatchManager类对Watcher进行管理,重要的数据结构如下:
 private final HashMap<String, HashSet<Watcher>> watchTable =
        new HashMap<String, HashSet<Watcher>>();
    private final HashMap<Watcher, HashSet<String>> watch2Paths =
        new HashMap<Watcher, HashSet<String>>();
首先要对ZKServer端的Watcher进行解释,这里的Watcher并不是用户定义的watcher,而是ServerCnxn,代表一个从Client到Server的连接。
watchTable定义了节点Node与Client连接的映射关系,即一个节点对应一组注册到此节点上的Client,这样这个节点发生变化可以通过这样的映射找到所有应该收到通知的Client。
watch2Paths和watchTable刚好是相反的结构,是一个Client连接与节点的映射关系,它的主要作用在于:一个连接关闭的时候,需要将这个连接相关的所有watch都移除,有这样一个数据结构就可以很简单地做到这一点。

第二部分:Zookeeper是如何注册Watcher和管理Watcher触发的
  1. 注册Watcher流程
                                                             
用户请求某个数据的时候,同时要求给节点注册一个watcher:
  • ZKClient接收到请求之后,会将请求封装成一个Packet发送给ZKServer. 
     这里需要注意两点:
     (1)ZKClient并没有把真实的Watcher发送给ZKServer,而只是给Server发送此请求是否注册了Watch
     (2)ZKClient在此时并没有将Watcher注册到本地的数据结构WatcherManager中,而只是生成一个WatchRegistration对象,真正在Client端注册是在最后一步。
  • ZKServer接收到ZKClient发送的信息之后首先会调用WatcherManager.addWatch函数将Watcher对象注册到对应节点上。
     注意: 步骤1说到了ZKClient并没有发送真正的Watcher给Server,所以步骤2中的Watcher对象并不是真实的Watcher,此时的watcher实际上是ZKClient与ZKServer连接通道ServerCnxn。因此在Server端WatcherManager维护的是节点和注册到此节点上的Client的映射关系,如果此节点数据发生了变化,会通过此映射关系定位到应该向哪些Client发送通知。
  • ZKServer根据用户请求将节点数据响应给用户。
  • ZKClient接收到成功响应之后再去调用WatchRegistration对象的register方法将实际Watcher和节点Path注册到Map<String, Set<Watcher>> dataWatches中。
 2.     Watcher触发流程
                                                       
  • ZKClient请求改变某个节点上的数据,即setData(path,data)
  • ZKServer接收请求信息之后会进行各种预处理,最后会调用DataTree中的setData函数,首先更新数据存储中对应节点的数据,然后调用triggerWatcher函数唤醒注册在这个节点上的所有watcher
  • WatcherManager首先会将每个被唤醒的Watcher从watcherTable中移除,再调用watcher.process方法。每个Watcher实际上是ServerCnxn,而watcher.process方法会调用sendResponse方法将序列化后的事件发送给对应的ZKClient。
    public void process(WatchedEvent event) {
        ReplyHeader h = new ReplyHeader(-1, -1L, 0);
   
        WatcherEvent e = event.getWrapper();
        try {
            sendResponse(h, e, "notification");
        } catch (IOException e1) {
            if (LOG.isDebugEnabled()) {
                LOG.debug("Problem sending to " + getRemoteSocketAddress(), e1);
            }
            close();
        }
    }
  • ZKClient端clientcnxn模块接收到响应之后交由sendThread处理。sendThread先反序列化该事件,再将反序列化之后的事件对象放入中间队列waitingEvents中。
  • EventThread会不断轮询队列waitingEvents,如果有响应数据,就会回调用户重写的watcher进行处理。
 
 第三部分:FAQ
1. watch是一次性的
解释:每次watch触发的时候,ZKServer端都会将对应的记录从watchTable中移除掉,这样下次watch触发是不会找到上次注册的连接对象的。
解决方案:重新注册。
2. 同一个ZK客户端,反复对同一个ZK节点(znode)注册相同的watcher,是无效的,最终只会有一个生效。
3. Client与Server之间的连接因为某些原因断开之后,并且在session_timeout时间内没有重新连上导致session过期,此Client在节点上注册的所有watcher还有效吗?
as:失效了。
解释:这就是watch2Paths存在的实际意义。ServerCnxn在close的时候会调用watch2Paths.remove(watcher)方法将watch2Paths里相应的记录移除掉,然后根据返回的paths,将watchTable中的相应记录也清掉。这样就使得此连接上的所有watcher都失效。
解决方案:重新注册。
4. 为什么ZK不提供一个永久性的watcher注册机制
不支持永久注册是因为ZK无法保证性能的原因
5. 节点数据的版本变化会触发NodeDataChanged,注意,这里特意说明了是版本变化。存在这样的情况,只要成功执行了setData()方法,无论内容是否和之前一致,都会触发NodeDataChanged。
6.  对某个节点注册了watch,但是节点被删除了,那么注册在这个节点上的watches都会被移除。

本文来自网易实践者社区,经作者范欣欣授权发布。