共计 8268 个字符,预计需要花费 21 分钟才能阅读完成。
Zookeeper 概述
zookeeper 高容错数据一致性协议 (CP) 的分布式小文件系统,提供类似于文件系统的目录方式的数据存储。
- 全局数据一致性:每个 server 保存一份相同的数据副本,client 无论连接到哪个 server 展示的数据都是一致的。
- 可靠性:一旦事务成功提交,就会被保留下来。
- 有序性:客户端发起的事务请求,在也会顺序的应用在 Zookeeper 中。
- 数据更新原子性:一次数据更新要么成功要么失败,不存在中间状态。
- 实时性:保证客户端在一个间隔时间范围内获取服务的更新消息或服务器失效信息。
zookeeper 单机
数据模型:
每一个节点都是 znode(兼具文件和目录两种特点),每一个 znode 都具有原子操作。znode 存储的数据大小有限制(默认 1MB),通过绝对路径引用。
znode 分为 3 个部分:
- stat:状态信息, 描述 znode 的版本和权限等信息。
- data:与该 znode 关联的数据。
- children:该 znode 下的子节点。
znode 节点类型
- 临时节点:该节点的生命周期依赖于创建它的会话,一旦会话结束临时节点就会被删除。临时节点不允许拥有子节点。
- 永久节点:只能通过客户端显示执行删除操作。
- 临时序列化节点。
- 永久序列化节点。
znode 序列化:znode 的名字后面追加一个不断增加的序列号。每一个序列号对父节点来说是唯一的,可以记录每一个子节点的先后顺序。
znode 节点属性
zookeeper 的节点属性包括节点数据,状态,权限等信息。
属性 | 说明 |
---|---|
cZxid | znode 创建节点的事务 ID,Zookeeper 中每个变化都会产生一个全局唯一的 zxid。通过它可确定更新操作的先后顺序 |
ctime | 创建时间 |
mZxid | 修改节点的事务 ID |
mtime | 最后修改时间 |
pZxid | 子节点变更的事务 ID,添加子节点或删除子节点就会影响子节点列表,但是修改子节点的数据内容则不影响该 ID |
cversion | 子节点版本号,子节点每次修改版本号加 1 |
dataversion | 数据版本号,数据每次修改该版本号加 1,多个客户端对同一个 znode 进行更新操作时,因为数据版本号,才能保证更新操作的先后顺序性。例:客户端 A 正在对 znode 节点做更新操作,此时如果另一个客户端 B 同时更新了这个 znode,则 A 的版本号已经过期,那么 A 调用 setData 不会成功。 |
aclversion | 权限版本号,权限每次修改该版本号加 1 |
dataLength | 该节点的数据长度 |
numChildern | 该节点拥有的子节点的数量 |
znode ACL 权限控制
ACL 权限控制使用 schema:id:permission 来标识。
示例:setAcl /test2 ip:128.0.0.1:crwda
Schema
Schema 枚举值 | 说明 |
---|---|
world | 使用用户名设置,id 为一个用户,但这个 id 只有一个值:anyone,代表所有人 |
ip | 使用 IP 地址进行认证,ID 对应为一个 IP 地址或 IP 段 |
auth | 使用已添加认证的用户进行认证,以通过 addauth digest user:pwd 来添加当前上下文中的授权用户 |
digest | 使用“用户名: 密码”方式认证 |
permission
权限 | ACL 简写 | 描述 |
---|---|---|
CREATE | c | 可以创建子节点 |
DELETE | d | 可以删除子节点(仅下一级节点) |
READ | r | 可以读取节点数据及显示子节点列表 |
WRITE | w | 可以设置节点数据 |
ADMIN | a | 可以设置节点访问控制列表权限 |
权限相关命令
命令 | 使用方式 | 描述 |
---|---|---|
getAcl | getAcl <path> | 读取 ACL 权限 |
setAcl | setAcl <path> <acl> | 设置 ACL 权限 |
addauth | addauth <scheme> <auth> | 添加认证用户 |
zookeeper: 提供了分布式发布 / 订阅功能, 能让多个订阅者同时监听某一个主题对象, 通过 watche 机制来实现。
zookeeper watch 机制
一个 Watch 事件是一个一次性的触发器,当被设置了 Watch 的数据发生了改变的时候,则服务器将这个改变发送给设置了 Watch 的客户端,以便通知它们。
- 父节点的创建,修改,删除都会触发 Watcher 事件。
- 子节点的创建,删除会触发 Watcher 事件。
监听器 watch 特性
特性 | 说明 |
---|---|
一次性 | Watcher 是一次性的,一旦被触发就会移除,再次使用时需要重新注册。监听的客户端很多情况下,每次变动都要通知到所有的客户端,给网络和服务器造成很大压力。一次性可以减轻这种压力 |
客户端顺序回调 | 客户端 Watcher 回调的过程是一个串行同步的过程。 |
轻量 | Watcher 通知非常简单,只会告诉客户端发生了事件,而不会说明事件的具体内容。 |
监听器原理
- 首先要有一个 main()线程, 在 main 线程中创建 Zookeeper 客户端,这时就会创建两个线程,一个负责网络连接通信(connet),一个负责监听(listener)。
- 通过 connect 线程将注册的监听事件发送给 Zookeeper 服务端。
- 在 Zookeeper 服务端的注册监听器列表中将注册的监听事件添加到列表中。
- Zookeeper 监听到有数据或路径变化,就会将这个消息发送
给 listener 线程。
- listener 线程内部调用了 process()方法来触发 Watcher。
zookeeper 会话管理
- 客户端会不时地向所连接的 ZkServer 发送 ping 消息,ZkServer 接收到 ping 消息,或者任何其它消息的时候,都会将客户端的 session_id,session_timeout 记录在一个 map 中。
- Leader ZkServer 会周期性地向所有的 follower 发送心跳消息,follower 接收到 ping 消息后,会将记录 session 信息的 map 作为返回消息,返回给 leader,同时清空 follower 本地的 map。Leader 使用这些信息重新计算客户端的超时时间。
- 一旦在 session timout 的时间到,leader 即没有从其它 follower 上收集到客户端的 session 信息,也没有直接接收到该客户端的任何请求,那么该客户端的 session 就会被关闭。
zookeeper 数据模型
- zk 维护的数据主要有:客户端的会话(session)状态及数据节(dataNode)信息。
- zk 在内存中构造了个 DataTree 的数据结构,维护着 path 到 dataNode 的映射以及 dataNode 间的树状层级关系。为了提高读取性能,集群中每个服务节点都是将数据全量存储在内存中。所以,zk 最适于读多写少且轻量级数据的应用场景。
3. 数据仅存储在内存是很不安全的,zk 采用事务日志文件及快照文件的方案来落盘数据,保障数据在不丢失的情况下能快速恢复。
Zookeeper 集群
集群角色
- Leader:集群工作的核心,事务请求的唯一调度和处理者,保证事务处理的顺序性。对于有写操作的请求,需统一转发给 Leader 处理。Leader 需决定编号执行操作。
- Follower:处理客户端非事务请求,转发事务请求转发给 Leader,参与 Leader 选举。
- Observer 观察者:进行非事务请求的独立处理, 对于事务请求, 则转发给 Leader 服务器进行处理. 不参与投票。
事务
- 事务:ZooKeeper 中,能改变 ZooKeeper 服务器状态的操作称为事务操作。一般包括数据节点创建与删除、数据内容更新和客户端会话创建与失效等操作。对应每一个事务请求,ZooKeeper 都会为其分配一个全局唯一的事务 ID,用 ZXID 表示,通常是一个 64 位的数字。每一个 ZXID 对应一次更新操作,从这些 ZXID 中可以间接地识别出 ZooKeeper 处理这些事务操作请求的全局顺序。
- 事务日志:所有事务操作都是需要记录到日志文件中的,可通过 dataLogDir 配置文件目录,文件是以写入的第一条事务 zxid 为后缀,方便后续的定位查找。zk 会采取“磁盘空间预分配”的策略,来避免磁盘 Seek 频率,提升 zk 服务器对事务请求的影响能力。默认设置下,每次事务日志写入操作都会实时刷入磁盘,也可以设置成非实时(写到内存文件流,定时批量写入磁盘),但那样断电时会带来丢失数据的风险。事务日志记录的次数达到一定数量后,就会将内存数据库序列化一次,使其持久化保存到磁盘上,序列化后的文件称为 ” 快照文件 ”。有了事务日志和快照,就可以让任意节点恢复到任意时间点

