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, 请酌情设置。

负载均衡

这里的负载均衡是客户端的负载均衡,客户端获取所有可用服务列表,自身选择合适的负载均衡算法。

实现思路:

  1. 建立一个服务的ZNode, 如/server
  2. 服务端启动时,在/server 目录下建立临时ZNode, 并设置服务器IP:Port 等连接信息
  3. 客户端启动时获取/Server下的所有子节点,即所有可用服务端,并设置Watcher监控子节点数量的变动。
  4. 当服务端新增或减少节点时,引起子节点数量变动。客户端获得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

  1. 获取锁,客户端通过 create()创建 临时ZNode,若创建成功则表示获取锁,当使用完锁,删除临时 ZNode.
  2. 若获取锁失败,则注册一个节点变更的 Watcher
  3. 当Watcher 被触发,所有等待的客户端重新开始新一轮的竞争锁。

实现2:

注册一个ZNode 节点,所有客户端再该节点下创建**临时顺序ZNode **/distribute_lock/Lock-00000000001

  1. 客户端创建 ZNode, 创建完成后,获取所有子节点列表,若序号最小,则获取锁。任务结束后,删除临时子节点。
  2. 创建 ZNode, 发现当前序号非最小,则对前一个序号节点注册监听事件。

共享锁

共享锁是基于排他锁实现2的一个扩展,这里我们以读写锁为例,读读共享的一个场景。

在创建临时的子节点时,我们增加节点读写属性,如下所示。

1
2
3
4
5
6
7
8
/
├── /host1-R-000000001
├── /host2-R-000000002
├── /host3-W-000000003
├── /host4-R-000000004
├── /host5-R-000000005
├── /host6-R-000000006
└── /host7-W-000000007

实现方式:

  1. 获取锁,客户端在共享锁目录下创建临时子节点,并标注其读写属性。
    • 对于读锁而言,在创建节点后判断是否存在比自己节点序号更小的写节点,如果没有则获取锁。如果有,则对序号小于自己且最近的写节点注册监听事件。
    • 对于写锁而言,在创建节点后判断是否存在比自己节点序号更小的节点,如果没有则获取锁。如果有,则对自己的前一个节点注册监听事件。
  2. 释放锁,客户端完成业务逻辑后,删除临时节点来释放锁,后续由监听事件来唤醒等待客户端。

分布式队列

FIFO

PUSH:queue_fifo节点下,创建自增序号子节点,并将数据放入子节点中。

POP: 获取 queue_fifo节点下序号最小的节点,取出数据,然后删除此节点。

Barrier

在 Java 提供的同步工具类中就有 CylicBarrier,适用于需要所有线程到达一个共同的检查点后,再同时继续的场景。

通过 ZK 来实现 Barrier:

首先创建 queue_barrier 节点,并设置节点的值为需要等待的线程数量,所有线程在该节点下创建临时子节点。线程创建子节点结束后,判断子节点数量是否等于等待线程数量,如果是则继续,如果不是则注册子节点数量变更的 Watcher.

Reference

[从Paxos到ZooKeeper]

图解ZooKeeper的典型应用场景

ZooKeeper典型应用场景一览

ZooKeeper 的应用场景

ZooKeeper Recipes and Solutions