ZooKeeper入门(包括CAP理论,2PC、3PC和Paxos算法等)
0. 前置知识
0.1 集群与分布式
很多人搞不清楚集群和分布式这两个概念,甚至认为两个概念说的是一件事。其实这两个概念很好区分和理解。
- 集群:将同一种服务多机器部署就形成了一个集群。
- 分布式:一个系统的服务分为多个子服务进行开发,将子服务部署在不同的机器上形成一个分布式系统。
简单举个例子就是,现在有一个博客服务,并发访问量巨大,单机系统根本顶不住,这是我加多几台服务器一样是提供博客服务,这就是集群。
但如果我将博客服务拆分为多个子服务,比如查看博客文章排行榜服务,博客网站内聊天服务等,然后将这些子服务都部署在不同的服务器上,这就是分布式。
0.2 什么是ZooKeeper

ZooKeeper 是由 Yahoo 开发,后来捐赠给了 Apache 的一个分布式协调服务框架。ZooKeeper 是开源的,它为分布式系统提供一致性服务。ZooKeeper 是通过基于 Paxos 算法的 ZAB 协议实现一致性。主要可以用来做分布式同步、配置维护、集群管理、分布式事务等。
1. ZooKeeper 重要概念
1.1 Data model (数据模型)
Zookeeper的数据模型是一个多叉树,根节点 key为“ / ”,每个结点都可以有多个子结点,每个结点其访问路径是唯一的,如“ / ”,“ /app1 ”,“ /app1/data01 ”分别表示根节点、根节点的某个子结点以及该结点的某个子结点。
1.2 znode (数据结点)
Zookeeper 的数据结点被称为 znode,是 Zookeeper 最小的数据单元。Zookeeper 的数据模型就是由一个个 znode 组成的多叉树。
1.2.1 znode 的 4 种类型
znode 通常被分为 4 个类型:
- 持久结点:一旦创建就永久存在,直到被删除。
- 临时结点:被创建之后如果当前客户端的会话结束,该结点消失。需要注意的是,临时结点只能作为叶子结点,不能创建子结点。
- 持久顺序结点:一旦创建永久存在,直到被删除。且其子结点的名称具有顺序性,这个顺序体现在 节点名称 上,在节点名称后自动添加一个由 10 位数字组成的数字串,从 0 开始计数。。
- 临时顺序结点:其生命周期与客户端会话绑定,会话结束该结点消失。且其子结点的名称具有顺序性。
1.2.2 znode 的数据结构
znode 的数据结构可分为两部分:
- data 部分:结点存放的数据
- stat 部分:存放结点状态信息的数据结构
[zk: localhost:2181(CONNECTED) 6] get /node1
// 下一行是 /node1 结点的数据
node1's data
[zk: localhost:2181(CONNECTED) 7] stat /node1
// 以下是 /node1 结点的 stat
cZxid = 0x4
ctime = Mon Sep 06 20:14:44 CST 2021
mZxid = 0x29
mtime = Wed Oct 27 17:27:20 CST 2021
pZxid = 0xf
cversion = 3
dataVersion = 2
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 12
numChildren = 1
znode 的状态信息表:
| znode 状态信息 | 解释 |
|---|---|
| cZxid | create ZXID,即该数据节点被创建时的事务 id |
| ctime | create time,即该节点的创建时间 |
| mZxid | modified ZXID,即该节点最终一次更新时的事务 id |
| mtime | modified time,即该节点最后一次的更新时间 |
| pZxid | 该节点的子节点列表最后一次修改时的事务 id,只有子节点列表变更才会更新 pZxid,子节点内容变更不会更新 |
| cversion | 子节点版本号,当前节点的子节点每次变化时值增加 1 |
| dataVersion | 数据节点内容版本号,节点创建时为 0,每更新一次节点内容(不管内容有无变化)该版本号的值增加 1 |
| aclVersion | 节点的 ACL 版本号,表示该节点 ACL 信息变更次数 |
| ephemeralOwner | 创建该临时节点的会话的 sessionId;如果当前节点为持久节点,则 ephemeralOwner=0 |
1.3 版本(version)
每个 znode 有三个不同意思的版本,Zookeeper 都会给每个 znode 维护一个 Stat 数据结构,Stat 中记录了这个 znode 的三个相关的版本:
- dataVersion:当前 znode 结点的版本号
- cversion:子结点的版本号,子结点每次变化时都 +1
- aclVersion:结点的 ACL 版本号,表示该结点的 ACL 信息变更次数
1.4 ACL(权限控制)
Zookeeper 的权限控制:
ACL 权限控制,使用 Schema : ID : Permission 来标识,Schema(权限控制方案,鉴权的策略)、ID(授权对象)、Permission(权限)。
- 对 znode 的操作的权限(5 种):
- CREATE:可以为当前结点创建子结点
- WRITE:可以设置 / 修改当前结点的数据
- READ:能获取当前结点数据以及列出子结点
- DELETE:能删除当前结点的子结点
- ADMIN:能设置当前结点的 ACL 权限
- 身份认证的方式(权限控制方案,鉴权的策略)(4 种):
- world:默认,只有一个用户 anyone,代表所有人
- auth:使用已添加认证的用户认证
- digest:使用用户名:密码认证方式,
username:password - ip:对指定 ip 进行权限控制
1.5 Watcher 事件监视器
Zookeeper 中,一个很重要的特性就是 Watcher(事件监视器)。它可以给 Zookeeper 中得节点注册一个 Watcher,当节点上发生了 Watcher 所监听的事件时,Watcher 会通知客户端,然后客户端根据通知调用对应的回调函数处理。这是 Zookeeper 可以提供分布式协调服务的重要特性。
1.6 会话(Session)
Zookeeper 的 Session 我们可以看作是一种客户端与服务端的 TCP 连接,通过 TCP 长连接 维持的会话机制,会话客户端可以对服务端进行心跳检测和保持连接,并且客户端可以对服务端发送请求和接收响应,以及接收来自服务端的发送的 Watcher 事件通知。
每个会话对应一个客户端与服务端的连接,在客户端创建会话之前,服务端会对每个客户端分配一个 sessionID,sessionID 唯一标识一个会话,是需要保证全局唯一性的。
其中会话(Session)还有一个属性 sessionTimeout ,sessionTimeout 指明了当当前客户端因服务端负担过重网络中断,服务端异常关闭,客户端重启等各种原因断开连接时,当前客户端的会话不会立即失效,而是等在 sessionTimeout 的时间内客户端仍未连接上集群中的任意一台服务器才失效。
ZooKeeper 的会话还有对应的事件,比如 CONNECTION_LOSS 连接丢失事件 、SESSION_MOVED 会话转移事件 、SESSION_EXPIRED 会话超时失效事件 。
2. Zookeeper 集群
一般使用 Zookeeper 我们需要保证高可用,都会使用集群部署 Zookeeper。只要集群中大部分机器可用,我们的 Zookeeper 服务就是可用的(有一定的故障容忍性)。集群间通过 ZAB 协议(Zookeeper Atomic Broadcast)来保证数据一致性。
典型的集群模式就是主备模式(Master/Slave 模式),通常主服务器提供写服务,从服务器从主服务器中通过异步复制的方法获取主服务器的最新数据提供读服务。
2.1 Zookeeper 集群的基本知识