- 数据快照:数据快照是 zk 数据存储中另一个非常核心的运行机制。数据快照用来记录 zk 服务器上某一时刻的全量内存数据内容,并将其写入到指定的磁盘文件中,可通过 dataDir 配置文件目录。可配置参数 snapCount,设置两次快照之间的事务操作个数,zk 节点记录完事务日志时,会统计判断是否需要做数据快照(距离上次快照,事务操作次数等于[snapCount/2~snapCount] 中的某个值时,会触发快照生成操作,随机值是为了避免所有节点同时生成快照,导致集群影响缓慢)。
过半原则
1. 过半:所谓“过半”是指大于集群机器数量的一半,即大于或等于(n/2+1),此处的“集群机器数量”不包括 observer 角色节点。leader 广播一个事务消息后,当收到半数以上的 ack 信息时,就认为集群中所有节点都收到了消息,然后 leader 就不需要再等待剩余节点的 ack,直接广播 commit 消息,提交事务。半数选举导致 zookeeper 通常由 2n+ 1 台 server 组成。
zookeeper 的两阶段提交:zookeeper 中,客户端会随机连接到 zookeeper 集群中的一个节点,如果是读请求,就直接从当前节点中读取数据。如果是写请求,那么请求会被转发给 leader 提交事务,然后 leader 会广播事务,只要有超过半数节点写入成功,那么写请求就会被提交。
- Leader 将写请求转化为一个 Proposal(提议),将其分发给集群中的所有 Follower 节点。
- Leader 等待所有的 Follower 节点的反馈,一旦超过半数 Follower 进行了正确的反馈,那么 Leader 就会再次向所有的 Follower 节点发送 Commit 消息,要求各个 Follower 节点对前面的一个 Proposal 节点进行提交。
- leader 节点将最新数据同步给 Observer 节点。
- 返回给客户端执行的结果。
ZAB 协议
ZooKeeper 能够保证数据一致性主要依赖于 ZAB 协议的 消息广播 , 崩溃恢复 和数据同步 三个过程。
消息广播
- 一个事务请求进来之后,Leader 节点会将写请求包装成提议(Proposal)事务,并添加一个全局唯一的 64 位递增事务 ID,Zxid。
- Leader 节点向集群中其他节点广播 Proposal 事务,Leader 节点和 Follower 节点是解耦的,通信都会经过一个 FIFO 的消息队列,Leader 会为每一个 Follower 节点分配一个单独的 FIFO 队列,然后把 Proposal 发送到队列中。
- Follower 节点收到对应的 Proposal 之后会把它持久到磁盘上,当完全写入之后,发一个 ACK 给 Leader。
- 当 Leader 节点收到超过半数 Follower 节点的 ACK 之后会提交本地机器上的事务,同时开始广播 commit,Follower 节点收到 commit 之后,完成各自的事务提交。
消息广播类似一个分布式事务的两阶段提交模式。在这种模式下,无法处理因 Leader 在发起事务请求后节点宕机带来的数据不一致问题。因此 ZAB 协议引入了崩溃恢复机制。
崩溃恢复
当整个集群在启动时,或者 Leader 失联后,ZAB 协议就会进入恢复模式,恢复模式的流程如下:
- 集群通过过民主举机制产生新的 Leader,纪元号加 1,开始新纪元
- 其他节点从新的 Leader 同步状态
- 过半节点完成状态同步,退出恢复模式,进入消息广播模式
Leader 选举流程
server 工作状态
状态 | 说明 |
---|---|
LOOKING | 竞选状态,当服务器处于该状态时,它会认为当前集群中没有 Leader,因此需要进入 Leader 选举状态。 |
FOLLOWING | 跟随者状态。表明当前服务器角色是 Follower。它负责从 Leader 同步状态,并参与选举投票。 |
LEADING | 领导者状态。表明当前服务器角色是 Leader。 |
OBSERVING | 观察者状态, 表明当前服务器角色是 Observer,它负责从同步 leader 状态,不参与投票。 |
选举原则
- 选举投票必须在同一轮次中进行,如果 Follower 服务选举轮次不同,不会采纳投票。
- 数据最新的节点优先成为 Leader,数据的新旧使用事务 ID 判定,事务 ID 越大认为节点数据约接近 Leader 的数据,自然应该成为 Leader。
- 如果每个个参与竞选节点事务 ID 一样,再使用 server.id 做比较。server.id 是节点在集群中唯一的 id,myid 文件中配置。
选举阶段
集群间互传的消息称为投票,投票 Vote 主要包括二个维度的信息:ID、ZXID

