Zk-应用场景
本文介绍了基于ZK的典型应用场景,这些都基于ZK所提供的ZNode 顺序ID和Watchs功能。
我们在使用 ZK 来实现组件时,可以考虑使用 Apache Curator,它提供了封装好,方便操作 ZK 的接口。
- **数据发布与订阅(配置中心)**:将数据放在ZNode上,订阅者通过Watcher监控ZNode的数据变更。
- 负载均衡: 建立服务节点,服务端在该节点下注册自身信息的临时节点,客户端使用Watcher监控子节点变化,获取可用服务列表。
- 命名服务:实现方式同负载均衡
- 分布式协调/通知: 注册ZNode,通过监控ZNode数量和节点数据来协调通知节点,
- **分布式UUID:**:基于ZNode的顺序ID功能,注册一个ZNode, 获取其序号。
- Master 选举: 有两种实现方式
- 分布式锁: 可以实现共享锁和互斥锁
- 分布式队列:可以实现 FIFO Queue 和 Barrier
数据发布与订阅(配置中心)
将数据放在ZNode上,订阅者通过Watcher监控ZNode的数据变更,从而实现配置信息的集中式管理和动态更新。
ZNode 存在数据大小的限制,最好不要超过1MB, 请酌情设置。
负载均衡
这里的负载均衡是客户端的负载均衡,客户端获取所有可用服务列表,自身选择合适的负载均衡算法。
实现思路:
- 建立一个服务的ZNode, 如/server
- 服务端启动时,在/server 目录下建立临时ZNode, 并设置服务器IP:Port 等连接信息
- 客户端启动时获取/Server下的所有子节点,即所有可用服务端,并设置Watcher监控子节点数量的变动。
- 当服务端新增或减少节点时,引起子节点数量变动。客户端获得Watcher提醒获取最新服务端列表。
命名服务
实现方式同负载均衡。
在Dubbo实现中:
服务提供者在启动的时候,向ZK上的指定节点**/dubbo/${serviceName}/providers**目录下写入自己的URL地址,这个操作就完成了服务的发布。
服务消费者启动的时候,订阅/dubbo/${serviceName}/providers目录下的提供者URL地址, 并向**/dubbo/${serviceName} /consumers目录下写入自己的URL地址。**
注意,所有向ZK上注册的地址都是临时节点,这样就能够保证服务提供者和消费者能够自动感应资源的变化。
另外,Dubbo还有针对服务粒度的监控,方法是订阅/dubbo/${serviceName}目录下所有提供者和消费者的信息。
分布式协调/通知
节点心跳:
实现思路: 注册一个临时ZNode, 节点之间通过Watcher监控ZNode数量,当ZNode不存在时,则表示服务不可用。一般而言,分布式环境通过心跳来判断节点是否存活。
优点: 通过ZK来关联节点,减少了系统之间的耦合。
节点协调:
实现思路: 注册一个ZNode, 通过Watcher监控ZNode数据变动,当发生数据变动时,获取最新数据,由此实现节点协调和数据同步。
分布式UUID
实现思路:基于ZNode的顺序ID功能,注册一个ZNode, 获取其序号。
缺点: ZNode的当前最大ID由其父亲ZNode存储,是一个4Byte的Integer, 存在最大ID数量的限制。
Master选举
这里存在两种选举思路:
思路1: 客户端集群往zookeeper上创建一个/master临时节点。在这个过程中,只有一个客户端能够成功创建这个节点,那么这个客户端就成了master。同时其他没有在zookeeper上成功创建节点的客户端,都会在节点/master上注册一个变更的watcher,用于监控当前的master机器是否存活,一旦发现当前的master挂了,那么其余的客户端将会重新进行master选举
思路2: 基于顺序ID的功能,所有客户端节点都创建一个子ZNode,谁的顺序ID小谁就是Master并监控所有子Znode的数量,一旦发生Master节点下线,则选举顺序ID最小的节点为新的Master。
分布式锁
通过 ZK 实现分布式的共享锁和排他锁。
排他锁
排他锁也存在两种实现方式。
实现1:
将一个 ZNode 表示为一个锁,比如/distribute_lock
- 获取锁,客户端通过
create()
创建 临时ZNode,若创建成功则表示获取锁,当使用完锁,删除临时 ZNode. - 若获取锁失败,则注册一个节点变更的 Watcher
- 当Watcher 被触发,所有等待的客户端重新开始新一轮的竞争锁。
实现2:
注册一个ZNode 节点,所有客户端再该节点下创建**临时顺序ZNode **/distribute_lock/Lock-00000000001
- 客户端创建 ZNode, 创建完成后,获取所有子节点列表,若序号最小,则获取锁。任务结束后,删除临时子节点。
- 创建 ZNode, 发现当前序号非最小,则对前一个序号节点注册监听事件。
共享锁
共享锁是基于排他锁实现2的一个扩展,这里我们以读写锁为例,读读共享的一个场景。
在创建临时的子节点时,我们增加节点读写属性,如下所示。
1 | / |
实现方式:
- 获取锁,客户端在共享锁目录下创建临时子节点,并标注其读写属性。
- 对于读锁而言,在创建节点后判断是否存在比自己节点序号更小的写节点,如果没有则获取锁。如果有,则对序号小于自己且最近的写节点注册监听事件。
- 对于写锁而言,在创建节点后判断是否存在比自己节点序号更小的节点,如果没有则获取锁。如果有,则对自己的前一个节点注册监听事件。
- 释放锁,客户端完成业务逻辑后,删除临时节点来释放锁,后续由监听事件来唤醒等待客户端。
分布式队列
FIFO
PUSH: 在 queue_fifo
节点下,创建自增序号子节点,并将数据放入子节点中。
POP: 获取 queue_fifo
节点下序号最小的节点,取出数据,然后删除此节点。
Barrier
在 Java 提供的同步工具类中就有 CylicBarrier,适用于需要所有线程到达一个共同的检查点后,再同时继续的场景。
通过 ZK 来实现 Barrier:
首先创建 queue_barrier
节点,并设置节点的值为需要等待的线程数量,所有线程在该节点下创建临时子节点。线程创建子节点结束后,判断子节点数量是否等于等待线程数量,如果是则继续,如果不是则注册子节点数量变更的 Watcher.
Reference
[从Paxos到ZooKeeper]