2.1.1 Zookeeper 的三种角色:Leader,Follower 和 Observe
- Leader:所有机器通过 Leader 选举过程 来选定一台机器为 “ Leader ” ,Leader 既可以给客户端提供写服务也可以提供读服务,并负责投票的发起和决议,更新系统状态。(集群唯一的写请求处理者)
- Follower:只能提供读服务,收到写服务则转发给 Leader。参与 Leader 选举过程 和 写操作的 “ 过半写成功 ”策略 。
- Observe:只能提供读服务,收到写服务则转发给 Leader。
2.1.2 Leader 选举的四个阶段 (当 Leader 服务器崩溃,网络中断等进行选举,重新选举 Leader)
- Leader election(选举阶段):节点此时都在选举阶段,只要有一个节点超过半数投票,该结点成为准 Leader。
- Discovery(发现阶段):所有 followers 都与准 Leader 通信,同步 followers 最近收到的的事务提议。
- Synchronization(同步阶段):准 Leader 利用上一阶段收到的新的事务提议,同步集群中的所有副本。同步完成后成为真正的 Leader。
- Broadcast(广播阶段):此时集群正式对外提供事务服务,此时 Leader 可以广播。若有新节点加入集群,则同步新节点。
2.1.3 集群的服务器状态
- LOOKING:此时是寻找 Leader 的状态。
- LEADING:表示服务器的角色为 Leader。
- FOLLOWING:表示服务器角色为 Follower。
- OBSERVING:表示服务器为 Observe,不参与 Leader 选举。
2.2 Zookeeper 集群的问题
2.2.1 为什么 Zookeeper 集群节点建议为单数个?
Zookeeper 集群在宕掉一部分节点之后,只有剩下的节点数大于宕掉的节点数整个 Zookeeper 才是可用的。那么对于 2n 个节点的集群最多允许宕掉 n-1 个节点,而 2n-1 个节点的集群最多也是允许宕掉 n-1 个节点。在一样的故障容忍性下我们没必要多部署一台服务器。
那么为什么说 ZooKeeper 集群剩下可用节点数大于总节点数一半整个集群才可用呢?ZooKeeper 使用的数据一致性协议 ZAB 存在过半机制,当 Leader 宕机后,新 Leader 的选举需要过半的机器数的票选才能进行做出决议。
2.2.2 ZooKeeper 的过半机制如何防止脑裂?
集群脑裂是什么?
一般我们部署集群时都会部署在不同的机房提高集群可用性,防止例如一个机房网络故障导致整个集群不可用等情况。因此,当我们将集群部署在不同机房时,当机房间网络不通时,集群就被分为了多个小部分,每一小部分作为一个小集群。一个小集群会认为其他小集群都宕机而在小集群内发起 Leader 选举而导致产生多个 Leader,每个小集群单独对外提供服务将导致数据不一致等问题。
如何防止?
ZooKeeper 集群是通过过半机制防止脑裂,当不同机房网络不通,集群被分为多个小集群时,最多只有一个小集群能达到过半机制的要求选举出 Leader 对外提供服务。
3. 解决分布式数据的一致性问题
3.1 数据一致性问题
数据一致性是指,所有可用的节点上的数据是一致的。了解数据一致性问题,并尝试解决数据一致性问题前,我们需要了解一个重要的定理:CAP 定理。
3.1.1 CAP 定理
-
C、A、P 的含义
数据一致性 C:对于客户端的每次读操作,要么可以读到最新的数据,要么读取失败。意思是,分布式系统对客户端承诺:我给你要么返回错误,要么返回绝对一致的最新数据。我返回的数据一定是最新的。
系统可用性 A:对于任意客户端的请求都能得到响应数据,不会响应错误。意思是,分布式系统对客户端承诺:我一定给你返回数据,不会返回错误。即使数据不是最新的仍然返回。
分区容忍性 P:系统需要在系统的部分机器连接不上时仍能对外提供服务。意思是,对客户端承诺:即使我内部出现问题,也不会让你访问不了我提供的服务。
-
CAP 定理
一个分布式系统不可能同时满足 C、A、P 三个特性。
-
只能选择 CP 或 AP
对于一个分布式系统来说,P 是前提,是必要的。因为分布式系统内通过网络进行交互,必定是有网络延迟和数据丢失的,在系统内部分机器连接不上是我们的系统必须保证不能挂掉仍能提供服务。
接下来选择 C or P?这时一般我们会根据需求选择 C 或 P。
对数据一致性有要求的就选择 CP。对系统可用性有要求的就选择 AP。(ZooKeeper 是保证 CP 的,而 Eureka 保证 AP,Nacos 可以选择保证 CP/AP)
3.1.2 解决数据一致性问题的方案
我们知道 ZooKeeper 是保证 CP 的。那么 ZooKeeper 是如何实现保证数据一致性的呢?在了解 ZooKeeper 的解决方案之前我们不妨了解一下有哪些解决方案。
3.1.2.1 2PC(两阶段提交)
保证分布式系统数据一致性的协议,许多数据库都采用两阶段提交协议完成分布式事务的处理。
在分布式事务中,我们首先得解决整个调用链中(整个分布式事务中),我们所有服务的数据处理要么都成功要么都失败,也就是首先得解决分布式事务的原子性问题。
2PC:
涉及的角色:事务发起者,事务协调者和事务参与者。
-
第一阶段:要执行一个分布式事务时,事务发起者向协调者发起事务请求,协调者给所有参与者发送
prepare请求和事务内容(通知参与者执行事务,但不提交),参与者收到prepare请求后执行事务,同时将undo和redo信息写入事务日志,并将处理结果通知协调者。 -
第二阶段:协调者若收到所有参与者执行成功的消息,则发送
Commit请求通知所有参与者提交事务,参与者提交完毕后给协调者发送提交成功的响应。若第一阶段并不是所有参与者都准备好了,则发送rollback请求通知所有参与者回滚事务,参与者将处理结果返回给协调者,协调者收到响应后通知事务发起者事务处理失败。
2PC 的问题:
- 单点故障问题:协调者挂了,则整个系统都不可用了。
- 阻塞问题:当协调者发送 prepare 请求,参与者若能够处理请求则会执行事务但不提交(会占着资源),此时协调者挂了则这些资源不能释放,极大影响性能。
- 数据不一致问题:当协调者发送了一部分 Commit 请求之后,协调者宕机,则只有部分参与者提交事务,导致数据不一致。
3.1.2.2 3PC(三阶段提交)
3PC:
- CanCommit 阶段:事务发起者向协调者发起事务请求,此时进入 CanCommit 阶段,协调者发起 CanCommit 请求,若参与者可以执行事务则返回 YES 进入预备状态,否则返回 NO。
- PreCommit 阶段:协调者收到所有参与者的 YES 则发起 PreCommit 请求,参与者收到 PreCommit 请求则执行事务并将 undo 和 redo 写入事务日志,成功后响应 YES,否则响应 NO。但若协调者收到上一阶段的任一一个 NO 或者等待超时后任为收齐参与者的响应,则发起 abort 请求,参与者收到后立即中断事务或参与者等待超时仍未收到协调者的请求也会中断事务。
- DoCommit 阶段:协调者收到所有参与者的 YES 后发起 DoCommit 请求,参与者收到后提交事务最后响应事务提交结果。若收到任一参与者的 NO 或者一定时间未收到参与者响应,则发送 abort 请求,然后参与者中断提交并根据回滚日志进行事务回滚,并返回回滚情况。
问题:
- 数据不一致问题:比如在 PreCommit 阶段,部分参与者收到请求后其他参与者和协调者宕机,则这部分参与者最后会进行事务提交,出现数据不一致。
注:在 DoCommit 阶段,参与者在一定时间内未收到协调者的消息会自行提交事务。因为在 CanCommit 阶段所有参与者都回复了 YES,有理由相信所有参与者都执行了事务。
3.1.2.3 Paxos 算法
一致性算法,解决了分布式系统中如何就某个值(决议)达成一致。
涉及的角色:Proposer 提案者,Acceptor 表决者,Learner 学习者。
Paxos 算法:
-
prepare 阶段:
Proposer:提案者发起提案 proposal,每个提案者在发起提案时都会给提案分配一个全局唯一、单调递增的提案编号 N。提案者此时只将提案编号发送给所有决议者。
Acceptor:决议者每收到提案后,都将提案编号存在本地并维护一个最大的提案编号 maxN。每个决议者都只 accept 大于自身 maxN 的提案编号并将 maxN 响应给 Proposer(若返回 null 则表示批准该提案,返回 null 时表示收到的 N 为当前收到的最大的 N)。 -
accept 阶段:
当某个 Proposer 收到超过一半的批准,此时 Proposer 会给所有决议者发送真正的提案(包含提案编号和内容)。
决议者收到后比较本身批准过得最大提案编号和该提案编号,若收到的 N 大于等于 maxN 则 accept ——执行提案但不提交,并返回情况。
- 若收到超过半数的 accept 则给所有决议者发送提案的提交请求。给执行了提案内容的发送该提案的编号,让其提交。给未批准的acceptor 发送提案编号和内容,让其无条件执行和提交。
- 若未收到超过半数的 accept,则递增 Proposal 的编号重新进入 prepare 阶段。
Paxos 算法的死循环问题:
当两个 Proposer 依次发起 proposal,Acceptor 依次批准提案,则每次旧的提案都被新提案“覆盖”,则所有提案都不能不能进入 accept 阶段,此时进入了死循环。
如何解决死循环问题?只允许有一个 Proposer 即可解决。而 ZooKeeper 使用的一致性协议 ZAB 协议,就是基于 Paxos 算法定制的——只允许存在一个 Leader(类似 Proposer)。
3.2 ZooKeeper 如何保证数据一致性
ZooKeeper 基于 Paxos 算法定制了自己的一致性协议——ZAB 协议。ZAB 协议中有三个重要角色:Leader、Follower 和 Obserber。ZAB 协议对 Paxos 进行了定制,只允许存在一个 Leader(只有一个能发起提案,解决了 Paxos 算法的死循环问题)。
3.2.1 ZAB 协议
ZooKeeper 的分布式数据的一致性问题是通过 ZAB 协议 解决的。
ZAB 协议包括两个基本模式:消息广播模式和崩溃恢复模式。
3.2.2.1 消息广播模式
消息广播模式,其实就是在有写请求时 ZAB 协议是如何处理的。因为上面说到只有 Leader 能处理写请求,那么 Leader 在收到写请求时是如何处理的呢?
- 消息广播模式:
Leader 收到写请求后,首先必然是将写请求广播出去——发起提案(也就是将写请求的提案发送给所有 Follower),Follower 收到之后返回自身是否同意,若超过半数的 Follower 同意就可以给 Follower 和 Observer 发送 Commit 请求让它们进行写更新啦!