ID 候选者的服务器 ID
ZXID 候选者的事务 ID,从机器 DataTree 内存中获取,确保事务已经在机器上被 commit 过。
选主过程中主要有三个线程在工作
- 选举线程: 主动调用 lookForLeader 方法的线程,通过阻塞队 sendqueue 及 recvqueue 与其它两个线程协作。
- WorkerReceiver 线程: 选票接收器,不断获取其它服务器发来的选举消息,筛选后会保存到 recvqueue 队列中。zk 服务器启动时,开始正常工作,不停止
- WorkerSender 线程: 选票发送器,会不断地从 sendqueue 队列中获取待发送的选票,并广播至集群。
- WorkerReceiver 线程一直在工作,即使当前节点处于 LEADING 或者 FOLLOWING 状态,它起到了一个过滤的作用,当前节点为 LOOKING 时,才会将外部投票信息转交给选举线程处理;
- 如果当前节点处于非 LOOKING 状态,收到了处于 LOOKING 状态的节点投票数据(外部节点重启或网络抖动情况下),说明发起投票的节点数据跟集群不一致,这时,当前节点需要向集群广播出最新的内存 Vote(id,zxid),落后的节点收到该 Vote 后,会及时注册到 leader 上,并完成数据同步,跟上集群节奏,提供正常服务。
全新集群选举
- 每一机器都给自己一票。
- 主要服务器 ID 的值,值越大选举权重越大。
- 投票数过半,选举结束。
非全新集群选举
- 逻辑时钟:逻辑时钟小的选举结果被忽略
- 数据 ID:数据 ID 大的胜出
- 服务 ID:数据 ID 相同, 服务器 ID 大的胜出,被选举为 leader。
选举过程详细说明
Leader 选举是集群正常运行的前提,当集群启动或 Leader 失联后,就会进入 Leader 选举流程。
- 所有节点进入 LOOKING 状态
- 每个节点广播携带自身 ID 和 ZXID 的选票,投票推举自己为 Leader
- 节点接收其他节点发送的选票,把选票信息和自己推举的选票进行 PK(选票中 ZXID 大者胜出,ZXID 相同,则 ID 大者胜出)
- 如果外部选票获胜,则保存此选票信息,并把选票广播出去(赞成该选票)
- 循环上述 3 - 4 步骤
- 当有选票得到超过半数节点赞成,且该选票的所有者也赞成该选票,则选举成功,该选票所有者成为 Leader
- Leader 切换为 LEADING,Follower 切换为 FOLLOWING,Observer 切换为 OBSERVING 状态,选举结束,进入数据同步流程。
数据同步流程
数据同步流程,是要以 Leader 数据为基础,让集群数据达到一致状态。
- 新 Leader 把本地快照加载到内存,并通过日志应用快照之后的所有事务,确保 Leader 数据库是最新的。
- Follower 和 Observer 把自身的 ZXID 和 Leader 的 ZXID 进行比较,确定每个节点的同步策略
- 根据同步策略,Leader 把数据同步到各节点
- 每个节点同步结束后,Leader 向节点发送 NEWLEADER 指令
- 同步完成的 Follower 节点返回 ACK
- 当 Leader 收到过半节点反馈的 ACK 时,认为同步完成
Leader 向 Follower 节点发送 UPTODATE 指令,通知集群同步完成,开始对外服务。
zk 应用举例
- 命名服务:通过使用命名服务,客户端应用能够根据指定名字来获取资源或服务的地址,提供者等信息。通过创建全局唯一的 path 作为一个名字。
- 分布式锁:独占锁,获取数据之前要求所有的应用去 zk 集群的指定目录去创建一个临时非序列化的节点。谁创建成功谁就能获得锁,操作完成后断开节点。其它应用如果需要操作这个文件就可去监听这个目录是否存在。
- 控制时序:通过创建一个临时序列化节点来控制时序性。
- 心跳检测:让不同的进程都在 ZK 的一个指定节点下创建临时子节点,不同的进程直接可以根据这个临时子节点来判断对应的进程是否存活。大大减少了系统耦合。
- master 选举:每个客户端请求创建同一个临时节点,那么最终一定只有一个客户端请求能够创建成功。利用这个特性,就能很容易地在分布式环境中进行 Master 选举了。成功创建该节点的客户端所在的机器就成为了 Master。同时,其他没有成功创建该节点的客户端,都会在该节点上注册一个子节点变更的 Watcher,用于监控当前 Master 机器是否存活,一旦发现当前的 Master 挂了,那么其他客户端将会重新进行 Master 选举。
zookeeper 缺点:
1. 非高可用:极端情况下 zk 会丢弃一些请求: 机房之间连接出现故障。
- zookeeper master 就只能照顾一个机房,其他机房运行的业务模块由于没有 master 都只能停掉,对网络抖动非常敏感。
- 选举过程速度很慢且 zk 选举期间无法对外提供服务。
- zk 的性能有限: 典型的 zookeeper 的 tps 大概是一万多,无法覆盖系统内部每天动辄几十亿次的调用。因此每次请求都去 zookeeper 获取业务系统 master 信息是不可能的。因此 zookeeper 的 client 必须自己缓存业务系统的 master 地址。
- zk 本身的权限控制非常薄弱.
- 羊群效应: 所有的客户端都尝试对一个临时节点去加锁,当一个锁被占有的时候,其他的客户端都会监听这个临时节点。一旦锁被释放,Zookeeper 反向通知添加监听的客户端,然后大量的客户端都尝试去对同一个临时节点创建锁,最后也只有一个客户端能获得锁,但是大量的请求造成了很大的网络开销,加重了网络的负载,影响 Zookeeper 的性能.
 * 解决方法: 是获取锁时创建一个临时顺序节点,顺序最小的那个才能获取到锁,之后尝试加锁的客户端就监听自己的上一个顺序节点,当上一个顺序节点释放锁之后,自己尝试加锁,其余的客户端都对上一个临时顺序节点监听,不会一窝蜂的去尝试给同一个节点加锁导致羊群效应。
