Watcher机制是Zookeeper实现分布式协调服务的重要特性。
Watcher机制实际上与观察者模式类似,也可看作是一种观察者模式在分布式场景下的实现方式。

Zookeeper watch是一种监听通知机制。Zookeeper所有的读操作getData(), getChildren()和 exists()都可以设置监视(watch),监视事件可以理解为一次性的触发器

Zookeeper提供了数据的发布/订阅功能,多个客户端可同时监听某一znode节点,当该znode节点的自身状态发生变化时(节点下的子节点列表改变,就是发生了增删改查当前节点和子节点,也会触发watcher事件.),例如节点内容改变,比如说增加属性,或者更新或者这个节点被删除了,都会触发一个watcher事件,该机制在znode节点发生变化时会异步通知订阅这个znode节点的客户端,因此这个客户端不必在Watcher注册后轮询阻塞,从而减轻了这个客户端压力。

触发了watcher事件之后,会实时、主动通知所有订阅的客户端,使得他们感知到节点已经发生了变化.

image.png

Watch的三个关键点

(一次性触发)One-time trigger

当设置监视的数据发生改变时,该监视事件会被发送到客户端,例如,如果客户端调用了getData(“/znode1”, true) 并且稍后 /znode1 节点上的数据发生了改变或者被删除了,客户端将会获取到 /znode1 发生变化的监视事件,而如果 /znode1 再一次发生了变化,除非客户端再次对/znode1 设置监视,否则客户端不会收到事件通知。

(发送至客户端)Sent to the client

Zookeeper客户端和服务端是通过 socket 进行通信的,由于网络存在故障,所以监视事件很有可能不会成功地到达客户端,监视事件是异步发送至监视者的,Zookeeper 本身提供了顺序保证(ordering guarantee):即客户端只有首先看到了监视事件后,才会感知到它所设置监视的znode发生了变化(a client will never see a change for which it has set a watch until it first sees the watch event)。

网络延迟或者其他因素可能导致不同的客户端在不同的时刻感知某一监视事件,但是不同的客户端所看到的一切具有一致的顺序。

(被设置 watch 的数据)The data for which the watch was set

这意味着znode节点本身具有不同的改变方式。你也可以想象 Zookeeper 维护了两条监视链表:数据监视和子节点监视(data watches and child watches) getData() 和exists()设置数据监视,getChildren()设置子节点监视。或者你也可以想象 Zookeeper 设置的不同监视返回不同的数据,getData() 和 exists() 返回znode节点的相关信息,而getChildren() 返回子节点列表。

因此,setData() 会触发设置在某一节点上所设置的数据监视(假定数据设置成功),而一次成功的create() 操作则会出发当前节点上所设置的数据监视以及父节点的子节点监视。一次成功的 delete操作将会触发当前节点的数据监视和子节点监视事件,同时也会触发该节点父节点的child watch。

Zookeeper 中的监视是轻量级的,因此容易设置、维护和分发。当客户端与 Zookeeper 服务器失去联系时,客户端并不会收到监视事件的通知,只有当客户端重新连接后,若在必要的情况下,以前注册的监视会重新被注册并触发,对于开发人员来说这通常是透明的。

只有一种情况会导致监视事件的丢失,即:通过exists()设置了某个znode节点的监视,但是如果某个客户端在此znode节点被创建和删除的时间间隔内与zookeeper服务器失去了联系,该客户端即使稍后重新连接 zookeeper服务器后也得不到事件通知。

工作机制

(1)客户端注册 watcher
(2)服务端处理 watcher
(3)客户端回调 watcher

客户端注册Watcher的流程?

1、客户端注册Watcher实现
2、调用getData()/getChildren()/exist()三个API,传入Watcher对象
3、标记请求request,封装Watcher到WatchRegistration
4、封装成Packet对象,发服务端发送request
5、收到服务端响应后,将Watcher注册到ZKWatcherManager中进行管理
6、请求返回,完成注册。

服务端处理Watcher的流程?

1、服务端接收Watcher并存储
接收到客户端请求,处理请求判断是否需要注册Watcher,需要的话将数据节点的节点路径和ServerCnxn(ServerCnxn代表一个客户端和服务端的连接,实现了Watcher的process接口,此时可以看成一个Watcher对象)存储在WatcherManager的WatchTable和watch2Paths中去。

2、Watcher触发

  • 以服务端接收到 setData() 事务请求触发NodeDataChanged事件为例:
  • 封装WatchedEvent
  • 将通知状态(SyncConnected)、事件类型(NodeDataChanged)以及节点路径封装成一个WatchedEvent对象
  • 查询Watcher
  • 从WatchTable中根据节点路径查找Watcher
    没找到;说明没有客户端在该数据节点上注册过Watcher
    找到;提取并从WatchTable和Watch2Paths中删除对应Watcher(从这里可以看出Watcher在服务端是一次性的,触发一次就失效了)

3、调用process方法来触发Watcher
这里process主要就是通过ServerCnxn对应的TCP连接发送Watcher事件通知。

客户端回调 Watcher流程?

1、客户端SendThread线程接收事件通知,交由 EventThread线程回调 Watcher。
2、客户端的Watcher机制同样是一次性的,一旦被触发后,该 Watcher就失效了。

