zookeeper

88 阅读5分钟

数据结构

Zookeeper数据模型的结构与Unix文件系统很类似,整体上可看作是一棵树,每个节点称作一个ZNode。每个ZNode默认能存储1MB数据,每个ZNode都可以通过其路径唯一标识

三种角色

Leader

  1. 与learner保持心跳
  2. 数据同步

崩溃恢复时负责恢复数据以及同步数据到Learner

  1. 事务调度

在zookeeper中,客户端会随机连接到zookeeper集群中的一个节点,如果是读请求,就会直接从当前节点读取数据,如果是写请求(这里应该说是事务请求,也就是涉及事务操作的增删改),- 客户端发出写入数据请求给任意Follower。过程:

  • Follower把写入数据请求转发给Leader。

  • Leader采用二阶段提交方式,先发送Propose广播给Follower。

  • Follower接到Propose消息,写入日志成功后,返回ACK消息给Leader。

  • Leader接到半数以上ACK消息,返回成功给客户端,并且广播Commit请求给Follower

Flower

  1. 与leader保持心跳
  2. 处理客户端非事务请求,读数据、转发事务请求给leader服务器
  3. 参与事务的Proposal投票(需要半数以上服务器通过才能通知leader commit数据)。
  4. 参与选举

Observer

Observer 是 zookeeper3.3 开始引入,Observer 的工作原理与follower 角色基本一致,而它和 follower 角色唯一的不同在于 observer 不参与任何形式的投票,包括事物请求Proposal 的投票和 leader 选举的投票。observer服务器只提供非事物请求服务,存在的意义在于不影响集群事物处理能力的前提下提升集群非事物处理的能力。

写法:server.4=127.0.0.1:2888:3888:observer

选举机制

SID 服务器编号 和集群搭建时myid一致。

ZXID

事务id, 用来标识服务器状态的变更,Zookeeper上任何数据节点的变更都会导致zxid递增。在某一时刻,集群中的每台机器的ZXID值不一定完全一致,这和ZooKeeper服务器对于客户端“更新请求”的处理逻辑有关。

image.png 是一个长度为64位的数字,其中低32位是按照数字来递增,任何数据的变更都会导致低32位数字简单加1。高32位是leader周期编号,每当选举出一个新的Leader时,新的Leader就从本地事务日志中取出ZXID,然后解析出高32位的周期编号,进行加1,再将低32位的全部设置为0。这样就保证了每次选举新的Leader后,保证了ZXID的唯一性而且是保证递增的。

ZXID 由两部分组成:

  • 任期:完成本次选举后,直到下次选举前,由同一 Leader 负责协调写入;
  • 事务计数器:单调递增,每生效一次写入,计数器加一。

第一次启动时候,已启动SID最大且得票数超过一半的当选leader,5台服务器中的第三台,3台服务器中的第二台

非第一次启动,ZXID比较大的当选leader,其实也是先比较任期,在比较事务计数器,ZXID一致时SID比较大的当选leader

zk实现分布式锁

  • 事件监听(观察者模式):在读取数据时,我们可以同时对节点设置事件监听,当节点数据或结构变化时,zookeeper会通知客户端。当前zookeeper有如下四种事件:
    • 节点创建
    • 节点删除
    • 节点数据修改
    • 子节点变更

基于以上的一些zk的特性,我们很容易得出使用zk实现分布式锁的落地方案:

  1. 使用zk的临时节点和有序节点,每个线程获取锁就是在zk创建一个临时有序的节点,比如在/lock/目录下。
  2. 创建节点成功后,获取/lock目录下的所有临时节点,再判断当前线程创建的节点是否是所有的节点的序号最小的节点
  3. 如果当前线程创建的节点是所有节点序号最小的节点,则认为获取锁成功。
  4. 如果当前线程创建的节点不是所有节点序号最小的节点,则对节点序号的前一个节点添加一个事件监听。
    比如当前线程获取到的节点序号为/lock/003,然后所有的节点列表为[/lock/001,/lock/002,/lock/003],则对/lock/002这个节点添加一个事件监听器。

如果锁释放了,会唤醒下一个序号的节点,然后重新执行第3步,判断是否自己的节点序号是最小。比如/lock/001释放了,/lock/002监听到时间,此时节点集合为[/lock/002,/lock/003],则/lock/002为最小序号节点,获取到锁。

curator 框架

zk实现分布式锁和redis实现分布式对比

  • 实现难度上:Zookeeper > redis

zk由于Zk的ZNode天然具有锁的属性,所以直接上手撸的话,很简单。 Redis需要考虑太多异常场景,比如锁超时、锁的高可用等,实现难度较大。

  • 服务端性能:redis > Zookeeper

Zk需要一半的节点ACK,才算写入成功,吞吐量较低。如果频繁加锁、释放锁,服务端集群压力会很大。

  • 客户端性能:Zookeeper > redis

Zk由于有通知机制,获取锁的过程,添加一个监听器就可以了。避免了轮询,性能消耗较小。 Redis并没有通知机制,它只能使用类似CAS的轮询方式去争抢锁,较多空转,会对客户端造成压力。

  • 可靠性:Zookeeper > redis

这个就很明显了。Zookeeper就是为协调而生的,有严格的Zab协议控制数据的一致性,锁模型健壮。 Redis追求吞吐,可靠性上稍逊一筹。即使使用了Redlock,也无法保证100%的健壮性,但一般的应用不会遇到极端场景,所以也被常用。

为什么真正选型时大部分会选择redis,因为

  1. 这四种情况肯定优先考虑的是服务端性能
  2. redis集群基本公司都会有,而为了个分布式锁,引入臃肿的zk,成本太高