技术解析:基于 ZooKeeper 实现高可用的主-从协调系统(通过例子深入理解Zookeeper如何进行协调分布式系统)

发布于:2025-09-08 ⋅ 阅读:(20) ⋅ 点赞:(0)

系列文章目录

第一章 ZooKeeper入门概述:Znode,Watcher,ZAB .



技术解析:基于 ZooKeeper 实现高可用的主-从协调系统

在分布式计算中,实现可靠的进程协调是构建稳定系统的基础。关键问题包括如何选举唯一的领导者、如何动态管理集群成员以及如何进行任务分发。本文将详细阐述如何使用 Apache ZooKeeper 的基本功能,构建一个高可用的主-从(Master-Slave)模式的协调系统。(通过该例子理解Znode和Watcher 如何进行分布式协调)

我们将通过 ZooKeeper 的命令行工具 zkCli 来演示整个过程,以清晰地展示其底层工作原理。

一、 系统设计:角色与数据模型

该系统由三个功能角色和一套定义在 ZooKeeper 中的数据结构组成。

集群配置: 三台服务器分别为host1,host2,host3.
系统角色:

  1. 主节点 (Master): 系统的控制单元。其功能是监控从节点和任务的状态,并负责将任务分配给当前可用的从节点。
  2. 从节点 (Slave): 任务的执行单元。它向系统注册自身的存在,并监视等待主节点分配的任务。
  3. 客户端 (Client): 任务的提交单元。它负责创建新任务,并监视任务的最终完成状态。

ZooKeeper 数据模型 (Znode 结构):

系统的状态和协调逻辑通过 ZooKeeper 的 znode 树来表达。关键路径定义如下:

  • /master (临时节点): 用于实现主节点选举。成功创建此节点的进程即为主节点。
  • /workers (持久节点): 用于从节点注册。其下的每个子节点代表一个处于活动状态的从节点。
  • /tasks (持久节点): 用于任务提交。客户端在此路径下创建子节点以发布新任务,形成一个任务序列。
  • /assign (持久节点): 用于任务分配。主节点在此路径下为每个从节点创建专用的子路径,用于存放分配给该从节点的任务信息。

二、 主节点:选举、故障转移与状态监控

主节点的功能实现依赖于 ZooKeeper 提供的原子操作和事件通知机制。

2.1 主节点选举与高可用性

核心机制:利用临时节点的唯一性及其与客户端会话的生命周期绑定。

  1. 创建尝试 (Creation Attempt):
    所有候选主节点的进程都尝试在 /master 路径下创建一个临时节点 (-e)

     # host1 尝试创建 /master 节点
    [zk: host1:2181(CONNECTED) 1] create -e /master "master1.example"
    Created /master
    
    

    由于 Znode 创建是原子操作,只有一个进程能成功。该进程成为活动主节点 (Active Master)。节点的数据字段可用于存储其网络地址等元信息。

  2. 处理创建失败 (Handling Creation Failure):
    其他进程在尝试创建时,将收到节点已存在的错误。

     # 进程host2 尝试创建,操作失败
    [zk: host2:2181(CONNECTED) 0] create -e /master "master2.example"
    Node already exists: /master
    

    收到此错误的进程将进入备份主节点 (Backup Master) 状态。

  3. 故障转移机制 (Failover Mechanism):
    备份主节点必须能够检测到活动主节点的故障并接管其角色。

    核心机制:在 /master 节点上设置监视点 (Watch)。

    # 进程host2 (备份主节点) 对 /master 节点设置监视
    [zk: host2:2181(CONNECTED) 1] ls /master true
    []
    

    ls /master true 命令在 /master 节点上注册了一个一次性的监视点。若活动主节点的进程失效或网络连接中断,其 ZooKeeper 会话将超时。由于 /master 是一个临时节点,它会随着会อ会话的终止而被自动删除
    我们在当前主节点模拟该过程,在host1上删除/master
    此删除操作会触发一个 NodeDeleted 事件,该事件会通知所有在该节点上注册了监视点的备份主节点(host2)。

#host1
	[zk: host1:2181(CONNECTED) 2] delete /master
#host2
	[zk: host2:2181(CONNECTED) 2]
	WATCHER::
	WatchedEvent state:SyncConnected type:NodeDeleted path:/master

