zookeeper的核心-watcher

​ 上篇文章讲过zookeeper的灵魂是ZAB协议,是其能够实现分布式的基石,但只有数据一致性协议还是无法应用于生产场景中的,本篇文章重点介绍一下zookeeper提供的watcher,基本上,所有依赖于zookeeper的框架、相关使用场景都严重依赖于watcher。


1 zk的相关使用场景

在生产环境中,使用ZK主要是应用于以下几种场景

  1. 分布式下的主节点选举。

    多数的分布式场景下,都是具有主节点(也称Master节点)的,由主节点负责统一调度、管理所有应用,在这种情况下,主节点就具有单点问题,为了解决单点问题,基于zookeeper来保证当主节点宕机后,能由其他节点自动选举为主节点。

  2. 分布式队列

    因为ZK保证了分布式条件下的数据一致性,所以能够提供分布式下的队列支持,并能够保证进队、出队的顺序性。

  3. 集群监控

    因为在ZK中有临时节点和watcher的存在,可以感知到整个分布式集群任意一台机器的离线,从而实现对集群机器存活状态的监控

  4. 类似于MQ的订阅通知

    zk有watcher机制,可以让客户端感知节点、节点数据的变化来实现主动通知,但是ZK的watcher都是一次性的,变化一次就被移除了,所以在频繁变化的场景下可能会漏掉数据变化的事件,其次,在ZK中,watcher是在单队列中按照顺序执行的,注册过多的watcher会显著影响性能。与之相比,MQ中间件才是更好的选择。

  5. 分布式锁。

    虽然zookeeper能够做到分布式锁,但是据我了解,使用ZK当分布式锁的公司基本上可以忽略不计,因为ZK本就不是为解决该问题而设计的(ZK不支持高并发、超低延时访问,也不支持在ZK中存储大量数据)。与之对比,redis是更好的选择。

大体上来看,真正使用频次高的只有前三种,但是以上每一点都能够看出,需要watcher的支持,所以,我认为,watcher才是ZK能够适用于生产环境的核心。

2 wathcer机制剖析

下图为wather注册、触发的流程图。

watch-flow

2.1 watch注册

客户端在创建的时候,可以申请一个全局的watcher(该watch是特殊的watcher,与客户端的session会话绑定,而不是node节点。并且该watcher不是一次性的,存活周期与session生命周期相同)

1
ZooKeeper zk = new ZooKeeper(String connectString, int sessionTimeout, Watcher watcher) ;

另一种方式就是使用客户端的API,比如getData、 getChildren等方法时,可以在指定的node节点上添加watcher,以上方法都有两个重载方法来选择使用全局watcher还是自定义的watcher,使用getData来举例

1
2
3
4
5
// 在节点上添加自定义watcher,节点变动会触发
public void getData(final String path, Watcher watcher, DataCallback cb, Object ctx)

// 使用创建客户端时的全局watcher,节点变动会触发全局的watcher
public void getData(String path, boolean watch, DataCallback cb, Object ctx)

当客户端的请求时附加了watcher,服务端就会把该watcher添加到服务端的watchManager类中,该类管理了整个ZK集群的所有watcher,其中有两个重要的数据结构watchTable和watch2Paths

1
2
3
4
5
6
7
// key 为 node节点值  value 为 节点下所有的watcher
private final HashMap<String, HashSet<Watcher>> watchTable =
new HashMap<String, HashSet<Watcher>>();

// key为watcher value 为 该watcher对应的所有节点列表
private final HashMap<Watcher, HashSet<String>> watch2Paths =
new HashMap<Watcher, HashSet<String>>();

有这两个map,就可以实现对watcher的所有查询需求。比如根据节点查watcher,根据watcher查节点。

在客户端中也会把自己的watcher注册到自己本地的ZKWatchManager类中,因为客户端和服务端处理的侧重点不同,所以数据结构也不相同(客户端不需要通过watcher反查node。客户端主要是为了能够执行watcher回调,因watcher有不同类型,所以同一node下的watcher分别储存)。

1
2
3
4
5
6
7
8
9
10
11
12
// key 为 node节点值  value 为 节点下data类型watcher
private final Map<String, Set<Watcher>> dataWatches =
new HashMap<String, Set<Watcher>>();
// key 为 node节点值 value 为 exists方法注册的watch
private final Map<String, Set<Watcher>> existWatches =
new HashMap<String, Set<Watcher>>();
// key 为 node节点值 value 为 节点下child类型watcher
private final Map<String, Set<Watcher>> childWatches =
new HashMap<String, Set<Watcher>>();

// 全局的watcher
protected volatile Watcher defaultWatcher;

上面提到了watcher具有不同的类型,在ZK中watcher分为三种,客户端实际管理watcher的方法都在org.apache.zookeeper.ZooKeeper.ZKWatchManager类中。

  1. data Watch

    exist、getData方法注册,create、setData、delete方法会触发。

  2. child Watch

    getChildren方法注册,create、delete方法会触发。

  3. None

    表示默认watcher受到的通知(就是创建客户端那时候的watcher)

2.2 watch触发

在一开始的流程图中提到了,到任意客户端修改了节点数据后,所有客户端都会受到触发(watcher的通知是异步进行的)

watcher通知的事件类型如下图所示(从网上抄的,写的很清晰)

watch-eventType

每当服务端处理以上内容时,对于非None情况(也就是节点上的watcher),从watchManager查询节点上的所有watcher,使用与客户端的socket连接进行异步通知,然后将watcher移除。对于None情况,则省去了查询,直接通知即可。

客户端接收到服务端相应,并将该请求包封装成统一的Event事件,发送到队列中。

客户端使用EventThread线程不断轮询处理所有的event事件,如果该事件为watcher类型(服务端的watch通知只会告知那个节点发生了变更,而不会告知其变更具体内容),则从ZKWatchManager查询该节点对应的watcher再次放到一个队列中进行处理(保证了wathcer的顺序执行),执行完后将watcher移除。

2.3 watch使用过程中问题

  1. watcher是一次性的

    看上面触发过程可以看出,对于非None类型watcher,使用一次后就被WatchManager移除掉了,ZK的封装框架curator中,watcher是能够一直存在,不过原理是框架内部封装了使用后自动再注册一个的细节。但是在高并发下会存在通知丢失问题(注册新的watcher的这一时刻中的变更是接收不到的)。

  2. watcher在客户端中是遍历队列顺序执行的

    如果有单个wather回调逻辑很复杂,则会影响到所有后续watch的调用,但是因为有顺序性,也保证了只有收到通知,才能感知到最新的数据。

  3. 节点数据内容不变仍会触发watch

    这个可以算一个小坑,因为在ZK中,只要节点数据变更了,其结构中的dataVersion就会增加,哪怕你的内容一点没有变化也算作变更了。

  4. 当客户端异常结束或者主动断开时,所有已注册的watcher都会失效。

    解决办法:在新建连接后,把上次所有注册过的watcher再重新注册一遍(只能这样。。)

  5. watch羊群效应

    watch通知的发送只能由服务端下发,如果所有客户端对一个频繁变化的节点频繁注册watch,则会导致通知持续大量发送,严重影响ZK集群的性能。