分布式中件间-zookeeper原理分析
Zookeeper 是一个开源的分布式协调框架,基于ZAB协议来确保在分布式环境下的数据一致性和可靠性,实现了一个高可用的、小型的、树形结构(类似文件系统)的数据存储。通常用于分布式系统中的配置管理、同步服务、命名服务等,Zookeeper 主要用于以下几种场景:
- 分布式锁:通过 Zookeeper 提供的节点机制,可以实现分布式环境中的锁机制。
- 配置管理:Zookeeper 用作分布式系统的配置中心,客户端可以从 Zookeeper 获取共享的配置信息。
- 命名服务:Zookeeper 可以作为一个高效的命名服务,提供唯一的命名空间。
- 集群管理:Zookeeper 可以用来管理分布式系统中节点的健康状况和成员变更。
架构
ZooKeeper 的架构设计旨在提供一个高可用、高性能、强一致性的分布式协调服务。其架构可以清晰地分为两个主要视角:逻辑架构(系统分层) 和 物理架构(部署模式)。
逻辑架构(系统分层)
| 层次 | 名称 | 描述 |
|---|---|---|
| API 层 | Client Interface | 提供客户端访问接口,支持 Java、C 等多种语言,封装了网络通信、会话管理、数据操作和 Watcher 注册等逻辑。 |
| 服务层 | Services | ZooKeeper 的核心业务逻辑层。负责处理客户端请求(读写分离)、管理会话 (Session)、处理 Watcher 事件通知、以及权限控制 (ACL) 等。 |
| 一致性层 | Distributed Consensus | 实现了 Zab (ZooKeeper Atomic Broadcast) 协议,是 ZooKeeper 保证数据一致性的关键。负责 Leader 选举、原子广播、数据同步和事务提交。 |
| 数据层 | Data Registry | 负责数据存储和持久化。数据存储在内存的 DataTree 中,同时通过事务日志(Transaction Log)和快照(Snapshot)确保数据持久性。 |
物理架构(部署模式)
在物理部署上,ZooKeeper 集群通常采用主备(Master-Replica)架构,由一组互相协作的服务器构成(通常建议部署奇数个节点,如 3、5、7 个)。集群中的每个节点根据 Zab 协议的角色,处于以下三种状态之一:
- Leader (领导者): 整个集群中唯一一个具有写权限的节点,负责处理所有的写请求(事务请求)。将写请求通过 Zab 协议广播给所有 Follower。负责集群内部的协调和管理,如管理 Session 超时、发起心跳等。
- Follower (跟随者): 具有读权限,可以处理客户端的读请求(读取本地内存数据)。参与 Leader 选举的投票过程,同时接收 Leader 的提案(Proposal),参与事务的多数派确认。
- Observer (观察者): 可选角色,ZooKeeper 3.x 版本引入。具有读权限,不参与 Leader 选举投票,也不参与事务的多数派确认。主要用于扩展集群的读性能,同时避免影响集群写操作的性能和可用性。
物理架构特点:
- 读写分离: 所有写操作都由 Leader 协调,读操作可以在任何 Follower/Observer 上执行,大大提高了系统的并发处理能力。
- 高可用性: 只要集群中大多数节点(超过半数)存活,集群就能正常对外提供服务(N个节点容忍 (N-1)/2 个节点失效)。
- 数据一致性: 依赖 Zab 协议保证所有客户端看到的数据视图是一致的、有序的。
Zookeeper核心原理分析
ZooKeeper 的实现原理核心在于其分布式协调机制,主要依赖于原子广播(Zab 协议)、Leader 选举、数据模型和 Watcher 机制 来确保分布式系统中的数据一致性和高可用性。其中,Zab 协议 是 ZooKeeper 保证数据一致性的核心算法,它要求所有事务请求的处理结果在整个集群中所有机器上的应用情况是一致的(原子性),并且保证事务的顺序一致性。
数据模型-Znode
ZooKeeper 的数据模型是一种层次化的、树形结构,类似于一个精简版的文件系统,每个节点为一个 ZNode。ZooKeeper 的数据模型围绕着 ZNode(ZooKeeper Node)构建,形成一个类似于 Unix 文件系统的倒立树状结构,根节点为 /。
- 路径(Path):ZNode 在层次结构中的唯一标识,由斜杠(/)分隔的路径元素组成,类似于文件系统路径。
- 数据(Data):ZNode 存储的字节数组形式的数据。ZooKeeper 的设计初衷是存储协调数据,而非大规模数据,因此单个 ZNode 的数据大小通常限制在 1MB 以内。
- ACL(访问控制列表):类似于文件权限,用于控制谁可以对 ZNode 执行读、写、创建、删除等操作。
- 元数据(Stat):包含 ZNode 的详细状态信息,是实现分布式协调的关键。
Znode类型
Znode 的类型是 ZooKeeper 数据模型中最具特色的部分,也是它实现各种协调功能(如服务发现、分布式锁)的基石。Znode 根据其生命周期和命名特性,可以组合成四种类型:
- PERSISTENT(持久节点):一旦创建,该节点将永久存在于 ZooKeeper 中,直到被显式删除。它不依赖于创建它的客户端会话。通常用于存储需要持久化的信息,如服务配置、ACL 规则等。
- EPHEMERAL(临时节点):临时节点的生命周期与创建它的客户端 Session(会话) 严格绑定。当客户端的 Session 启动时,它可以创建临时节点;当 Session 正常关闭(客户端主动 close)时,该临时节点被删除;当 Session 异常终止(例如客户端宕机、网络分区导致心跳超时)时,ZooKeeper 服务器会在 sessionTimeout 之后自动删除该临时节点。另外临时节点是不能有子节点的。临时节点通常用于服务发现和Leader 选举,服务提供者启动时,在 /services/my-service/ 目录下创建一个代表自己的临时节点(如 /services/my-service/provider-01)。服务消费者监听 /services/my-service 的子节点列表。当服务提供者宕机时,Session 超时,临时节点自动删除,消费者会立即收到通知,从而知道该服务已下线,同时集群中的其他节点可以通过检查(health-check)某个临时节点是否存在,来判断对应的服务是否存活。
- PERSISTENT_SEQUENTIAL(持久顺序节点):在创建时,ZooKeeper 会自动在客户端指定的路径后附加一个单调递增的 10 位数字序号,其具有持久节点的特性(永久存在)。通常应用于分布式ID和分布式锁,利用单调递增的特性可以生成全局唯一的、有序的 ID。在实现分布式锁(公平锁)时所有尝试获取锁的客户端都在锁节点下创建持久顺序节点,序号最小的节点获得锁。
- EPHEMERAL_SEQUENTIAL(临时顺序节点):同时具备临时节点(生命周期绑定 Session)和顺序节点(路径自动附加序号)的特性,这是实现分布式公平锁的完美模型。
- 获取锁:在 /locks/my-lock/ 下创建临时顺序节点。
- 判断:获取 /locks/my-lock/ 下的所有子节点,如果自己创建的节点序号最小,则获得锁。
- 释放锁(优雅):
- 如果客户端宕机,临时节点自动删除(防止死锁)。
- 如果客户端正常完成任务,主动删除该节点。
- 容器节点 (Container):3.5.x+ 新增,当所有子节点被删除后,自身会被自动清理。通常用于动态集群管理。
注意:在使用EPHEMERAL_SEQUENTIAL(临时顺序节点)实现分布式锁时应当避免“羊群效应”:如果自己不是序号最小的,不需要监听锁节点本身,而是只监听自己序号前一个的那个节点。当前一个节点被删除时(即前一个锁释放了),自己会收到通知,此时再次检查自己是否为最小序号。
Znode数据结构
每个 Znode 不仅仅是一个路径,它还包含了三个关键部分:Data(数据)、Stat(元数据)、ACL(访问控制列表)。
Data(数据):每个 Znode 都可以存储一小段数据(默认最大 1MB),ZooKeeper 的设计不是用来做大数据存储的,而是用来存储“协调数据”,如配置信息、状态标识、元数据等。1MB 的限制就是为了防止滥用,保证 ZK 的高性能和低延迟。另外,Znode数据的读写是原子性的,要么读取全部数据,要么写入全部数据,没有部分读写的概念。
Stat(元数据):Stat 结构包含了多个字段,这些字段共同提供了 ZNode 的完整生命周期信息,是 ZooKeeper 实现高一致性和乐观锁机制的基础。Stat对象关键属性如下:
| 字段名称 | 描述 |
|---|---|
czxid |
创建 ZNode 的事务 ID(zxid)。每当 ZooKeeper 的状态发生改变,都会分配一个全局唯一的 zxid。czxid 记录了创建该 ZNode 的操作对应的 zxid。 |
mzxid |
最后修改 ZNode 的事务 ID(zxid)。每当 ZNode 的数据发生变化,该字段都会更新。 |
pzxid |
最后修改 ZNode 子节点的事务 ID(zxid)。当 ZNode 的子节点被添加或删除时,pzxid 字段会更新。pzxid 对于数据缓存和事件通知非常重要。 |
ctime |
ZNode 的创建时间,以毫秒为单位。 |
mtime |
ZNode 的最后修改时间,以毫秒为单位。 |
version |
数据版本号。每当 ZNode 的数据被修改一次,version 的值就会加 1。这是实现乐观锁的核心机制,客户端在更新数据时需要提供期望的版本号。 |
cversion |
子节点版本号。每当 ZNode 的子节点列表发生变化(增加或删除子节点),cversion 的值就会加 1。 |
aversion |
ACL 版本号。每当 ZNode 的 ACL 发生变化,aversion 的值就会加 1。 |
ephemeralOwner |
临时节点的会话 ID。如果该 ZNode 是临时节点,该字段记录了拥有它的客户端的会话 ID。如果客户端断开连接,会话结束,该临时节点就会被自动删除。对于持久节点,该值为 0。 |
dataLength |
ZNode 数据内容的长度。 |
numChildren |
ZNode 的子节点数量。 |
关键元数据字段的作用说明:
- ZXID(事务 ID):一个 64 位的数字,由两部分组成。高 32 位表示当前的 epoch(时代),每次进行 Leader 选举时都会增加。低 32 位表示该 epoch 内的事务计数器,每次写操作都会增加。ZXID 暴露了所有变更的全局顺序性,较小的 ZXID 总是先于较大的 ZXID 发生,这是保证一致性的关键。
- 版本号(version、cversion、aversion):版本号机制是实现乐观锁的基础。客户端在更新或删除 ZNode 时,可以指定期望的版本号,如果服务器上的 ZNode 版本号与客户端提供的版本号不一致,操作就会失败,从而避免了并发修改导致的数据不一致问题。
- 临时节点和 ephemeralOwner:临时节点是实现分布式锁、服务发现和 Leader 选举的核心。ephemeralOwner 字段确保了当客户端会话过期或断开连接时,相关的临时节点能被自动清理,从而释放资源或触发重新选举。
- ACL(访问控制列表):ZooKeeper 提供了一套 ACL 机制来控制对 Znode 的访问(读、写、创建、删除、管理)。这为 ZooKeeper 上的敏感数据(如配置信息)提供了安全保障。
Zookeeper 数据存储
ZooKeeper 的数据存储原理是它实现高性能、高可用和一致性的保证,与传统数据库(如 MySQL)主要依赖磁盘存储不同,ZooKeeper 的存储原理是以内存为中心,辅以持久化的事务日志和快照。ZK 数据存储主要有三个核心:内存数据 (DataTree)、事务日志 (Transaction Log)、数据快照 (Snapshot)。
内存数据 (DataTree)
Zookeeper 集群中的每个节点(Leader 或 Follower)都维护一个内存中的数据树,称为 内存数据存储树(In-memory Data Tree)。这个数据树保存了所有 ZNode 的结构和数据。这种设计使得 Zookeeper 在读取操作时非常快速,因为数据直接从内存中读取。当客户端发起一个读请求(如 get /path 或 ls /path)时,ZooKeeper 服务器直接从内存中读取数据并返回,这个过程不涉及任何磁盘 I/O,因此速度极快,这也是 ZK 适合作为“协调”服务(读多写少)的原因。
DataTree 的主要组成部分:
- DataNode:代表 ZNode,每个 DataNode 对象对应于树中的一个 ZNode。DataNode 中包含 ZNode 的所有数据,包括路径、字节数组形式的数据内容(data)、ACL 信息和 Stat 元数据,每个 DataNode 维护对其父节点和子节点列表的引用,从而构建出完整的树形结构。
- ConcurrentHashMap:为了提高查找效率,DataTree 使用一个 ConcurrentHashMap来映射 ZNode 的完整路径(String path)到其对应的 DataNode 对象。客户端可以通过 ZNode 的路径快速定位到内存中的 DataNode,而无需遍历整个树。
- 其他信息:DataTree 还维护了关于会话、观察者(Watcher)和临时节点(ephemeral nodes)的信息,这些都存储在内存中,以便快速访问和管理。
ZooKeeper 定义了一个内存数据ZKDatabase,它封装了 DataTree,是Zookeeper数据管理工具。ZKDatabase 除了管理 DataTree 外,还负责事务日志(WAL)和数据快照的管理,从而实现内存数据与磁盘数据的同步。
事务日志 (Transaction Log) - 保证持久化
ZooKeeper 的事务日志(Transaction Log),也称为 预写日志(Write-Ahead Log, WAL),是其实现数据持久化和故障恢复的核心机制之一。尽管 ZooKeeper 的数据主要在内存中操作以保证高性能,但事务日志确保了即使在服务器崩溃的情况下,所有已提交的事务也不会丢失。 事务日志是 ZooKeeper 用来记录所有写操作(如创建、修改、删除 ZNode)的日志文件。其工作流程遵循 “先写日志,再执行操作” 的原则。
事务流程如下:
- 写请求到达:当客户端发起一个写请求时,该请求首先被 Leader 服务器接收。
- 生成事务:Leader 为该请求分配一个唯一的事务ID(zxid),并生成相应的事务(Txn),其中包含操作类型和数据变更内容。
- Follower ACK:Leader 在将事务应用到内存中的 DataTree 之前,会广播到Follower节点。Follower会将该事务以日志条目的形式追加写入到磁盘上的事务日志文件中(预提交)。写入成功后,Follower 会向 Leader 发送一个确认(Ack)消息,表示它已经记录了这个事务,并准备好提交。
- 多数派提交:当 Leader 收到集群中多数 Follower 的确认后,Leader 会向所有 Follower 广播一个 Commit 消息,要求所有Follower节点提交该 ZXID 对应的事务。最终,Leader 也在本地提交该事务,并响应客户端。
事务日志的关键特性
- 顺序写入:事务日志采用追加写入的方式,这是一种顺序 I/O 操作。顺序写入比随机写入磁盘的性能高很多,保证了 ZooKeeper 对写请求的高吞吐量。
- 高性能要求:由于所有写操作都需要先写入事务日志并同步到磁盘,因此磁盘的性能是影响 ZooKeeper 性能的关键因素。官方推荐将事务日志和数据快照目录配置在不同的磁盘设备上,以避免相互影响。
- 文件名与zxid:事务日志文件以 log.
格式命名,其中 代表该日志文件中第一条记录的 zxid。这使得系统能够快速定位需要回放的日志文件。
数据快照 (Snapshot) - 优化恢复速度
ZooKeeper 的数据快照(Snapshot)是其持久化存储机制的重要组成部分,它与事务日志(Transaction Log)协同工作,共同确保数据的可靠性和服务器的快速恢复。
数据快照作用:
- 记录内存状态:数据快照是 ZooKeeper 在特定时间点对内存中 DataTree(ZNode 树)的完整状态进行序列化,并将其写入磁盘文件的过程。它相当于对 ZooKeeper 内存数据的一次“全量备份”。
- 加速恢复:当 ZooKeeper 服务器启动或重启时,如果只依赖事务日志来恢复数据,就需要从头开始回放所有历史事务。随着时间的推移,日志文件会非常庞大,导致恢复时间过长。通过数据快照,服务器只需要加载最新的快照文件,然后回放自该快照生成之后产生的新事务日志,从而大大缩短恢复时间。
- 防止日志无限增长:快照机制允许 ZooKeeper 在生成新快照后,安全地清理掉旧的快照和其之前的事务日志,防止存储空间被无限增长的日志文件耗尽。
快照触发机制
- 事务日志计数:当事务日志记录的写操作数量达到一个预设的阈值(通过配置项 snapCount 控制,默认为100,000)时,ZooKeeper 会触发一次快照。
- 新 Leader 数据同步:在新 Leader 选举完成后,新 Leader 会向 Follower 同步数据。这个过程也可能涉及快照的生成和传输。
快照文件存储在 ZooKeeper 配置中 dataDir 参数指定的目录下。每个快照文件以 snapshot.<zxid> 格式命名,其中 <zxid> 代表该快照所对应的最后一次事务的 ID。在生成快照的过程中,ZooKeeper 仍然会继续处理客户端的请求。因此,快照文件记录的是一个“模糊”的时间点的数据,它并不完全精确地对应某一瞬间,但由于事务日志会继续记录,后续可以基于快照和日志完成精确恢复。
Session与Watcher
ZooKeeper 的 Session(会话)是客户端与 ZooKeeper 集群之间的逻辑连接,管理着客户端连接状态和Znode生命周期,其核心在于心跳检测、超时管理和状态同步。
Session的生命周期:
- 创建(Establish):当客户端第一次连接到Zookeeper集群时,会尝试与集群中的一个服务器建立TCP连接,并向服务器发送连接请求,服务器会为客户端分配一个唯一的Session ID,并设置一个Session Timeout。客户端会与服务器之间会建立一个心跳机制,客户端会周期性地向服务器发送PING请求,服务器也周期性地响应。
- 维持(Maintain):在Session Timeout时间内,只要客户端与服务器保持心跳,Session就会一直保持活跃状态。如果客户端连接的服务器宕机,Zookeeper客户端会自动尝试连接集群中的其他服务器,并重用当前的Session ID。
- 过期(Expire):如果在Session Timeout时间内,Zookeeper服务器没有收到客户端的任何心跳或请求,服务器就会认为客户端已经断开连接,服务器会触发Session过期,删除该Session创建的所有临时节点(包括临时有序节点),同客户端会收到SessionExpiredException异常。
- 关闭(Close):客户端主动调用close()方法关闭连接,Session会被关闭,同时服务器也会删除该Session创建的所有临时节点。
Watcher(观察者)是Zookeeper实现事件通知机制的核心,它允许客户端订阅Znode的变化,并在变化发生时收到异步通知。Watcher的触发事件类型主要由Watcher.Event.EventType枚举定义:
- 数据变化事件(Data Changed Events):
- NodeDataChanged:Znode的数据内容发生变化。
- NodeDeleted:Znode被删除。
- 子节点变化事件(Child Changed Events):
- NodeChildrenChanged:Znode的子节点列表发生变化(增加或删除子节点)。
- 节点创建事件(Node Created Events):
- NodeCreated:Znode被创建。(通常通过exists()方法注册Watcher来监听。)
- 连接状态变化事件(Connection State Events):
- None:这是一个特殊事件类型,通常用于表示连接状态的变化,如SyncConnected(成功连接)、Disconnected(连接断开)、Expired(Session过期)。
Watcher工作流程可以概括为以下三个过程:客户端注册 Watcher、服务端处理 Watcher 注册与事件触发、以及客户端回调 Watcher:
- 客户端注册 Watcher
- 随读取操作注册: 客户端不能随意注册 Watcher,必须伴随 getData()、getChildren() 或 exists() 等读取操作进行注册。
- 本地存储: 当客户端发起带有 Watch 请求的读取操作时,它会做两件事:向服务器发送带有 Watch 标记的请求,同时客户端会将本地的 Watcher 对象存储在一个名为 ZKWatchManager (或类似的结构) 的本地管理器中,等待服务端的事件通知。
- 服务端处理 Watcher 与事件触发
- 服务器存储: 服务端(通常是 Leader 节点,或由 Leader 转发)接收到客户端的注册请求后,会将该客户端的会话信息和它要监听的 ZNode 路径存储在服务端的 DataTree 结构中,具体存储在 dataWatches(数据变化)或 childWatches(子节点变化)列表中。
- 事件触发: 当被监听的 ZNode 发生改变(数据更新、节点创建、删除、子节点变更等)时,服务端会触发相应的 Watcher 事件。
- 异步通知: 服务端会异步地向注册了 Watcher 的客户端发送一个通知包 (packet),包含事件类型、状态等信息。
- 客户端回调 Watcher
- 接收通知: 客户端与服务端保持着长连接。当客户端收到服务端的事件通知后,客户端的后台线程会将该事件放入一个等待处理的队列中。
- 查找并执行: 客户端的主处理线程会从 ZKWatchManager 中取出对应的本地 Watcher 对象,执行其定义的回调逻辑 (process(WatchedEvent event) 方法)。
- 一次性触发: 默认情况下,Watcher 是一次性触发的(”one-time trigger”)。一旦 Watcher 被触发并发送通知给客户端,该 Watcher 在服务端的注册就会被移除。客户端如果想继续监听该 ZNode,必须在收到通知后重新注册 Watcher。
Watcher特性:
- 一次性触发:Watcher一旦被触发,就会从服务器的WatchManager中移除。如果需要持续监听,客户端必须重新注册。
- 异步通知:事件通知是异步发送给客户端的,不能保证立即到达。
- 轻量级:Zookeeper的Watcher机制是事件驱动的,只在Znode发生变化时才通知,避免了客户端频繁轮询的开销。
- 一致性:Zookeeper保证Watcher事件的顺序性,即事件的发送顺序与Znode的实际变化顺序一致。
- 可能丢失事件:在极少数情况下(如网络分区、客户端长时间断开连接),客户端可能会错过某些事件。因此,客户端在收到事件通知后,通常会再次读取最新的数据以确保数据是最新的。
Watcher 机制的实现完全依赖Session:
生命周期绑定:Session 结束,Watcher 失效
- 会话是前提: 所有 Watcher 的注册都是基于一个活动的客户端会话 (Session) 进行的。当客户端连接到 ZooKeeper 服务时,会建立一个会话。
- 失效机制: 如果客户端因为网络问题、进程崩溃等原因导致心跳超时,ZooKeeper 服务端会判定该 Session 过期。
- Watcher 清理: 当 Session 过期时,服务端会自动清除该客户端会话注册的所有 Watcher。 客户端也会收到一个特殊的 SessionExpiredException 通知(本地触发),此时之前注册的所有 Watcher 都将失效。客户端必须重新建立连接、开启新会话,并重新注册 Watcher。
连接状态管理与通知:Session 机制负责维护客户端与服务端的连接状态。当连接状态发生变化时,客户端会收到相应的 Watcher 事件通知:
- SyncConnected: 成功建立连接并创建会话。
- Disconnected: 连接暂时断开(会话可能仍然有效,客户端会自动尝试重连)。
- Expired: 会话彻底过期。
这些连接相关的事件会通过客户端预设的默认 Watcher(在 ZK 客户端初始化时指定的 Watcher 实例)进行通知。
一次性触发机制的协同:Watcher 默认是“一次性触发”的。这个设计与 Session 管理协同工作,确保了通知的可靠性:
- Watcher 被触发后,服务端会立即将通知发送给客户端,并从自己的存储中移除该 Watcher 注册。
0 如果客户端在收到通知之前与服务器断开连接(Session 未过期),客户端重连后,仍然有机会收到之前未送达的通知(ZooKeeper 尽力保证在 Session 存活期内将通知送达)。 - 一旦收到通知,或者 Session 过期,Watcher 便失效了。
- Watcher 被触发后,服务端会立即将通知发送给客户端,并从自己的存储中移除该 Watcher 注册。
Session 是 Watcher 存在的容器和生命线。Watcher 依赖于一个活动的 Session 才能有效注册和接收通知。Session 机制提供了可靠的连接管理,而 Watcher 机制则利用这些连接状态来保证数据变更通知的可靠性和及时性。
Zookeeper一致性保证
ZooKeeper 通过一套精心设计的机制和协议来保证数据的一致性,这主要包括以下几个核心要素:Zab 协议、单领导者模型、多数派提交和版本控制。
ZAB 协议(ZooKeeper Atomic Broadcast)
Zab 协议是 ZooKeeper 保证数据一致性的核心,它是一种原子广播协议,确保了所有写操作在集群中以相同的顺序、原子性地被处理。Zab 协议主要包含两个阶段:
- 领导者选举:当 ZooKeeper 集群启动或 Leader 出现故障时,集群会进入选举阶段。
- 所有服务器都会进入 LOOKING 状态,并通过互相交换选票来选举新的 Leader。
- 选票中包含服务器的 ID 和它所见过的最新事务 ID(zxid)。zxid 最大的服务器更有可能成为 Leader。
- 当一台服务器获得了集群中大多数服务器(即法定人数,Quorum)的选票时,它就会成为新的 Leader。
- 原子广播:一旦 Leader 选举完成,系统就进入原子广播阶段,开始处理客户端的写请求。
- 所有写请求都由 Leader 处理。
- Leader 为每个写请求分配一个唯一的 zxid,并将其封装成一个“提议”(Proposal)广播给所有 Follower。
- Follower 接收到提议后,会将其写入自己的日志,并向 Leader 发送确认(ACK)。
- 当 Leader 收到多数 Follower 的确认后,就会向所有 Follower 发送“提交”(COMMIT)消息,指示它们应用该事务。
- 原子性:Zab 协议保证,一个事务要么被集群中的所有服务器应用,要么不被应用。如果 Leader 在提交前崩溃,新选举的 Leader 会接管未完成的事务,并完成提交。
- 顺序性:所有事务都严格按照 zxid 的顺序执行,确保了全局的有序性。
单领导者模型
在任何时刻,ZooKeeper 集群中都只有一个 Leader 服务器,负责处理所有客户端的写请求。
- 集中式写入:这种模型避免了分布式环境下多点写入可能导致的数据冲突,简化了数据同步逻辑。
- 读写分离:读请求可以由任何服务器处理(包括 Leader、Follower 和 Observer),大大提高了读操作的性能和吞吐量。
- 一致性保证:虽然读请求可以从 Follower 处读取,但 ZooKeeper 保证了“顺序一致性”。这意味着客户端看到的更新会以发送的顺序应用,且一个客户端总能看到自己之前的写操作。
多数派提交(Quorum)
ZooKeeper 的多数派提交机制是其容错性的核心。
- 一个写操作必须得到集群中过半服务器(即 Quorum)的认可才能被提交。
- 容错性:在一个包含 (2n+1) 台服务器的集群中,只要有 (n+1) 台服务器正常工作,系统就可以正常提供服务。这意味着 ZooKeeper 可以容忍最多 (n) 台服务器的故障。
- 数据持久化:Leader 在收到多数派确认后,会将事务写入本地的预写日志(Write-Ahead Log, WAL),然后才发送提交命令。这确保了即使所有服务器都宕机,重启后也能恢复到最后一次成功提交的状态。
版本控制(Version)
ZooKeeper 的每个 ZNode 都维护一个 Stat 结构,其中包含了三个版本号:version(数据版本号)、cversion(子节点版本号)、aversion(ACL 版本号)。
- CAS 操作:客户端在进行 setData 或 delete 等修改操作时,可以指定期望的版本号。
- 乐观锁:如果服务器上的 ZNode 版本号与客户端提供的版本号不一致,说明数据已被其他客户端修改,操作就会失败。这有效避免了并发修改导致的数据覆盖问题。
一致性小结
- 单一入口:所有写操作都通过唯一的 Leader 服务器进行,确保了全局的写入顺序。
- 原子广播:Zab 协议确保所有写操作以原子、有序的方式广播到所有服务器。
- 多数派机制:写操作需要获得多数服务器的确认,保障了高可用性和数据的可靠性。
- 版本控制:乐观锁机制通过版本号,在客户端层面防止并发冲突。
- 持久化:预写日志保证了即使发生崩溃,数据也能从存储中恢复。
总结
- ZAB 协议:ZooKeeper 的灵魂。通过 ZAB,ZK 在“Leader 选举”(崩溃恢复)和“原子广播”(正常运行)两种模式间切换,保证了数据在集群中的一致性和顺序性。
- ZXID:实现 ZAB 协议的基础,保证了所有事务的全局顺序。
- Quorum (大多数) 机制:这是 ZK 实现高可用(容错)和高性能(不需要等待所有节点)之间的平衡点。无论是选举 Leader 还是提交事务,都只需要超过半数的节点同意。
- 临时节点 + Watcher:这是 ZK 实现分布式协调(如服务发现、分布式锁)的具体手段。通过 Session 管理临时节点的生命周期,通过 Watcher 机制异步通知数据变更。