但是这里有个问题,如何保证 zkServer 能以正确地顺序接收消息呢?
首先就是要保证消息是有顺序地依次发送的:可以看到上图中有两类队列:FollowerQueues 和 ObserverQueues。其实在 Leader 中给每个 zkServer(Follower 和 Observer)都准备了一个队列,ZAB 协议通过 TCP 进行网络通信,可以保证顺序性地给对应的 zkServer 发送消息以及让 zkServer 有顺序性地接收消息。
除此之外,ZAB 还定义了全局单调递增的事务 ID ZXID,ZXID 是一个 64 位 long 型,其中高 32 位是 epoch 表示年代,低 32 位是事务 ID。epoch 是由 Leader 决定的,每次更换 Leader 都会更新 epoch,而低 32 位的就是单调递增的 id。每个 proposal 在 Leader 中产生之后会通过其 ZXID 进行排序然后再进行处理。这也是保证顺序性。
3.2.2.2 崩溃恢复模式
提及崩溃恢复模式前,我们需要先了解 Leader 的选举算法帮助我们理解 ZAB 的崩溃恢复模式。
-
Leader 的选举算法: Leader 的选举需要分为两种情况:第一种是初始化选举,即在 ZooKeeper 服务框架启动时的选举;第二种是 Leader 服务器崩溃时的重新选举。
-
初始化选举:假设我们的集群有 3 台机器,这就意味着 Leader 需要 2 台或以上的机器同意(超过半数)。我们首先启动了 server1,它会首先投票给自己,投票内容是机器自身的 myid 和 ZXID 组成,这时候 myid=1,ZXID=0(因为初始化启动)则投票内容为 (1, 0)。因为 server1 此时只有一票,无法成为 Leader。此时集群处于 Looking 状态,接着,server2 启动,首先投票给自己并且将投票广播出去(server1 也会,只是当时并无其他机器),server1 收到 server2 的票后将与自己支持的票对比,更新自己的选择并将其广播出去(选择 ZXID 较大的票,若 ZXID 相同则选择 myid 较大的),也就是 server1 会更新自己的投票为 (2, 0) 并广播出去。server2 收到 (2, 0) 发现和自己支持的票一致则不做更新并接收,此时发现自己的票数为 2(超过半数)。最后 server2 当选 Leader。在 server3 启动后发现集群正处于正常状态,会将自身作为 Follower 加入集群。
-
重新选举:继续上面举的例子,若 server2 宕机,则重新选举。server1 和 server3 从 Following 到 Looking,初始投票都投给自己并将票广播出去,若 server1 为 (1, 57),server3 为 (3, 50),server1 收到 (3, 50) 发现对方做 Leader 没有自己合适则不改票,server3 收到 (1,57) 发现对方的 ZXID 较大于是改票为 (1, 57) 并将更新的票广播。server1 收到后发现自己所持票数已超过一半(一半即是 1.5)此时 server1 当选 Leader。集群状态变为正常状态。
注:这里再次解释为什么都说搭建 ZooKeeper 集群的节点个数要为奇数。继续上面的例子,3 个节点的集群要想服务可用最多只能容忍一台机器宕机,而 4 个节点仍然只能容忍一台机器宕机。所以 ZooKeeper 集群的节点个数都是奇数。
-
-
崩溃恢复:
简单点说崩溃恢复就是——集群中有机器挂了,如何在这时让整个集群仍保证数据一致性?
有机器挂了可以分为两种情况:Follower 挂了和 Leader 挂了。① Follower 挂只要没超过半数机器都不用担心,在 Leader 中有维护队列,所以不用担心后面的数据没接收到导致数据不一致。
② Leader 挂了比较麻烦:
- 如果 Leader 在给 Followers 发送 Commit 请求的中途宕机,会导致 zkServer 数据不一致(有些节点没收到 Commit 请求)。此时集群进入 Looking 状态,由于有些节点未 Commit 所以其节点数据不是最新的,但是其事务 ID: ZXID 也会较小,这导致其不会成为 Leader。拥有最新数据的节点成为 Leader 之后会同步数据。
- 如果 Leader 在自身提交了事务 T1 并在准备发送 Commit 请求给 Followers 时宕机(Leader 提交了,但其他节点未收到 Commit请求)。此时重新选举出 Leader 后,原 Leader 结点以 Follower 重新连接,此时该结点的 T1 事务会被丢弃。
4. ZooKeeper 实战
- 使用 Docker 安装 ZooKeeper:
docker run -d --name zookeeper -p 2181:2181 zookeeper:3.5.8 #使用Docker安装ZooKeeper
docker run -d --name zookeeper -p 2181:2181 zookeeper:3.5.8 #运行ZooKeeper
docker ps #查看ZooKeeper的ContainerID
docker exec -it ContainerID /bin/bash #进入ZooKeeper所在的容器中
cd bin
./zkCli.sh -server 127.0.0.1:2181 #将zkCli.sh连接在本机的2181端口,也就是连接ZooKeeper的服务
- ZooKeeper 命令:
help #查看常用命令
create #创建节点
set #更新节点
get #获取节点数据
get -s #获取节点数据和状态信息
ls #查看某目录的子节点
stat #查看节点状态
ls2 #查看节点的状态信息以及子节点列表
delete #删除节点
- ZooKeeper 的 Java 客户端——Curator:https://curator.apache.org/

浙公网安备 33010602011771号