Zookeeper对节点的 watch监听通知是永久的吗?为什么不是永久的?

不是永久的。
官方声明:一个 Watch事件是一个次性的触发器,当被设置了 Watch的数据发生了改变的时候,则服务器将这个改变发送给设置了 Watch的客户端,以便通知它们。

原因:
如果服务端变动频繁,而监听的客户端很多情况下,每次变动都要通知到所有的客户端,给网络和服务器造成很大压力。一般是客户端执行getData(“/节点”,true),如果节点A发生了变更或删除,客户端会得到它的 watch事件,但是在之后节点A又发生了变更,而客户端又没有设置 watch事件,就不再给客户端发送。

在实际应用中,很多情况下,我们的客户端不需要知道服务端的每一次变动,我只要最新的数据即可。

Watcher特性

一次性

Watcher是一次性的,无论是服务端还是客户端一旦被触发一次就会立即销毁,再次使用时需要重新注册,因此,开发人员在Watcher的时候上要记住的一点是需要反复注册,
这样的设计有效地减轻了服务端的压力,试想一下,如果注册一个Watcher之后一直有效,那么,针对那些更新非常频繁的节点,服务端会不断地向客户端发送事件通知,这无论对于网络还是服务端的性能的影响都非常大.

客户端顺序回调

Watcher回调是顺序串行化执行的,只有回调后客户端才能看到最新的数据状态。一个Watcher回调逻辑不应该太多,以免影响别的watcher执行

轻量级

Watcher 通知非常简单,结构上只包含通知状态、事件类型和节点路径只会告诉客户端发生了事件,而不会说明事件的具体内容。
列如针对NodeDataChanged事件,Zookeeper的Watcher只会通知客户端指定数据节点的数据内容发生了变更,而对于原始数据以及变更后的新数据都无法从这个事件中直接获取到,而是需要客户端主动重新去获取数据—-这也是zookeeper的watcher机制的一个非常重要的特性.
客户端向服务端注册 Watcher 的时候,并不会把客户端真实的 Watcher 对象实体传递到服务端,仅仅是在客户端请求中使用 boolean 类型属性进行了标记,同时服务端也仅仅是保存了当前连接的ServerCnxn对象
如此轻量的Watcher机制设计,在网络开销和服务端内存开销上都是非常廉价的.

时效性

Watcher只有在当前session彻底失效时才会无效,若在session有效期内快速重连成功,则watcher依然存在,仍可接收到通知;

watcher event 异步发送

  1. watcher event 异步发送 watcher 的通知事件从 server 发送到 client 是异步的,这就存在一个问题,不同的客户端和服务器之间通过 socket 进行通信,由于网络延迟或其他因素导致客户端在不通的时刻监听到事件,由于 Zookeeper 本身提供了 ordering guarantee,即客户端监听事件后,才会感知它所监视 znode发生了变化。所以我们使用 Zookeeper 不能期望能够监控到节点每次的变化。Zookeeper 只能保证最终的一致性,而无法保证强一致性。

Watcher接口设计

Watcher是一个接口,任何实现了Watcher接口的类就是一个新的Watcher。Watcher内部包含了两个枚举类:KeeperState、EventType。
image.png

Watcher通知状态(KeeperState)


  KeeperState是客户端与服务端连接状态发生变化时对应的通知类型。路径为org.apache.zookeeper.Watcher.Event.KeeperState,是一个枚举类,其枚举属性如下;

枚举属性 说明
Unknown(-1) 属性过期
Disconnected(0) 客户端与服务器断开连接时
NoSyncConnected(1) 属性过期
SyncConnected(3) 客户端与服务器正常连接时
AuthFailed(4) 身份认证失败时
ConnectedReadOnly(5) 3.3.0版本后支持只读模式,一般情况下ZK集群中半数以上服务器正常,zk集群才能正常对外提供服务。该属性的意义在于:若客户端设置了允许只读模式,则当zk集群中只有少于半数的服务器正常时,会返回这个状态给客户端,此时客户端只能处理读请求
SaslAuthenticated(6) 服务器采用SASL做校验时
Expired(-112) 会话session失效时

Watcher事件类型(EventType)

  EventType是数据节点(znode)发生变化时对应的通知类型。EventType变化时KeeperState永远处于SyncConnected通知状态下;当KeeperState发生变化时,EventType永远为None。其路径为org.apache.zookeeper.Watcher.Event.EventType,是一个枚举类,枚举属性如下;

枚举属性 说明
None (-1)
NodeCreated (1) Watcher监听的数据节点被创建时
NodeDeleted (2) Watcher监听的数据节点被删除时
NodeDataChanged (3) Watcher监听的数据节点内容发生变更时(无论内容数据是否变化)
NodeChildrenChanged (4) Watcher监听的数据节点的子节点列表发生变更时

注:客户端接收到的相关事件通知中只包含状态及类型等信息,不包括节点变化前后的具体内容,变化前的数据需业务自身存储,变化后的数据需调用get等方法重新获取;

常见的监听场景有以下两项:
1. 监听Znode节点的数据变化.
2. 监听子节点的增减变化.