ZooKeeper 是一个开源的分布式协调服务,由Apache软件基金会维护。它主要用于解决分布式应用中的一些常见协调问题,例如配置管理、命名服务、分布式同步和集群管理。
核心特点:
-
分布式协调:提供分布式环境下的一致性和同步机制。
-
高可用性:通过集群部署(通常为奇数个节点,如3、5、7)保证服务的高可用性。
-
顺序一致性:所有事务请求按照顺序执行,保证数据的一致性。
-
原子性:更新操作要么全部成功,要么全部失败。
-
可靠性:一旦数据被写入,除非主动删除,否则会一直保留。
-
实时性:在一定时间范围内,客户端可以读到最新的数据。
主要应用场景:
-
配置管理:集中管理分布式系统的配置信息,如数据库连接信息、服务地址等。
-
命名服务:提供分布式系统中的服务注册与发现,类似于DNS。
-
分布式锁:实现跨进程的互斥锁,保证资源访问的互斥性。
-
集群管理:监控节点的状态,实现主节点选举、故障转移等。
-
队列管理:实现简单的分布式队列。
基本架构:
-
数据模型:采用类似文件系统的树形结构(ZNode树),每个节点可以存储少量数据。
-
ZNode类型:
-
持久节点:永久存在,除非手动删除。
-
临时节点:与客户端会话绑定,会话结束则节点自动删除。
-
顺序节点:节点名会自动追加递增的序列号。
-
-
会话机制:客户端与ZooKeeper服务器建立会话,通过心跳保持连接。
-
Watch机制:客户端可以监听ZNode的变化(如数据更新、节点删除),当事件发生时接收通知。
典型工作模式:
-
客户端连接到ZooKeeper集群中的某个节点。
-
通过ZooKeeper的API对ZNode进行增删改查操作。
-
可以设置Watch监听ZNode的变化,实现事件驱动的编程模型。
示例应用:
-
Kafka:使用ZooKeeper管理集群元数据、Broker注册和消费者组信息。
-
Hadoop:用于HDFS的故障恢复和YARN的资源管理。
-
Dubbo:作为注册中心管理服务提供者和消费者。
简单命令行操作示例:
java
# 启动ZooKeeper服务
bin/zkServer.sh start
# 连接客户端
bin/zkCli.sh -server localhost:2181
# 创建节点
create /myapp "appdata"
# 获取节点数据
get /myapp
# 监听节点变化
get -w /myapp
# 删除节点
delete /myapp
Zookeeper的功能详解:
1. 配置管理
集中化配置:
在分布式系统中,将配置信息(如数据库连接、服务地址等)集中存储在 ZooKeeper 的节点(ZNode)中。
核心思想 :将分布式系统中所有服务节点都需要访问的、动态变化的配置信息,从每个应用的本地配置文件中剥离出来,统一存储在一个高可用、强一致的中心化存储服务中,这个服务就是 ZooKeeper。
为什么需要集中化配置?
在传统架构中,配置通常写在每个服务的 application.properties、config.xml 等本地文件中。当系统扩展到成百上千个节点时,这种方式的弊端非常明显:
-
难以维护:修改一个配置(如数据库地址),需要登录每台服务器,逐个修改文件,极易出错和遗漏。
-
无法保证一致性:由于手工操作或部署延迟,不同节点可能使用不同版本的配置,导致系统行为不一致。
-
重启成本高:每次配置变更后,为了生效,通常需要重启应用,这在大型系统中是不可接受的。
ZooKeeper 如何实现?
ZooKeeper 采用一个类似文件系统 的树形结构(ZNode Tree)来存储数据。
-
ZNode:树上的每个节点称为 ZNode,它既可以存储少量数据(通常配置信息就放在这里),也可以有子节点。
-
路径 :每个 ZNode 都有一个唯一的路径,如
/config/app/database/url。
操作示例 :
假设我们有一个微服务集群,需要共享数据库连接字符串。
-
创建配置节点:我们在 ZooKeeper 上创建一个持久节点(Persistent ZNode)
javacreate /config/myapp/db_url "jdbc:mysql://192.168.1.100:3306/mydb" create /config/myapp/db_user "admin" -
应用启动时读取:每个微服务实例在启动时,不再是读取本地文件,而是连接到 ZooKeeper 集群,通过指定路径获取配置。
javaString dbUrl = zk.getData("/config/myapp/db_url", false, null); -
统一管理 :运维人员只需要通过 ZooKeeper 的客户端(CLI 或可视化工具)或运维平台,修改
/config/myapp/db_url节点的数据,即可实现对所有依赖该配置服务的统一更改。
优点:
-
一致性:所有服务实例读取的是同一份数据,来源唯一。
-
实时性:配置变更立即可见(配合 Watcher 机制)。
-
简化运维:配置管理入口单一,易于实现自动化。
动态更新:
应用可以监听配置节点的变化(Watcher 机制),当配置变更时实时获取更新,无需重启服务。
核心思想 :配置集中存储解决了"统一"的问题,但"动态生效"才是关键。ZooKeeper 提供了 Watcher(监听器) 机制,允许客户端在指定的 ZNode 上注册监听。当该节点数据变化 或子节点列表变化 时,ZooKeeper 服务器会主动向注册了 Watcher 的客户端发送一个事件通知。
Watcher 机制的工作流程
这是一个一次性触发、异步通知的发布/订阅模型。
场景延续:我们希望当数据库地址变更时,所有微服务能自动感知并重新连接新数据库,而无需重启。
Watcher 机制的关键特性
-
注册监听 :应用在第一次读取配置时,同时在配置节点上设置一个 Watcher。
java// 伪代码示例 byte[] data = zk.getData("/config/myapp/db_url", new Watcher() { @Override public void process(WatchedEvent event) { // 当节点数据变化时,会触发这个方法 if (event.getType() == EventType.NodeDataChanged) { // 重新读取配置,并应用新配置(如重建连接池) reloadConfigFromZK(); } } }, null);此时,应用本地持有
data(初始配置),并且 ZooKeeper 服务端记录了这个客户端对/config/myapp/db_url节点的监听关系。 -
触发变更:管理员通过客户端更新了该节点的数据。
javaset /config/myapp/db_url "jdbc:mysql://192.168.1.200:3306/mydb" -
服务端通知 :ZooKeeper 服务端检测到该节点的数据变化,会查找所有在该节点上注册了 Watcher 的客户端,并向它们发送一个
NodeDataChanged事件通知。注意:通知只包含事件类型和节点路径,不包含新数据本身。 -
客户端处理 :客户端的 Watcher 回调函数(如
process方法)被异步调用。-
函数内会收到事件,得知
/config/myapp/db_url发生了变化。 -
应用逻辑需要再次主动去 ZooKeeper 获取最新的数据 (
zk.getData)。 -
获取到新数据后(
jdbc:mysql://192.168.1.200...),执行动态更新逻辑,例如:重建数据库连接池、更新内存中的缓存配置等。
-
-
一次性触发:Watcher 被触发后就会失效。这是为了避免大量事件通知压垮网络和客户端,同时也简化了服务端状态管理。客户端必须显式重新注册才能继续监听。
-
异步通知 :通知从服务端发送到客户端是异步的。在通知到达和客户端处理完成之间,节点的数据可能已经再次发生了变化。客户端最终读取到的是触发Watcher后最新的数据。
-
轻量级:网络传输的只有事件类型和节点路径,不包含数据,开销小。
-
顺序保证:客户端会按照事件发生的顺序收到通知。
- 重新注册 :Watcher 是一次性 的。在
process方法中,通常会在重新调用getData方法时,再次注册新的 Watcher,以便持续监听后续的变更。这是一个典型的循环监听模式。
- 重新注册 :Watcher 是一次性 的。在
总结:配置管理的最佳实践
结合以上两点,一个健壮的、基于 ZooKeeper 的动态配置管理中心工作流程如下:
初始化:
-
应用启动,连接 ZooKeeper。
-
读取配置节点(如
/config/myapp下的所有子节点)的初始数据。 -
在关心的所有配置节点上注册 Watcher。
运行与监听:
-
应用使用内存中的配置正常运行。
-
ZooKeeper 服务端维护着监听关系。
动态更新:
-
运维人员修改 ZooKeeper 中的配置数据。
-
ZooKeeper 服务端通知所有注册了 Watcher 的客户端。
-
客户端回调函数执行,重新拉取新配置并应用 (如重启线程池、重连数据源等),并重新注册 Watcher。
容错与回退:
-
通常会在客户端设计一个本地缓存文件(例如
/data/app/config.cache)。 -
应用启动时,先尝试从 ZooKeeper 获取配置;如果 ZooKeeper 不可用,则降级使用本地缓存文件,保证系统能启动。
-
从 ZooKeeper 获取到新配置后,同时更新本地缓存。
典型应用场景:
通过 ZooKeeper 的集中化配置和 Watcher 机制,分布式系统实现了 "一处修改,处处生效,实时更新" 的配置管理能力,极大地提升了系统的可维护性和弹性
-
数据库、消息队列等中间件的连接地址和参数。
-
功能开关(Feature Flags)的动态开启与关闭。
-
负载均衡中的服务器列表(Service Registry)。
-
限流、降级规则的动态调整。
2. 命名服务
- 服务注册与发现:通过创建唯一的路径节点,为分布式系统提供全局统一的命名服务。例如,Dubbo、Kafka 等框架使用 ZooKeeper 注册服务地址,消费者通过它发现服务。
核心机制:利用 ZNode 特性
ZooKeeper 实现命名和服务发现,主要依赖于其数据模型 ZNode 的以下几个关键特性:
-
层次化命名空间 :类似文件系统的目录树结构,路径(如
/services/dubbo/com.example.UserService/providers/host:port)本身就是一种清晰、可分类的命名。 -
持久节点与临时节点:
-
持久节点(Persistent):用于存储静态的、长期存在的元数据(例如服务接口定义、配置)。
-
临时节点(Ephemeral) :这是实现服务健康状态自动管理的核心 。当服务提供者(Provider)启动时,在特定路径下创建一个临时节点来代表自己。如果该提供者进程崩溃或与 ZooKeeper 失联(会话结束),这个临时节点会被自动删除。
-
-
顺序节点:可以创建具有全局唯一递增后缀的节点,常用于实现分布式锁、选主,在服务注册中也能保证节点名称唯一。
-
Watch 机制 :客户端(服务消费者)可以在关心的 ZNode 上设置监听(Watch)。当该节点的子节点列表发生变化(如服务提供者上线或下线)时,ZooKeeper 会主动通知客户端。这使得消费者能近乎实时地感知到服务列表的变动。
服务注册与发现的工作流程
我们以 Dubbo 这类 RPC 框架为例,拆解整个过程:
1. 服务注册(Service Registration)
-
服务提供者 启动时,连接到 ZooKeeper 集群。
-
在预定的命名空间路径下(例如:
/dubbo/{serviceInterface}/providers),创建一个临时的、携带自身地址信息(如host:port)的子节点。- 例如:创建一个节点
/dubbo/com.example.UserService/providers/192.168.1.100:20880。
- 例如:创建一个节点
-
注册完成。这个临时节点就代表了该服务提供者的在线状态。
2. 服务发现(Service Discovery)
-
服务消费者 启动时,也连接到 ZooKeeper 集群。
-
它找到服务接口对应的路径(
/dubbo/com.example.UserService/providers),并获取该路径下所有子节点。 -
子节点列表就是当前所有可用服务提供者的地址列表。消费者可以缓存这个列表,并根据负载均衡策略(如随机、轮询)选择一个提供者进行调用。
3. 动态感知与健康检查(核心优势)
-
消费者在第一次获取列表后,会在该父节点上设置一个 Watch,监听子节点的变化。
-
当有新的服务提供者上线(创建新的临时子节点)时,ZooKeeper 会通知消费者。消费者更新本地缓存的服务列表。
-
当有服务提供者宕机或下线(临时节点被自动删除或主动删除)时,ZooKeeper 同样会通知消费者。消费者从本地列表中移除该不可用的提供者。
-
这个过程实现了自动化的服务健康检查和故障转移,无需中心化的心跳检测系统。
| 功能模块 | ZooKeeper 做的事情 | Dubbo 做的事情 |
|---|---|---|
| 地址存储 | 存储节点数据(二进制) | 定义数据结构、序列化 |
| 健康检测 | 会话心跳、节点自动删除 | 无应用层健康检查 |
| 服务发现 | 返回节点列表、发送变更通知 | 解析地址、维护缓存、负载均衡 |
| 配置管理 | 存储配置数据 | 定义配置格式、动态读取、应用配置 |
| 路由规则 | 存储规则数据 | 解析规则、过滤提供者、执行路由逻辑 |
| 集群容错 | 无 | 失败重试、故障转移、熔断降级 |
| 监控统计 | 无 | 收集指标、统计分析、上报监控 |
3. 分布式锁
互斥锁:
- 多个进程/服务通过竞争创建临时有序节点(或临时节点),实现互斥访问共享资源(监听前一个节点的删除)。
分布式锁是ZooKeeper临时顺序节点和Watch机制的完美结合。假设多个客户端要竞争一个资源锁,它们在ZooKeeper上的操作如下:
第一步:所有竞争者创建临时顺序节点
所有想获得锁的客户端,都在锁的父目录(如 /locks/my_lock)下创建临时顺序节点。
排序的依据不是客户端的本地请求时间,而是ZooKeeper服务器收到请求并处理时分配的全局唯一严格递增的"事务ID"(zxid)。
java
# 5个客户端竞争,创建了5个节点
/locks/my_lock/lock_000000001 # 客户端A
/locks/my_lock/lock_000000002 # 客户端B
/locks/my_lock/lock_000000003 # 客户端C
...
第二步:判断自己是否为序号最小的节点
-
每个客户端获取
/locks/my_lock下的所有子节点,并按序号排序。 -
核心规则 :创建了最小序号节点(
lock_000000001)的客户端A,成功获得锁。
第三步:未获锁者进入"排队等待"状态
-
没有获得锁的客户端(如B、C)不需要轮询查询。
-
每个客户端只需监听(Watch)比自己序号小的最后一个节点的"删除"事件。
-
客户端B监听
lock_000000001。 -
客户端C监听
lock_000000002。
-
第四步:锁的释放与传递
-
客户端A完成任务后,主动删除 自己的节点
lock_000000001,或因其会话断开而自动删除。 -
节点删除事件触发,一直监听着它的客户端B立刻收到通知。
-
客户端B被唤醒,重复第二步 :再次获取所有子节点,此时它发现自己成为了新的最小序号节点(
lock_000000002),于是成功获得锁。 -
客户端C则转而监听新的最小节点(
lock_000000002,即B的节点)。
这种设计保证了:
-
公平性:严格按申请顺序(节点序号)获得锁,先到先得。
-
可靠性:锁持有者宕机,其临时节点自动删除,锁自然释放,无死锁风险。
-
高效性:等待者通过事件通知被唤醒,避免了盲目的轮询,节省资源。
简单来说,ZooKeeper通过临时顺序节点 建立了一个"排队取号系统 ",通过 Watch监听 实现了一个"叫号通知系统",两者结合便形成了一个强大、公平、可靠的分布式锁。
读写锁:
- 通过节点设计区分读锁和写锁,支持更细粒度的并发控制(监听前一个节点锁状态的变化)。
读锁(共享锁)和写锁(排他锁)是通过临时顺序节点(EPHEMERAL_SEQUENTIAL)的不同命名规则和获取逻辑来区分的
常见的做法是:
读锁节点 :/locks/mylock/read-0000000001
写锁节点 :/locks/mylock/write-0000000002
通常用节点名来区分类型,例如:
-
写锁请求节点名:
write-前缀 -
读锁请求节点名:
read-前缀
读写锁(Shared Lock / Exclusive Lock)的逻辑比互斥锁复杂,因为它需要区分"读"与"写"的冲突规则:读读不互斥,读写互斥,写写互斥。
-
实现逻辑:
-
写请求 :与互斥锁一样,只监听比自己序号小的上一个节点(无论是读还是写)。
-
读请求:
-
向比自己序号小的节点中寻找最后一个写请求节点(Write Node)。
-
如果你前面全是读请求,你可以直接获取锁。
-
如果你前面有写请求,你只需要监听距离你最近的那个写节点。
-
-
-
状态变化监听: 在这种模式下,客户端不只是看节点消失,更是在判断"阻碍我运行的那个特定状态"是否消失。
两种模式的对比总结
| 特性 | 标准互斥锁 (Mutex) | 读写锁 (ReadWrite Lock) |
|---|---|---|
| 监听对象 | 始终是序号比自己小的前一个节点 | 根据类型决定:读请求找前一个写 节点;写请求找前一个节点 |
| 并发性能 | 较低(完全串行) | 较高(允许多个读操作并行) |
| 应用场景 | 独占资源修改(如余额扣减) | 高频读取、低频修改的场景(如配置分发) |
4. 集群管理
节点监控:
- 通过临时节点(Ephemeral Nodes)监控集群节点状态。当节点失效时,其创建的临时节点会自动删除,其他节点可据此感知故障。
这是 Zookeeper 实现服务发现和存活检测的经典模式。
-
核心逻辑 :当客户端(集群节点)与 Zookeeper 建立会话(Session)后,创建的临时节点 的生命周期将与这个会话绑定。
-
故障感知流程:
-
注册 :集群中的每个节点在启动时,都在一个约定的父节点(如
/cluster/members)下为自己创建一个临时节点 (如/cluster/members/node-1)。 -
监听:所有节点都监听(Watch)这个父节点的子节点列表变化。
-
故障发生 :当某个节点宕机或网络分区导致其与 Zookeeper 的会话超时断开时。
-
自动清理:Zookeeper 服务端会自动删除该会话创建的所有临时节点。
-
通知 :父节点的子节点列表发生变化,Zookeeper 会向所有监听了该父节点的客户端发送
NodeChildrenChanged事件。 -
处理:其他节点收到通知后,会去获取最新的子节点列表,从而立即知道是哪个节点失效了。
-
优点:
-
实时性强:相比心跳轮询,这种事件驱动模型能更快发现故障。
-
服务发现 :新节点加入时,创建自己的临时节点,其他节点同样能感知到新成员。
ls命令即可获取当前所有活跃节点。
选主机制:
- 利用临时有序节点和 Watcher 机制,实现分布式选举(如 Kafka 控制器选举)。
这是实现分布式锁 和领导者选举(Leader Election)的经典模式,比临时节点更进一步。
-
核心逻辑 :节点在创建临时节点时,Zookeeper 会自动在其路径末尾附加一个单调递增的序列号,形成临时有序节点 。选举时,序号最小的节点通常被选为 Leader。
-
经典选举流程(以抢主为例):
-
发起竞选 :所有候选节点都在一个约定的父节点(如
/election)下创建临时有序节点,例如:-
Node-A 创建了
/election/guid-n_0000000001 -
Node-B 创建了
/election/guid-n_0000000002 -
Node-C 创建了
/election/guid-n_0000000003
-
-
判断是否为主 :每个节点都获取父节点的子节点列表,并按序号排序。序号最小的节点(这里是Node-A) 即成为 Leader。
-
监听前驱节点 :非Leader节点 (Node-B, Node-C)无需监听所有节点,它们只需要监听比自己序号小的最后一个节点 (即它的前驱节点)。
-
Node-B 监听
...0000000001(Node-A) -
Node-C 监听
...0000000002(Node-B)
-
-
主节点故障处理:
-
如果 Leader(Node-A)故障,其临时节点被自动删除。
-
此时,Node-B(监听着Node-A)会收到节点删除的通知。
-
Node-B 被唤醒,重新获取子节点列表,发现自己已成为新的最小序号节点(权重较大,还有事务版本号等其他判断),从而晋升为新的 Leader。
-
之后,Node-C 会开始监听新的前驱节点 Node-B。
-
-
"羊群效应"避免 :关键点在于第3步的"监听前驱节点",而不是所有节点都监听Leader。这避免了当Leader挂掉时,所有节点同时被唤醒去竞争,造成网络和服务器的冲击(即"羊群效应")。
-
5. 数据发布/订阅
- 服务将数据发布到 ZooKeeper 的节点上,订阅者通过 Watcher 机制监听节点变化,实现数据的动态推送。
-
数据发布(发布者):
-
发布者将需要共享的配置、状态或元数据写入 Zookeeper 的一个特定ZNode (通常是持久节点)。
-
例如,写入路径
/configs/service_database_url,数据内容为"jdbc:mysql://master:3306"。
-
-
数据订阅(订阅者):
-
所有相关的订阅者(客户端应用)在启动时,会先读取
/configs/service_database_url节点上的当前数据。 -
更重要的是,它们在读取数据的同时,会通过
getData()或exists()方法,在该ZNode上注册一个Watcher(监听器)。
-
-
动态推送(Zookeeper 的 Watcher 触发):
-
当发布者更新了
/configs/service_database_url节点的数据(例如,切换到备库:"jdbc:mysql://slave:3306")时,Zookeeper 服务端会检测到这次变更。 -
服务端会主动通知所有在该节点上注册了Watcher的客户端。
-
客户端收到通知(一个事件,如
NodeDataChanged)后,会再次主动去拉取 节点的最新数据,并更新本地配置,同时重新注册Watcher以监听下一次变更。
-
关键特性与优势
-
最终一致性:所有订阅者最终都会收到更新,并保持一致的状态。虽然存在短暂的不一致窗口,但这是分布式系统可接受的。
-
解耦:发布者和订阅者不需要知道彼此的存在,只与 Zookeeper 交互,降低了系统耦合度。
-
可靠性:数据存储在 Zookeeper 集群中,具备高可用性。
-
实时性:相比客户端轮询,Watcher 机制能更及时地感知变化。
6. 队列管理
- 通过持久化节点和顺序节点实现同步队列 或优先级队列,协调任务调度。
持久化节点(PERSISTENT)
-
创建后一直存在,除非主动删除。
-
用于存储队列元信息或配置。
2. 顺序持久化节点(PERSISTENT_SEQUENTIAL)
-
在节点名后附加单调递增的数字序号。
-
保证多个客户端创建节点时的顺序性。
-
这是实现有序队列的关键。
3. 临时节点(EPHEMERAL)
-
客户端会话断开后自动删除。
-
可用于消费者存活检测。
4. 临时顺序节点(EPHEMERAL_SEQUENTIAL)
-
结合顺序性和临时性。
-
常用于分布式锁和队列消费者协调。
7. 一致性保障
- 基于 ZAB 协议(ZooKeeper Atomic Broadcast)保证数据一致性,提供顺序一致性和原子性操作。
核心:ZAB协议是基石
ZAB协议是Zookeeper自己设计的、专门为主从架构 的协调服务(写操作频繁但状态变更不频繁)设计的崩溃可恢复的原子广播协议。它是Zookeeper强一致性的根本保证。
ZAB的两个核心阶段:
-
选举阶段 :当Leader崩溃或与多数Follower失去联系时,进入此阶段。通过选举算法(基于
zxid和myid)选出新的Leader,确保被推举的节点拥有集群中最全的数据。 -
广播阶段(消息广播/原子广播):这是正常状态下的数据同步过程。
-
Leader提案 :Leader为每个写请求生成一个全局单调递增的事务ID ,并为其创建一个提案发送给所有Follower。
-
Follower确认:Follower收到提案后,将其写入本地事务日志,并返回ACK给Leader。
-
Leader提交 :当Leader收到超过半数 Follower的ACK后,就认为提案已提交,它会自己先应用提案,然后发送
COMMIT消息给所有Follower,通知它们应用该提案。
-
关键特性:
-
可靠提交:只有被多数节点持久化的提案才会被提交。
-
全局有序 :所有被提交的提案在服务器上都以相同顺序被应用。这是顺序一致性的基础。
-
单一主节点:所有写请求都必须经过Leader,保证了写操作的线性顺序。
- 支持多种一致性模式(如顺序一致性、最终一致性)。
-
顺序一致性
-
全局有序 :这是最重要的保证。来自客户端的所有写操作 (无论通过哪个服务器)都会被分配一个全局唯一的、递增的
zxid,所有服务器都严格按照这个顺序来应用状态变更。这意味着,如果一个写操作A先于写操作B被Leader提交,那么在所有服务器上,状态A都一定出现在状态B之前。 -
FIFO客户端顺序 :来自同一个客户端 的所有请求(包括读写),会严格按照其发出的顺序被服务器处理和执行。这保证了客户端会话内的操作顺序。
-
-
原子性
- 一个事务操作要么在所有服务器上都成功应用,要么在所有服务器上都不应用(失败回滚),不存在中间状态。这是通过ZAB的"超过半数确认才提交"机制保证的。
8. 高可用性
- 采用多节点集群部署,Leader-Follower 架构,故障时自动切换 Leader,保障服务持续可用。
1. 集群角色与法定人数(Quorum)
-
典型部署 :一个Zookeeper集群通常由奇数台服务器组成(如3、5、7台)。
-
角色:
-
Leader :唯一,负责处理所有写请求 和事务性操作,是集群的大脑。
-
Follower :参与写投票 ,处理读请求,并参与Leader选举。如果Leader失效,Follower有机会被选为新的Leader。
-
Observer (可选):不参与写投票 ,只异步同步数据 并处理读请求 。用于横向扩展读性能,不影响写操作的投票效率,从而提升集群整体吞吐量。
-
-
法定人数 :ZAB协议要求任何写操作(提案)必须得到
N/2 + 1(多数派)节点的确认才能提交。这是集群能正常工作的最低存活节点数。- 例如:一个3节点集群,至少需要2个节点存活;一个5节点集群,至少需要3个节点存活。
2. 自动故障切换流程(核心)
这是高可用的动态体现。当Leader失效(崩溃、网络分区),集群会自动且快速地恢复服务。
步骤:
-
故障检测 :Follower通过心跳(通过TCP长连接)与Leader保持通信。如果在一定时间内(
tickTime * initLimit)收不到Leader的心跳,Follower会认为Leader已失效。 -
进入选举状态 :这些Follower会将自己的状态从
FOLLOWING改为LOOKING,并开始新一轮的Leader选举。 -
领导者选举:
-
每个参与选举的节点会广播自己的投票。投票内容包含两个最关键的信息:
-
epoch:逻辑时钟,标识Leader的任期。 -
zxid:节点本地所见的最新事务ID。
-
-
选举规则 :节点会优先投票给拥有最高
zxid的节点,因为这代表它拥有最新的数据。如果zxid相同,则投票给server id更大的节点。 -
选举结果 :当某个节点获得超过半数 的投票时,它即当选为新Leader。选举算法(如FastLeaderElection)保证了快速收敛,通常能在几百毫秒内完成。
-
-
数据同步与恢复:
-
新Leader会确认自己的
epoch比旧Leader的大。 -
然后,它会与其他Follower进行数据同步 ,确保所有已提交 的提案在所有节点上一致。拥有更高
zxid的Follower会向拥有较低zxid的节点进行同步。
-
-
恢复服务:
-
新Leader完成数据同步后,集群进入新的广播阶段,开始正常处理客户端请求。
-
客户端库会感知到与旧Leader的连接中断,并自动重连到新的Leader或其他Follower。
-
3. 客户端透明故障处理
高可用性不仅体现在服务端,客户端库也起到了关键作用:
-
会话(Session) :客户端与Zookeeper服务器建立连接时,会创建一个会话。会话是绑定在集群上的,而非单个服务器。
-
自动重连:当客户端连接的服务器失效时,ZooKeeper客户端库会自动尝试连接集群中的其他服务器。
-
会话转移 :只要在会话超时时间内(
sessionTimeout)重新连接成功,客户的会话 以及在该会话中创建的临时节点 和Watcher都会被保留。这对上层应用是透明的。
4. 高可用性特点总结
| 特性 | 实现机制 | 对高可用的贡献 |
|---|---|---|
| 无单点故障 | 多节点集群,Leader可动态切换 | 核心机器故障不影响整体服务 |
| 快速故障恢复 | 基于 zxid 和 server id 的快速选举算法 |
服务中断时间极短(秒级) |
| 数据持久性 | 事务日志和快照定期落盘 | 节点重启后数据不丢失,保证服务状态连续 |
| 读高可用与扩展 | Follower/Observer处理读请求,客户端可连接任一节点 | 读请求负载均衡,读性能可水平扩展 |
| 客户端透明性 | 客户端库自动处理连接断开与重连、会话转移 | 上层应用无需复杂容错逻辑 |
设计权衡与注意事项
-
性能与一致性 :高可用性建立在强一致性基础上。写操作必须经过Leader和多数派确认,这带来了延迟,但保证了数据的可靠。
-
集群规模 :增加节点数(如从3个到5个)能提高容错能力 (允许更多机器故障),但可能降低写性能(需要更多确认)。Observer节点是提升读性能而不影响写性能的优选。
-
脑裂预防 :ZAB的多数派原则 天然防止了脑裂。在网络分区时,只有拥有多数节点的分区能选举出Leader并继续提供服务,少数节点分区将无法工作,从而保证了数据一致性。
-
配置优化 :
tickTime、initLimit、syncLimit等参数的设置会影响故障检测和恢复的速度,需要根据网络环境和硬件性能进行调优。