本文主要介绍 ZooKeeper 的基本数据模型、watcher机制、分布式一致性协议 ZAB 协议、选举机制。
基本数据模型
ZooKeeper 的基本数据模型是一个树形结构:
- 每一个节点都称之为 znode,它可以有子节点,也可以有数据;
- 每个节点分为临时节点和永久节点,临时节点在客户端断开后消失;
- 每个 zk 节点都有各自的版本号,可以通过命令行来显示节点信息;
- 每当节点数据发生变化,那么该节点的版本号会累加(乐观锁);
- 删除/修改过时的节点,版本号不匹配则会报错;
- 每个 zk 节点存储的数据不宜过大,几 kb 即可;
ZooKeeper 的作用体现:
- master 节点选举,主节点挂了之后,从节点就会接手工作,并且保证这个节点是唯一的,这也是所谓的首脑模式,从而保证我们的集群是高可用的;
- 统一配置文件管理,即只需要部署一台服务器,则可以把相同的配置文件同步更新到其他服务器,此操作在云计算中用的特别多(假设修改了 redis 统一配置);
- 发布与订阅,类似消息队列 MQ,发布者把数据存在 znode 上,订阅者会读取这个数据(watcher 机制);
- 提供分布式锁,分布式环境中不同进程之间争夺资源,类似于多线程中的锁;
- 集群管理,集群中保证数据的强一致性。
ZooKeeper session 的基本原理:
- 客户端与服务端之间的连接存在会话;
- 每个会话都会可以设置一个超时时间;
- 心跳结束,session 则过期;
- session 过期,则临时节点 znode 会被抛弃;
- 心跳机制:客户端向服务端的 ping 包请求;
watcher机制
- watcher 是针对每个节点操作的监督者;
- 当监控的某个 znode 发生了变化,则触发 watcher 事件;
- 在 zk 中的 watcher 是一次性的,触发后,当前的 watcher 事件立即被销毁(注意 zk 客户端 zkclient 和 curator 解决了这个问题,watcher 是可以反复注册的);
- 父节点、子节点 增删改查都能够触发其 watcher;
- 针对不同类型的操作,触发的 watcher 事件也不同:
- (子) 节点创建事件
- (子) 节点删除事件
- (子) 节点数据变化事件
watcher 使用场景 :统一资源配置、分布式锁依赖watch机制实现
ZAB协议
Zookeeper 如何保证数据的一致性?
一致性协议 ZAB 协议是为 Zookeeper 专门设计的一种支持崩溃恢复和原子广播协议。基于 ZAB 协议,Zookeeper 实现了一种主备模式的系统架构来保持集群中各个副本之间的数据一致性。
- 所有客户端写入数据都是写入到主进程(称为 Leader)中,然后,由 Leader 复制到备份进程(称为 Follower)中。从而保证数据一致性。
- 复制过程只需要 Follower 有一半以上返回 Ack 信息就可以执行提交,大大减小了同步阻塞。也提高了可用性。
下面开始重点介绍消息广播和崩溃恢复,整个 Zookeeper 就是在这两个模式之间切换。 简而言之,当 Leader 服务可以正常使用,就进入消息广播模式,当 Leader 不可用时,则进入崩溃恢复模式。
消息广播
ZAB 协议的消息广播过程使用的是一个原子广播协议,类似一个二阶段提交过程。对于客户端发送的写请求,全部由 Leader 接收,Leader 将请求封装成一个事务 Proposal,将其发送给所有 Follwer ,然后,根据所有 Follwer 的反馈,如果超过半数成功响应,则执行 commit 操作(先提交自己,再发送 commit 给所有 Follwer)。
基本上,整个广播流程分为三步:
- 将数据都复制到 Follwer 中
- 等待 Follwer 回应 Ack,最低超过半数即成功
- 当超过半数成功回应,则执行 commit ,同时提交自己
通过以上 3 个步骤,就能够保持集群之间数据的一致性。实际上,在 Leader 和 Follwer 之间还有一个消息队列,用来解耦他们之间的耦合,避免同步,实现异步解耦。
还有一些细节:
- Leader 在收到客户端请求之后,会将这个请求封装成一个事务,并给这个事务分配一个全局递增的唯一 ID,称为事务ID(ZXID),ZAB 兮协议需要保证事务的顺序,因此必须将每一个事务按照 ZXID 进行先后排序然后处理。
- 实际上,这是一种简化版本的 2PC,不能解决单点问题。等会我们会讲述 ZAB 如何解决单点问题(即 Leader 崩溃问题)。
崩溃恢复
消息广播过程中,Leader 崩溃怎么办?还能保证数据一致吗?如果 Leader 先本地提交了,然后 commit 请求没有发送出去,怎么办?
实际上,当 Leader 崩溃,即进入我们开头所说的崩溃恢复模式(崩溃即:Leader 失去与过半 Follwer 的联系)。下面来详细讲述。
假设1:Leader 在复制数据给所有 Follwer 之后崩溃,怎么办?
假设2:Leader 在收到 Ack 并提交了自己,同时发送了部分 commit 出去之后崩溃怎么办?
针对这些问题,ZAB 定义了 2 个原则:
- ZAB 协议确保那些已经在 Leader 提交的事务最终会被所有服务器提交。
- ZAB 协议确保丢弃那些只在 Leader 提出/复制,但没有提交的事务。
所以,ZAB 设计了下面这样一个选举算法:能够确保提交已经被 Leader 提交的事务,同时丢弃已经被跳过的事务。
针对这个要求,如果让 Leader 选举算法能够保证新选举出来的 Leader 服务器拥有集群总所有机器编号(即 ZXID 最大)的事务,那么就能够保证这个新选举出来的 Leader 一定具有所有已经提交的提案。
而且这么做有一个好处是:可以省去 Leader 服务器检查事务的提交和丢弃工作的这一步操作。
这样,我们刚刚假设的两个问题便能够解决。假设 1 最终会丢弃调用没有提交的数据,假设 2 最终会同步所有服务器的数据。这个时候,就引出了一个问题,如何同步?
数据同步
当崩溃恢复之后,需要在正式工作之前(接收客户端请求),Leader 服务器首先确认事务是否都已经被过半的 Follwer 提交了,即是否完成了数据同步。目的是为了保持数据一致。
当所有的 Follwer 服务器都成功同步之后,Leader 会将这些服务器加入到可用服务器列表中。
实际上,Leader 服务器处理或丢弃事务都是依赖着 ZXID 的,那么这个 ZXID 如何生成呢?
答:在 ZAB 协议的事务编号 ZXID 设计中,ZXID 是一个 64 位的数字,其中低 32 位可以看作是一个简单的递增的计数器,针对客户端的每一个事务请求,Leader 都会产生一个新的事务 Proposal 并对该计数器进行 + 1 操作。
而高 32 位则代表了 Leader 服务器上取出本地日志中最大事务 Proposal 的 ZXID,并从该 ZXID 中解析出对应的 epoch 值,然后再对这个值加一。
高 32 位代表了每代 Leader 的唯一性,低 32 代表了每代 Leader 中事务的唯一性。同时,也能让 Follwer 通过高 32 位识别不同的 Leader。简化了数据恢复流程。
基于这样的策略:当 Follower 链接上 Leader 之后,Leader 服务器会根据自己服务器上最后被提交的 ZXID 和 Follower 上的 ZXID 进行比对,比对结果要么回滚,要么和 Leader 同步。
选举过程
集群初次启动或者进入奔溃恢复之后需要进行领导者选举。
- 每个Server发出一个投票,集群刚启动时,所有服务器的zxid都为0。各服务器初始化后,都投票给自己,并将自己的一票存入自己的票箱
- 接收来自各个服务器的投票
- PK选票并更新选票
- 优先检查ZXID,ZXID大的服务器优先作为Leader服务器
- 如果ZXID相同的话,比较myid,myid较大的服务器作为Leader服务器
- 统计选票。统计所有选票,判断是否有过半机器接收到相同的投票信息
- 改变服务器状态。一旦确定Leader,每个服务器更新自己的状态
注释:myid:服务器ID,用来标识一台Zookeeper集群中的机器,每台机器不能重复,ZXID:事务ID
常见问题
zookeeper在消息广播阶段和奔溃恢复阶段对外提供服务吗?
- 奔溃恢复阶段,非事务性请求是提供服务的
- 消息广播阶段读写事务都提供服务,zookeeper是最终一致性