收到通知后,所有备份主节点host2将重新执行步骤1的创建尝试,从而使其中一个成为新的活动主节点。此流程确保了主节点角色的高可用性。此时host2成为了主节点。

[zk: host2:2181(CONNECTED) 2] create -e /master "master2.example"
Created /master

2.2 监控从节点与任务

活动主节点需要监控系统的动态变化,即从节点的可用性和新任务的提交情况。
创建workers 。tasks,和assign

[zk: host2:2181(CONNECTED) 3] create /workers ""
Created /workers
[zk: host2:2181(CONNECTED) 4] create /assign ""
Created /assign
[zk: host2:2181(CONNECTED) 5] create /tasks ""
Created /tasks
[zk: host2:2181(CONNECTED) 6] ls /
[zookeeper, workers, tasks, master, assign]

核心机制:活动住节点监视子节点列表的变化 (NodeChildrenChanged)。

# 监视 /workers 路径的子节点变化(是否有从节点注册)
[zk: host2:2181(CONNECTED) 7] ls /workers true
[]

# 监视 /tasks 路径的子节点变化(是否有任务待处理)
[zk: host2:2181(CONNECTED) 8] ls /tasks true
[]

通过在 ls 命令中使用 true 参数,主节点可以监视 /workers/tasks 两个路径。

  • /workers 下有子节点被创建或删除时,表明有从节点上线或下线,主节点会收到通知。
  • /tasks 下有子节点被创建时,表明有新任务提交,主节点同样会收到通知。

三、 从节点:注册与任务接收

从节点负责执行具体任务,它需要向系统声明其可用性,并准备接收任务。

  1. 注册 (Registration):
    从节点启动后,在 /workers 路径下创建一个代表自身的临时节点
    (host1 ,host 3注册从节点)
    [zk: host1:2181(CONNECTED) 1]  create -e /workers/worker1.example.com "worker1.example.com:2224"
    Created /workers/worker1.example.com
    

主节点host2发现workers有了变化(NodeChildrenChanged ),知道有了从节点上线。由于Watcher的一次性,host2为了可以继续监控,需要重新申请一个workers的监视Watcher

	[zk: host2:2181(CONNECTED) 9]
	WATCHER::
	
	WatchedEvent state:SyncConnected type:NodeChildrenChanged path:/workers
	
	[zk: host2:2181(CONNECTED) 9] ls /workers true
	[worker1.example.com]

Note: 使用临时节点是一种有效的存活性检测机制。只要从节点进程正常,该节点就存在。进程异常终止将导致节点自动删除,主节点可通过监视点立即感知到该从节点的离线。

  1. 准备接收任务 (Preparing for Assignment):
    每个从节点(例如 host1)在启动时,都会在 /assign 路径下创建一个以自身标识符(如 orker1.example.com)命名的持久性 Znode,例如 /assign/host1。这个 Znode 作为该从节点的专属任务队列。随后,从节点会在此 Znode 上设置一个子节点变化(NodeChildrenChanged)的监视点。当主节点向该路径下添加子节点(即分配新任务)时,该从节点会收到通知并处理任务。使用持久节点确保了即使在从节点短暂离线期间,分配给它的任务信息也不会丢失。

    # worker1 创建其任务分配路径
    [zk: host1:2181(CONNECTED) 2] create /assign/worker1.example.com ""
    Created /assign/worker1.example.com
    # 监视该路径,等待任务分配
    [zk: host1:2181(CONNECTED) 3] ls /assign/worker1.example.com true
    []
    

四、 客户端:任务提交与结果获取

客户端是任务的初始输入源。

  1. 任务提交
    客户端(用host3模拟)通过在 /tasks 路径下创建一个顺序节点 (-s) 来提交一个新任务。

    # 客户端提交一个任务,其数据为 "cmd"
    [zk: host3:2181(CONNECTED) 6] create -s /tasks/task- "cmd"
    Created /tasks/task-0000000001
    #客户端监控任务是否完成
    [zk: host3:2181(CONNECTED) 7] ls /tasks/task-0000000001 true
    []
    
    

    使用顺序节点,ZooKeeper 会自动为节点名附加一个单调递增的数字后缀。这提供了一个简单且可靠的分布式序列生成功能,可用于任务排序。

  2. 活动主节点分配任务处理