- zk 进行读取操作,读取到的数据可能是过期的旧数据,不是最新的数据。如果一个 zk 集群有 10000 台节点,当进行写入的时候,如果已经有 6K 个节点写入成功,zk 就认为本次写请求成功。但是这时候如果一个客户端读取的刚好是另外 4K 个节点的数据,那么读取到的就是旧的过期数据。
zookeeper 脑裂:
假死:由于心跳超时(网络原因导致的)认为 leader 死了,但其实 leader 还存活着。
脑裂
由于假死会发起新的 leader 选举,选举出一个新的 leader,但旧的 leader 网络又通了,导致出现了两个 leader,有的客户端连接到老的 leader,而有的客户端则连接到新的 leader。
quorum(半数机制)机制解决脑裂
在 zookeeper 中 Quorums 有 3 个作用:
- 集群中最少的节点数用来选举 leader 保证集群可用。
- 通知客户端数据已经安全保存前集群中最少数量的节点数已经保存了该数据。一旦这些节点保存了该数据,客户端将被通知已经安全保存了,可以继续其他任务。而集群中剩余的节点将会最终也保存了该数据。
- 假设某个 leader 假死,其余的 followers 选举出了一个新的 leader。这时,旧的 leader 复活并且仍然认为自己是 leader,这个时候它向其他 followers 发出写请求也是会被拒绝的。因为每当新 leader 产生时,会生成一个 epoch 标号(标识当前属于那个 leader 的统治时期),这个 epoch 是递增的,followers 如果确认了新的 leader 存在,知道其 epoch,就会拒绝 epoch 小于现任 leader epoch 的所有请求。那有没有 follower 不知道新的 leader 存在呢,有可能,但肯定不是大多数,否则新 leader 无法产生。Zookeeper 的写也遵循 quorum 机制,因此,得不到大多数支持的写是无效的,旧 leader 即使各种认为自己是 leader,依然没有什么作用。