zookeeper的核心-watch机制
zookeeper的核心-watcher
上篇文章讲过zookeeper的灵魂是ZAB协议,是其能够实现分布式的基石,但只有数据一致性协议还是无法应用于生产场景中的,本篇文章重点介绍一下zookeeper提供的watcher,基本上,所有依赖于zookeeper的框架、相关使用场景都严重依赖于watcher。
1 zk的相关使用场景
在生产环境中,使用ZK主要是应用于以下几种场景
分布式下的主节点选举。
多数的分布式场景下,都是具有主节点(也称Master节点)的,由主节点负责统一调度、管理所有应用,在这种情况下,主节点就具有单点问题,为了解决单点问题,基于zookeeper来保证当主节点宕机后,能由其他节点自动选举为主节点。
分布式队列
因为ZK保证了分布式条件下的数据一致性,所以能够提供分布式下的队列支持,并能够保证进队、出队的顺序性。
集群监控
因为在ZK中有临时节点和watcher的存在,可以感知到整个分布式集群任意一台机器的离线,从而实现对集群机器存活状态的监控
类似于MQ的订阅通知
zk有watcher机制,可以让客户端感知节点、节点数据的变化来实现主动通知,但是ZK的watcher都是一次性的,变化一次就被移除了,所以在频繁变化的场景下可能会漏掉数据变化的事件,其次,在ZK中,watcher是在单队列中按照顺序执行的,注册过多的watcher会显著影响性能。与之相比,MQ中间件才是更好的选择。
分布式锁。
虽然zookeeper能够做到分布式锁,但是据我了解,使用ZK当分布式锁的公司基本上可以忽略不计,因为ZK本就不是为解决该问题而设计的(ZK不支持高并发、超低延时访问,也不支持在ZK中存储大量数据)。与之对比,redis是更好的选择。
大体上来看,真正使用频次高的只有前三种,但是以上每一点都能够看出,需要watcher的支持,所以,我认为,watcher才是ZK能够适用于生产环境的核心。
2 wathcer机制剖析
下图为wather注册、触发的流程图。
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 | // 在节点上添加自定义watcher,节点变动会触发 |
当客户端的请求时附加了watcher,服务端就会把该watcher添加到服务端的watchManager类中,该类管理了整个ZK集群的所有watcher,其中有两个重要的数据结构watchTable和watch2Paths
1 | // key 为 node节点值 value 为 节点下所有的watcher |
有这两个map,就可以实现对watcher的所有查询需求。比如根据节点查watcher,根据watcher查节点。
在客户端中也会把自己的watcher注册到自己本地的ZKWatchManager类中,因为客户端和服务端处理的侧重点不同,所以数据结构也不相同(客户端不需要通过watcher反查node。客户端主要是为了能够执行watcher回调,因watcher有不同类型,所以同一node下的watcher分别储存)。
1 | // key 为 node节点值 value 为 节点下data类型watcher |
上面提到了watcher具有不同的类型,在ZK中watcher分为三种,客户端实际管理watcher的方法都在org.apache.zookeeper.ZooKeeper.ZKWatchManager类中。
data Watch
exist、getData方法注册,create、setData、delete方法会触发。
child Watch
getChildren方法注册,create、delete方法会触发。
None
表示默认watcher受到的通知(就是创建客户端那时候的watcher)
2.2 watch触发
在一开始的流程图中提到了,到任意客户端修改了节点数据后,所有客户端都会受到触发(watcher的通知是异步进行的)
watcher通知的事件类型如下图所示(从网上抄的,写的很清晰)
每当服务端处理以上内容时,对于非None情况(也就是节点上的watcher),从watchManager查询节点上的所有watcher,使用与客户端的socket连接进行异步通知,然后将watcher移除。对于None情况,则省去了查询,直接通知即可。
客户端接收到服务端相应,并将该请求包封装成统一的Event事件,发送到队列中。
客户端使用EventThread线程不断轮询处理所有的event事件,如果该事件为watcher类型(服务端的watch通知只会告知那个节点发生了变更,而不会告知其变更具体内容),则从ZKWatchManager查询该节点对应的watcher再次放到一个队列中进行处理(保证了wathcer的顺序执行),执行完后将watcher移除。
2.3 watch使用过程中问题
watcher是一次性的
看上面触发过程可以看出,对于非None类型watcher,使用一次后就被WatchManager移除掉了,ZK的封装框架curator中,watcher是能够一直存在,不过原理是框架内部封装了使用后自动再注册一个的细节。但是在高并发下会存在通知丢失问题(注册新的watcher的这一时刻中的变更是接收不到的)。
watcher在客户端中是遍历队列顺序执行的
如果有单个wather回调逻辑很复杂,则会影响到所有后续watch的调用,但是因为有顺序性,也保证了只有收到通知,才能感知到最新的数据。
节点数据内容不变仍会触发watch
这个可以算一个小坑,因为在ZK中,只要节点数据变更了,其结构中的dataVersion就会增加,哪怕你的内容一点没有变化也算作变更了。
当客户端异常结束或者主动断开时,所有已注册的watcher都会失效。
解决办法:在新建连接后,把上次所有注册过的watcher再重新注册一遍(只能这样。。)
watch羊群效应
watch通知的发送只能由服务端下发,如果所有客户端对一个频繁变化的节点频繁注册watch,则会导致通知持续大量发送,严重影响ZK集群的性能。