#主节点收到任务通知
[zk: host2:2181(CONNECTED) 5]
WATCHER::

WatchedEvent state:SyncConnected type:NodeChildrenChanged path:/tasks
#主节点将任务分配给host1
[zk: host2:2181(CONNECTED) 5] create -e /assign/worker1.example.com/task-0000000001 ""
Created /assign/worker1.example.com/task-0000000001
#主节点监控任务是否完成
[zk: host2:2181(CONNECTED) 6] ls /tasks/task-0000000001 true
[]
  1. host2处理任务,处理结束,告诉主节点和客户端结束:
#host1收到任务分配通知
[zk: host1:2181(CONNECTED) 4]
WATCHER::
WatchedEvent state:SyncConnected type:NodeChildrenChanged path:/assign/worker1.example.com
#host1完成任务,将任务完成通知写入
[zk: host1:2181(CONNECTED) 4] create /tasks/task-0000000001/status "done"
Created /tasks/task-0000000001/status
#客户端和主节点检测任务状态,如果不是done,则表示任务执行出现错误,可以尝试从新提交等。
[zk: host3:2181(CONNECTED) 5]  get /tasks/task-0000000001/status
done
cZxid = 0x100000037
ctime = Sun Sep 07 02:00:09 PDT 2025
mZxid = 0x100000037
mtime = Sun Sep 07 02:00:09 PDT 2025
pZxid = 0x100000037
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 4
numChildren = 0

  1. 主节点和客户端接收通知
    # 客户端监视任务节点 /tasks/task-0000000001
	[zk: host3:2181(CONNECTED) 8]
	WATCHER::
	
	WatchedEvent state:SyncConnected type:NodeChildrenChanged path:/tasks/task-0000000001
	# 主节点监视任务节点 /tasks/task-0000000001
	[zk: host2:2181(CONNECTED) 7]
	WATCHER::
	
	WatchedEvent state:SyncConnected type:NodeChildrenChanged path:/tasks/task-0000000001

五、 完整交互流程

以下是一个任务从创建到完成的完整事件序列:

  1. 初始化: 主节点 master1 选举成功,开始监视 /workers/tasks
  2. 从节点注册: 从节点 worker1 启动,创建 /workers/worker1.example.com。主节点收到通知,更新其可用从节点列表。
  3. 客户端提交任务: 客户端创建顺序节点 /tasks/task-0000000001
  4. 主节点分配任务:
    • 主节点收到 /tasks 的变化通知,检测到新任务 task-0000000001
    • 主节点查询 /workers,选择可用的 worker1
    • 主节点在 worker1 的分配路径下创建任务指派节点:
      create /assign/worker1.example.com/task-0000000000 ""
  5. 从节点执行任务:
    • worker1 收到其分配路径 /assign/worker1.example.com 的子节点变化通知。
    • 它读取到新分配的任务 task-0000000001,并开始处理。
  6. 反馈结果:
    • worker1 完成任务后,在原始任务节点下创建状态子节点:
      create /tasks/task-0000000000/status "done"
  7. 客户端获取结果:
    • 客户端对 /tasks/task-0000000000 的监视被触发。
    • 客户端和主节点通过 get /tasks/task-0000000000/status 读取到结果 "done",任务流程结束。

结论:ZooKeeper 的核心协调机制

此示例展示了如何组合使用 ZooKeeper 的几个核心功能来实现一个复杂的分布式协调模式:

  • 临时节点 (Ephemeral Nodes): 用于实现成员存活性检测和资源独占锁定。
  • 顺序节点 (Sequential Nodes): 用于实现分布式队列或唯一 ID 生成。
  • 监视点 (Watches): 提供了一个高效的、一次性的事件通知机制,是构建事件驱动系统的基础。

通过这些原语,不同角色的进程无需直接网络通信,而是通过改变和监视 ZooKeeper 中的共享状态来协同工作。这种方式降低了系统各组件间的耦合度,并利用 ZooKeeper 的一致性保证来确保整个系统的正确性。


网站公告

今日签到

点亮在社区的每一天
去签到