第六章 ZooKeeper 的典型应用场景(一)
第五章是 java 使用 ZooKeeper的 api 调用等,不重要,需要用到时候看api文档即可
6.1 数据发布/订阅(配置中心)
场景痛点:
在大型分布式系统中,通常有大量的服务实例需要共享同一份配置。如果将配置硬编码或放在每个实例的本地文件中,那么每次配置变更都需要逐台机器去修改、重启服务,过程繁琐、容易出错,且难以保证所有实例的配置在同一时间生效。
ZooKeeper 解决方案:
ZooKeeper 的“树形节点 + Watcher 机制”是实现数据发布/订阅模式的天然利器,常被用作分布式配置中心。
实现原理:
-
数据发布(Publisher):
- 将配置信息存储在 ZooKeeper 的一个或多个 ZNode 上。通常会有一个固定的父节点,例如
/app/config。 - 每个配置项可以是一个 ZNode,其数据区(Data)存储配置值。例如,
/app/config/db_url存储数据库连接地址,/app/config/thread_pool_size存储线程池大小。 - 当需要变更配置时,配置管理员或自动化脚本连接到 ZooKeeper,使用
setData命令修改对应 ZNode 的数据。
- 将配置信息存储在 ZooKeeper 的一个或多个 ZNode 上。通常会有一个固定的父节点,例如
-
数据订阅(Subscriber):
- 所有需要该配置的应用客户端(服务实例)在启动时连接到 ZooKeeper。
- 客户端使用
getData方法获取/app/config下各个 ZNode 的初始配置值。 - 关键一步:在获取数据的同时,客户端对这些 ZNode 注册一个 Watcher(监视器)。
- 当 ZNode 的数据被修改时(即配置发生变更),ZooKeeper 服务端会向所有注册了 Watcher 的客户端发送一个
NodeDataChanged通知。
-
配置更新:
- 客户端的 Watcher 在收到通知后被触发。
- 在 Watcher 的回调函数中,客户端会重新调用
getData方法来获取最新的配置数据。 - 重要:为了能够持续接收后续的变更通知,客户端在本次
getData时,需要再次注册一个新的 Watcher(因为 Watcher 是一次性的)。 - 获取到新配置后,客户端在内存中更新其配置,并根据业务逻辑执行相应的热更新操作(如重建数据库连接池、调整线程池参数等),无需重启服务。
架构图:
+-------------------+
| ZooKeeper Cluster |
| /app/config/db_url|
+-------------------+
^ |
| 1. | 2. Watcher Notification
setData() |
| |
+------+------+------+
| |
+-------+-------+ +-----+---------+
| Config Admin | | App Instances |
+---------------+ +---------------+
/ | \
/ | \
Client1 Client2 Client3
(Watching /app/config/db_url)
优点:
- 集中管理:所有配置集中存储,清晰明了。
- 实时同步:利用 Watcher 机制,配置变更可以准实时地推送给所有订阅者。
- 高可用:依赖 ZooKeeper 集群的高可用性,配置中心本身不会成为单点。
- 一致性:保证了所有客户端最终能获取到一致的配置视图。
6.2 负载均衡
场景痛点:
在一个服务化的架构中,一个服务(如订单服务)通常由多个实例(Provider)组成集群来提供高可用和高性能。服务消费者(Consumer)如何知道有哪些健康的 Provider 实例可用?如何在这些实例之间均匀地分配请求流量?当有 Provider 实例上线或下线时,Consumer 如何能动态地感知到变化?
ZooKeeper 解决方案:
ZooKeeper 的“临时顺序节点 + Watcher 机制”为实现动态、可靠的服务注册与发现提供了完美的解决方案,这也是实现负载均衡的基础。
实现原理:
-
服务注册(Service Provider):
- 首先,在 ZooKeeper 中创建一个用于服务注册的持久父节点,作为服务的根目录,例如
/services/order_service。 - 当一个订单服务提供者(Provider)实例启动时,它会连接到 ZooKeeper。
- 它在
/services/order_service目录下创建一个临时顺序节点(Ephemeral Sequential Node),例如/services/order_service/provider-0000000001。 - 该临时节点的数据区(Data)存储着这个 Provider 实例的 IP 地址和端口号,例如
"192.168.1.10:8080"。 - 利用临时节点:这是整个方案的精髓。如果该 Provider 实例宕机或与 ZooKeeper 断开连接,其会话(Session)会超时,这个临时节点将被 ZooKeeper 自动删除。
- 首先,在 ZooKeeper 中创建一个用于服务注册的持久父节点,作为服务的根目录,例如
-
服务发现(Service Consumer):
- 服务消费者(Consumer)启动时,首先连接到 ZooKeeper。
- 它使用
getChildren方法获取/services/order_service目录下的所有子节点列表,这个列表就是当前所有可用的 Provider 实例列表。 - 关键一步:Consumer 对父节点
/services/order_service注册一个 Watcher(监视器),用于监听其子节点列表的变化(NodeChildrenChanged事件)。 - Consumer 遍历子节点列表,对每个子节点路径(如
/services/order_service/provider-0000000001)调用getData方法,获取其存储的 Provider 地址信息,并将这些地址缓存在本地内存中。
-
负载均衡与动态感知:
- 当 Consumer 需要调用订单服务时,它从本地缓存的 Provider 地址列表中,根据负载均衡算法(如轮询、随机、哈希等)选择一个地址发起请求。
- 当有新的 Provider 上线时:它会在
/services/order_service下创建一个新的临时节点。这会触发 Consumer 注册的 Watcher。 - 当有 Provider 下线时:它的临时节点被自动删除。这同样会触发 Consumer 注册的 Watcher。
- Watcher 被触发后,Consumer 会重新调用
getChildren获取最新的 Provider 列表,并更新本地缓存。同时,再次注册一个新的 Watcher 以便继续监听。 - 通过这个闭环,Consumer 能够动态地感知 Provider 集群的变化,并始终将请求路由到健康的实例上,从而实现了高可用的动态负载均衡。
架构图:
+--------------------------+
| ZooKeeper Cluster |
| /services/order_service/ |
| - provider-00000001 | <-- Ephemeral Nodes
| - provider-00000002 |
+--------------------------+
^ | ^
1. Register | | 3. GetChildren() & Watch
(Create Ephemeral) | |
| | |
+------+ | 2. NodeChildrenChanged |
| | |
+-------+-------+ | +-----+---------+
| Service | +-----> | Service |
| Providers | | Consumers |
+---------------+ +---------------+
(Load Balancer)
优点:
- 动态感知:服务实例的上下线能够被消费者实时发现,无需人工干预。
- 高可靠:利用临时节点的特性,可以自动摘除宕机的服务节点,避免将请求发送到无效实例。
- 去中心化:每个 Consumer 自行从 ZooKeeper 获取服务列表并执行负载均衡逻辑,避免了中心化负载均衡器(如 Nginx、F5)的单点问题和性能瓶颈。
- 易于水平扩展:无论是 Provider 还是 Consumer,都可以任意增加节点,系统具备良好的伸缩性。
6.3 命名服务 (Naming Service)
场景痛点:
在分布式系统中,资源(如服务、配置、服务器等)的地址(IP、端口)或标识是动态变化的。如果将这些硬编码在代码或配置文件中,一旦资源地址变更,所有依赖它的应用都需要修改,这非常不便且容易出错。我们需要一个统一的地方,通过一个“名字”来找到这个资源,而不用关心它实际的物理地址。就像我们通过域名(www.google.com)访问网站,而不用关心其背后服务器的 IP 地址一样。
ZooKeeper 解决方案:
ZooKeeper 的树状路径结构天生就是一个强大的命名空间。通过将资源的标识信息存储在特定路径的 ZNode 中,可以实现一个动态、集中、可靠的命名服务。
实现原理:
-
定义命名空间:
- 首先,在 ZooKeeper 中规划一个用于命名服务的根路径,例如
/naming。 - 可以根据业务或资源类型创建不同的子路径,形成层次化的命名空间。例如,
/naming/services用于服务命名,/naming/config用于配置命名。
- 首先,在 ZooKeeper 中规划一个用于命名服务的根路径,例如
-
名字注册:
- 当一个需要被命名的资源(比如一个新上线的数据库服务)启动时,它会向 ZooKeeper 注册自己的名字和信息。
- 它会在预定义的路径下创建一个 ZNode,节点路径就是这个资源全局唯一的名字。例如,为“主数据库”创建一个节点
/naming/db/master。 - 该 ZNode 的数据区(Data)存储着这个资源的具体信息,比如它的 IP 地址和端口号
{"ip": "192.168.1.100", "port": 3306}。 - 这个 ZNode 通常是持久节点(Persistent Node),除非该名字被废弃,否则会一直存在。如果资源是临时的,也可以使用临时节点。
-
名字解析:
- 任何需要访问该资源的客户端,不再直接使用 IP 地址,而是向 ZooKeeper 查询这个预定义的名字。
- 客户端调用
getData方法,传入资源的唯一名字(即 ZNode 路径,如/naming/db/master)。 - ZooKeeper 返回该 ZNode 的数据,客户端解析后即可得到资源的实际地址信息,然后用该地址进行后续通信。
与 DNS 的类比:
- ZNode 路径 (
/naming/db/master) <==> 域名 (db-master.my-company.com) - ZNode 数据 (
{"ip": "...", "port": ...}) <==> DNS 记录 (A 记录, SRV 记录) - ZooKeeper 集群 <==> DNS 服务器
高级应用:生成全局唯一 ID
利用 ZooKeeper 的持久顺序节点(Persistent Sequential Node),还可以实现一个全局唯一 ID 的生成器。
- 在一个固定的父节点下(如
/naming/unique_ids/)反复创建持久顺序节点。 - ZooKeeper 会为每个创建请求自动追加一个单调递增的数字后缀。
- 这个完整的节点路径(如
/naming/unique_ids/id-0000000001)本身就可以作为一个全局唯一的 ID。由于 ZooKeeper 的一致性保证,这个过程是线程安全的,并且在整个分布式系统中唯一。
优点:
- 解耦:将资源的逻辑名称与物理地址解耦,提高了系统的灵活性和可维护性。
- 集中管理:所有资源的命名和寻址信息都集中在 ZooKeeper,便于管理和查询。
- 动态性:当资源地址变更时,只需修改 ZooKeeper 中对应 ZNode 的数据即可,所有客户端下次查询时会自动获取新地址。
6.4 分布式协调/通知 (Distributed Coordination/Notification)
场景痛点:
在分布式系统中,不同节点或子系统之间常常需要进行协作。例如,一个任务需要分发给多个 Worker 节点执行,Master 节点需要知道哪些 Worker 节点是存活的;或者一个系统完成某个初始化操作后,需要通知其他依赖它的系统开始工作。这种跨进程、跨机器的通信和状态同步是分布式协调的核心难题。
ZooKeeper 解决方案:
ZooKeeper 的 Watcher 机制是实现分布式协调与通知的核心。它提供了一种“发布-订阅”模型,允许一个节点的状态变化被其他节点感知,从而触发相应的动作。
实现原理:
该机制通常与其他功能(如集群管理、Master 选举)结合使用,但其核心模式是统一的:一个节点改变状态,其他节点获得通知。
通用模式:
-
定义“信号旗”节点:
- 在 ZooKeeper 中创建一个公共的 ZNode,作为协调的“信号旗”或“信箱”。例如,
/coordination/task_ready_signal。
- 在 ZooKeeper 中创建一个公共的 ZNode,作为协调的“信号旗”或“信箱”。例如,
-
等待通知方 (Receiver):
- 所有需要等待某个事件发生的节点(如 Worker 节点)在启动后,对这个“信号旗”节点调用
exists或getData方法,并注册一个 Watcher。 - 如果节点不存在,
exists的 Watcher 会在节点被创建时触发。 - 如果节点已存在,
getData的 Watcher 会在节点数据被修改或节点被删除时触发。 - 注册 Watcher 后,这些节点进入等待状态。
- 所有需要等待某个事件发生的节点(如 Worker 节点)在启动后,对这个“信号旗”节点调用
-
发送通知方 (Notifier):
- 当某个特定事件发生时(例如,Master 准备好了所有任务数据),负责通知的节点(Master 节点)会去操作“信号旗”节点。
- 操作方式可以是:
- 创建节点:调用
create创建/coordination/task_ready_signal。 - 修改数据:调用
setData修改该节点的数据,例如写入当前时间戳或任务版本号。 - 删除节点:调用
delete删除该节点。
- 创建节点:调用
-
触发与响应:
- Notifier 对 ZNode 的操作会立即被 ZooKeeper 服务端捕获。
- 服务端会向所有在该 ZNode 上注册了相应 Watcher 的 Receiver 节点发送通知。
- Receiver 节点的 Watcher 回调函数被触发,执行预设的逻辑,例如开始拉取任务、启动服务等。
- 同样,由于 Watcher 是一次性的,如果需要持续监听,Receiver 在收到通知后需要再次注册 Watcher。
典型示例:Master/Worker 模式下的心跳检测
- Master 节点:监听一个父节点,如
/cluster/workers,并注册NodeChildrenChanged的 Watcher。 - Worker 节点:启动时在
/cluster/workers下创建自己的临时节点,如/cluster/workers/worker-1。 - 协调/通知过程:
- Worker 创建节点,Master 的 Watcher 被触发,得知有新 Worker 加入(通知)。
- Worker 宕机,其临时节点被 ZooKeeper 自动删除,Master 的 Watcher 再次被触发,得知有 Worker 离线(通知)。
- 通过这种方式,Master 能够实时协调和管理整个 Worker 集群的状态。
优点:
- 轻量级通信:节点间无需直接建立复杂的网络连接,通过操作 ZooKeeper 的一个 ZNode 即可完成通信。
- 异步解耦:通知方和接收方完全解耦,它们只依赖于 ZooKeeper 中的一个约定好的节点,互不感知对方的存在。
- 可靠通知:基于 ZooKeeper 的一致性保证,通知不会丢失,并且能够可靠地送达所有监听者。
- 简化逻辑:将复杂的分布式状态同步问题,简化为对一个共享数据节点的监听和操作。

浙公网安备 33010602011771号