一、基础概念

官方解释:

ZooKeeper是面向分布式应用程序的分布式开源协调服务

它公开了一组简单的基元,分布式应用程序可以基于这些基元来实现更高级别的服务,以实现同步,配置维护以及组和命名。它被设计为易于编程,并且使用在文件系统熟悉的目录树结构之后的样式的数据模型。它运行在Java中,并具有Java和C的绑定。

协调服务是非常难以正确的。他们特别容易出现诸如竞赛状况和僵局等错误。ZooKeeper背后的动机是为分散的应用程序减轻从零开始实施协调服务的责任。

自己理解:

zookeeper就是个分布式文件系统,每当一个服务提供者部署后都要将自己的服务注册到zookeeper的某一路径上: /{service}/{version}/{ip:port}, 比如我们的HelloWorldService部署到两台机器,那么zookeeper上就会创建两条目录:分别为
/HelloWorldService/1.0.0/100.19.20.01:16888
/HelloWorldService/1.0.0/100.19.20.02:16888。

Zookeeper的默认端口: 2181

1.1 zookeeper产生背景:

项目从单体到分布式转变之后,将会产生多个节点之间协同的问题。如:
1.每天的定时任务由谁哪个节点来执行?如elatic-job
2.RPC调用时的服务如何做注册和发现?
3.如何保证并发请求的幂等?如分布式锁
4…

这些问题可以统一归纳为多节点协调问题,如果靠节点自身进行协调这是非常不可靠的,性能上也不可取。必须由一个独立的服务做协调工作,它必须可靠,而且保证性能。

1. zookeeper未使用前,如何发布自己的服务?

如何让别人使用我们的服务呢?有同学说很简单嘛,告诉使用者服务的IP以及端口就可以了啊。确实是这样,这里问题的关键在于是自动告知还是人肉告知。

人肉告知的方式:如果你发现你的服务一台机器不够,要再添加一台,这个时候就要告诉调用者我现在有两个ip了,你们要轮询调用来实现负载均衡;调用者咬咬牙改了,结果某天一台机器挂了,调用者发现服务有一半不可用,他又只能手动修改代码来删除挂掉那台机器的ip。现实生产环境当然不会使用人肉方式。

有没有一种方法能实现自动告知,即机器的增添、剔除对调用方透明,调用者不再需要写死服务提供方地址?当然可以,现如今zookeeper被广泛用于实现服务自动注册与发现功能!

使用zookeeper后,架构可演变成这样

这里写图片描述

1.2 ZooKeeper的应用场景

1.分布式锁这是雅虎研究员设计Zookeeper的初衷。利用Zookeeper的临时顺序节点,可以轻松实现分布式锁。

2.服务注册和发现利用Znode和Watcher,可以实现分布式服务的注册和发现。
比如:最著名的应用就是阿里的分布式RPC框架Dubbo,以及分布式配置中心disconf等。

3.共享配置和状态信息Redis的分布式解决方案Codis,就利用了Zookeeper来存放数据路由表和 codis-proxy 节点的元信息。同时 codis-config 发起的命令都会通过 ZooKeeper 同步到各个存活的 codis-proxy。

1.3 zk和eureka的区别

参考我的另一篇博文:
服务注册发现机制

1.4 zk的安装与配置

zk安装配置

参考:https://www.cnblogs.com/jimcsharp/p/8358271.html

1.安装位置

我一般的安装位置如下:

/Users/daiwei/Desktop/soft/apache-zookeeper-3.8.2

2.修改配置

上面提到,conf目录下提供了配置的样例zoo_sample.cfg,要将zk运行起来,需要将其名称修改为zoo.cfg。

打开zoo.cfg,可以看到默认的一些配置。

tickTime
时长单位为毫秒,为zk使用的基本时间度量单位。例如,1 * tickTime是客户端与zk服务端的心跳时间,2 * tickTime是客户端会话的超时时间。
tickTime的默认值为2000毫秒,更低的tickTime值可以更快地发现超时问题,但也会导致更高的网络流量(心跳消息)和更高的CPU使用率(会话的跟踪处理)。
clientPort
zk服务进程监听的TCP端口,默认情况下,服务端会监听2181端口。
dataDir
无默认配置,必须配置,用于配置存储快照文件的目录。如果没有配置dataLogDir,那么事务日志也会存储在此目录。

3.服务启动

在Windows环境下,直接双击zkServer.cmd即可。在Linux环境下,进入bin目录,执行命令

./zkServer.sh start

这个命令使得zk服务进程在后台进行。如果想在前台中运行以便查看服务器进程的输出日志,可以通过以下命令运行:

./zkServer.sh start-foreground

执行此命令,可以看到大量详细信息的输出,以便允许查看服务器发生了什么。

使用文本编辑器打开zkServer.cmd或者zkServer.sh文件,可以看到其会调用zkEnv.cmd或者zkEnv.sh脚本。zkEnv脚本的作用是设置zk运行的一些环境变量,例如配置文件的位置和名称等。

二、zk架构原理

2.1 集群架构和ZAB协议

Zookeeper Service集群是一主多从结构,其结构如下:

这里写图片描述

在更新数据时,首先更新到主节点(这里的节点是指服务器,不是Znode),再同步到从节点,要等从节点至少一半返回成功后,才更新完成。可见zk是牺牲了可用性而保证强一致性的。

在读取数据时,直接读取任意从节点。

为了保证主从节点的数据一致性,Zookeeper采用了ZAB协议,这种协议非常类似于一致性算法Paxos和Raft。

Raft算法可以参考:
漫画:什么是拜占庭将军问题? https://blog.csdn.net/bjweimengshu/article/details/80222416

2.1.1 ZAB 协议

Zab协议 的全称是 Zookeeper Atomic Broadcast (Zookeeper原子广播)

ZAB 协议是为分布式协调服务 ZooKeeper 专门设计的一种支持崩溃恢复的原子广播协议和实现主从同步需遵守的协议。

在学习ZAB之前,我们需要首先了解ZAB协议所定义的三种节点状态:

  • Looking :选举状态。
  • Following :Follower节点(从节点)所处的状态。
  • Leading :Leader节点(主节点)所处状态。

我们还需要知道最大ZXID的概念:

最大ZXID也就是节点本地的最新事务编号,包含epoch和计数两部分。epoch是纪元的意思,相当于Raft算法选主时候的term。

假如Zookeeper当前的主节点挂掉了,集群会进行崩溃恢复。ZAB的崩溃恢复分成三个阶段:

  1. Leader election

选举阶段,此时集群中的节点处于Looking状态。它们会各自向其他节点发起投票,投票当中包含自己的服务器ID和最新事务ID(ZXID)。

在这里插入图片描述
接下来,节点会用自身的ZXID和从其他节点接收到的ZXID做比较,如果发现别人家的ZXID比自己大,也就是数据比自己新,那么就重新发起投票,投票给目前已知最大的ZXID所属节点。

在这里插入图片描述
每次投票后,服务器都会统计投票数量,判断是否有某个节点得到半数以上的投票。如果存在这样的节点,该节点将会成为准Leader,状态变为Leading。其他节点的状态变为Following。

在这里插入图片描述

流程简介:

即选取最大的ZXID的节点。如果存在这样的节点,该节点将会成为准Leader,状态变为Leading。其他节点的状态变为Following。

  1. Discovery

发现阶段,用于在从节点中发现最新的ZXID和事务日志。

思考

1.既然Leader被选为主节点,已经是集群里数据最新的了,为什么还要从节点中寻找最新事务呢?

这是为了防止某些意外情况,比如因网络原因在上一阶段产生多个Leader的情况。

所以这一阶段,Leader集思广益,接收所有Follower发来各自的最新epoch值。
Leader从中选出最大的epoch,基于此值加1,生成新的epoch分发给各个Follower。
各个Follower收到全新的epoch后,返回ACK给Leader,带上各自最大的ZXID和历史事务日志。Leader选出最大的ZXID,如果比自己还大,则更新自身历史日志。

这是为了防止某些意外情况,比如因网络原因在上一阶段产生多个Leader的情况。

流程简介:

即再一次确认是否还有更大的ZXID的节点,并获取历史事务日志

  1. Synchronization

同步阶段,把Leader刚才收集得到的最新历史事务日志,同步给集群中所有的Follower。
只有当半数Follower同步成功,这个准Leader才能成为正式的Leader。

自此,故障恢复正式完成。

注意zk在zab协议恢复完成之前,zk集群是无法用的,即牺牲了可用性保证数据一致性。

2.2 ZK node存储模型

2.2.1 Zookeeper的数据模型

Zookeeper的数据模型是什么样子呢?它很像数据结构当中的树,也很像文件系统的目录。

在这里插入图片描述

与Linux文件系统不同的是,Linux文件系统有目录和文件的区别,而Zookeeper的数据节点称为ZNode,ZNode是Zookeeper中数据的最小单元,每个ZNode都可以保存数据,同时还可以挂载子节点,因此构成了一个层次化的命名空间,称为树。

Znode数据结构如下:

在这里插入图片描述

data:

Znode存储的数据信息。

ACL:

记录Znode的访问权限,即哪些人或哪些IP可以访问本节点。

stat:

包含Znode的各种元数据,比如事务ID、版本号、时间戳、大小等等。

child:

当前节点的子节点引用,类似于二叉树的左孩子右孩子。

这里需要注意一点,Zookeeper是为读多写少的场景所设计。Znode并不是用来存储大规模业务数据,而是用于存储少量的状态和配置信息,每个节点的数据最大不能超过1MB。

2.2.2 Znode节点四种类型

1、持久节点 (PERSISTENT)
默认的节点类型。创建节点的客户端与zookeeper断开连接后,该节点依旧存在 。

2、持久节点顺序节点(PERSISTENT_SEQUENTIAL)
所谓顺序节点,就是在创建节点时,Zookeeper根据创建的时间顺序给该节点名称进行编号:

3、临时节点(EPHEMERAL)
和持久节点相反,当创建节点的客户端与zookeeper断开连接后,临时节点会被删除

4、临时顺序节点(EPHEMERAL_SEQUENTIAL)
顾名思义,临时顺序节点结合和临时节点和顺序节点的特点:在创建节点时,Zookeeper根据创建的时间顺序给该节点名称进行编号;当创建节点的客户端与zookeeper断开连接后,临时节点会被删除。

2.2.3 Znode节点的基本操作

Zookeeper包含了哪些基本操作呢?这里列举出比较常用的API:

1.create

创建节点

2.delete

删除节点

3.exists

判断节点是否存在

4.getData

获得一个节点的数据

5.setData

设置一个节点的数据

6.getChildren

获取节点下的所有子节点

这其中,exists,getData,getChildren属于读操作。Zookeeper客户端在请求读操作的时候,可以选择是否设置Watch。

2.3 zk的watch机制

1. zk的watch机制是什么意思?

我们可以理解成是注册在特定Znode上的触发器,其设计思想则为观察者摸索
当这个Znode发生改变,也就是调用了create,delete,setData方法的时候,将会触发Znode上注册的对应事件,请求Watch的客户端会接收到异步通知。

一次性:一旦被触发就会移除,再次使用需要重新注册,因为每次变动都需要通知所有客户端,一次性可以减轻压力,3.6.0版本默认持久递归,可以触发多次。

轻量:只通知发生了事件,不会告知事件内容,减轻服务器和带宽压力。

具体交互过程如下:

1.客户端调用getData方法,watch参数是true。服务端接到请求,返回节点数据,并且在对应的哈希表里插入被Watch的Znode路径(key),以及Watcher列表(value)。
在这里插入图片描述

2.当被Watch的Znode已删除,服务端会查找哈希表,找到该Znode对应的所有Watcher,异步通知客户端,并且删除哈希表中对应的Key-Value。

在这里插入图片描述
Watcher 机制包括三个角色:客户端线程、客户端的 WatchManager 以及 ZooKeeper 服务器

  1. 客户端向 ZooKeeper 服务器注册一个 Watcher 监听,
  2. 把这个监听信息存储到客户端的 WatchManager 中.
  3. 当 ZooKeeper 中的节点发生变化时,会通知客户端,客户端会 在WatchManager中调用相应 Watcher 对象中的回调方法。watch回调是串行同步的

2. zk的watch机制实现原理

new ZooKeeper(String connectString, int sessionTimeout, Watcher watcher) 

这个Watcher将作为整个ZooKeeper会话期间的上下文,一直被保存在客户端ZKWatchManager的 defaultWatcher。
也可以动态添加watcher:getData(),exists,getChildren。

实现思想是分布式环境下的观察者模式:
通过客服端和服务端分别创建有观察者的信息列表。

客户端调用相应接口时,首先将对应的 Watch 事件放到本地的 ZKWatchManager 中进行管理。

服务端在接收到客户端的请求后根据请求类型判断是否含有 Watch 事件,并将对应事件放到 WatchManager 中进行管理。

在事件触发的时候服务端通过节点的路径信息查询相应的 Watch 事件通知给客户端,客户端在接收到通知后,首先查询本地的ZKWatchManager 获得对应的 Watch 信息处理回调操作。

这种设计不但实现了一个分布式环境下的观察者模式,而且通过将客户端和服务端各自处理 Watch 事件所需要的额外信息分别保存在两端,减少彼此通信的内容。大大提升了服务的处理性能

1.客户端实现过程:

1.标记该会话是一个带有Watch事件的请求

2.通过 DataWatchRegistration 类来保存 watcher 事件和节点的对应关系

3.客户端向服务器发送请求,将请求封装成一个 Packet 对象,并添加到一个等待发送队列outgoingQueue 中

4.调用负责处理队列outgoingQueue 的 SendThread 线程类中的 readResponse 方法接收服务端的回调,并在最后执行 finishPacket()方法将 Watch 注册到 ZKWatchManager,sendThread 通
过发送 path 路径和 watcher 为 true ,到 server 注册 watch 事件。

ZKWatchManager保存了 Map<String,Set> dataWatchers、Map<String,Set> existsWatchers、
Map<String,Set> childrenWatchers 三个集合,客户端会在 dataWatchers 中会添加一个 key 为path 路径的本地事件

2.服务端实现过程:

1.解析收到的请求是否带有Watch注册事件,通过FinalRequestProcessor 类中的 processRequest函数实现的。当 getDataRequest.getWatch() 值为 True 时,表明该请求需要进行 Watch 监控注册。

2.将对应的Watch事件存储到WatchManager,通过 zks.getZKDatabase().getData 函数实现,
WatchManger 该类中有 HashMap<String,HashSet> watchTable ,key为path , Watcher 是一个客户端网络连接封装,当节点变化时会通知对应的连接(连接通过心跳保持)

3.服务端触发过程:

1.调用 WatchManager中的方法触发数据变更事件

2.封装了一个具有会话状态、事件类型、数据节点 3 种属性的 WatchedEvent 对象。之后查询该节点注册的 Watch 事件,如果为空说明该节点没有注册过 Watch 事件。
如果存在 Watch 事件则添加到定义的 Wathcers 集合中,并在 WatchManager 管理中删除。

3.最后,通过调用 process 方法向客户端发送通知

4.客户端回调过程:

1.使用 SendThread.readResponse() 方法来统一处理服务端的响应

2.将收到的字节流反序列化转换成 WatcherEvent 对象。调用 eventThread.queueEvent( )方法将接收到的事件交给 EventThread 线程进行处理

3.从 ZKWatchManager 中查询注册过的客户端 Watch 信息。查询到后,会将其从ZKWatchManager 的管理中删除。
因此客户端的 Watcher 机制是一次性的,触发后就会被删除

4.将查询到的 Watcher 存储到 waitingEvents 队列中,调用 EventThread 类中的 run 方法循环取出在 waitingEvents 队列中等待的 Watcher 事件进行处理

2.4 zk的数据一致性原理

ZAB写入数据处于ZAB的Broadcast阶段,其原理使用的是分布式事务中2pc提交。

什么是Broadcast呢?简单来说,就是Zookeeper常规情况下更新数据的时候,由Leader广播到所有的Follower。其过程如下:

在这里插入图片描述

1.客户端发出写入数据请求给任意Follower。

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

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

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

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

可见,zk并不能保证严格的强一致性,但可以保证在同步阶段,所有从节点的数据是一致的。

2.5 zk脑裂

1.什么是脑裂

请参考我的另一篇博文

https://blog.csdn.net/sinat_34814635/article/details/129914369

2.Zookeeper集群中的脑裂场景

对于一个集群,想要提高这个集群的可用性,通常会采用多机房部署,比如现在有一个由6台zkServer所组成的一个集群,部署在了两个机房:

在这里插入图片描述
正常情况下,此集群只会有一个Leader,那么如果机房之间的网络断了之后,两个机房内的zkServer还是可以相互通信的,如果不考虑过半机制,那么就会出现每个机房内部都将选出一个Leader。

在这里插入图片描述
这就相当于原本一个集群,被分成了两个集群,出现了两个“大脑”,这就是脑裂。

对于这种情况,我们也可以看出来,原本应该是统一的一个集群对外提供服务的,现在变成了两个集群同时对外提供服务,如果过了一会,断了的网络突然联通了,那么此时就会出现问题了,两个集群刚刚都对外提供服务了,数据该怎么合并,数据冲突怎么解决等等问题。

刚刚在说明脑裂场景时,有一个前提条件就是没有考虑过半机制,所以实际上Zookeeper集群中是不会出现脑裂问题的,而不会出现的原因就跟过半机制有关。

3.脑裂产生原因

对于 Zookeeper 来说有一个很重要的问题,就是到底是根据一个什么样的情况来判断一个节点死亡 down 掉了。 在分布式系统中这些都是有监控者来判断的,但是监控者也很难判定其他的节点的状态,唯一一个可靠的途径就是心跳,Zookeeper 也是使用心跳来判断客户端是否仍然活着。

使用 ZooKeeper 来做 Leader HA 基本都是同样的方式:每个节点都尝试注册一个象征 leader 的临时节点,其他没有注册成功的则成为 follower,并且通过 watch 机制 (这里有介绍) 监控着leader所创建的临时节点,Zookeeper 通过内部心跳机制来确定 leader 的状态,一旦 leader 出现意外 Zookeeper 能很快获悉并且通知其他的 follower,其他 flower 在之后作出相关反应,这样就完成了一个切换,这种模式也是比较通用的模式,基本大部分都是这样实现的。

但是这里面有个很严重的问题,如果注意不到会导致短暂的时间内系统出现脑裂,因为心跳出现超时可能是 leader 挂了,但是也可能是 zookeeper 节点之间网络出现了问题,导致 leader 假死的情况,leader其实并未死掉,但是与 ZooKeeper 之间的网络出现问题导致Zookeeper 认为其挂掉了然后通知其他节点进行切换,这样 follower 中就有一个成为了 leader,但是原本的 leader 并未死掉,这时候 client 也获得 leader 切换的消息,但是仍然会有一些延时,zookeeper 需要通讯需要一个一个通知,这时候整个系统就很混乱,可能有一部分 client 已经通知到了连接到新的 leader 上去了,有的 client 仍然连接在老的 leader 上,如果同时有两个 client 需要对 leader 的同一个数据更新,并且刚好这两个 client 此刻分别连接在新老的 leader 上,就会出现很严重问题。

所以导致zk脑裂的主要有以下原因

假死:
由于心跳超时(网络原因导致的)认为 leader 死了,但其实 leader 还存活着。

重新选举

由于假死会发起新的 leader 选举,选举出一个新的 leader,但旧的 leader 网络又通了,导致出现了两个 leader ,有的客户端连接到老的 leader,而有的客户端则连接到新的 leader。

4.过半机制解决Zookeeper集群中的脑裂问题

在领导者选举的过程中,如果某台zkServer获得了超过半数的选票,则此zkServer就可以成为Leader了。

过半机制的源码实现其实非常简单:

public class QuorumMaj implements QuorumVerifier {
    private static final Logger LOG = LoggerFactory.getLogger(QuorumMaj.class);
    
    int half;
    
    // n表示集群中zkServer的个数(准确的说是参与者的个数,参与者不包括观察者节点)
    public QuorumMaj(int n){
        this.half = n/2;
    }
 
    // 验证是否符合过半机制
    public boolean containsQuorum(Set<Long> set){
        // half是在构造方法里赋值的
        // set.size()表示某台zkServer获得的票数
        return (set.size() > half);
    }
    
}

举个简单的例子: 如果现在集群中有5台zkServer,那么half=5/2=2,那么也就是说,领导者选举的过程中至少要有三台zkServer投了同一个zkServer,才会符合过半机制,才能选出来一个Leader。

那么有一个问题我们想一下,选举的过程中为什么一定要有一个过半机制验证?

因为这样不需要等待所有zkServer都投了同一个zkServer就可以选举出来一个Leader了,这样比较快,所以叫快速领导者选举算法呗。

那么再来想一个问题,过半机制中为什么是大于,而不是大于等于呢?

这就是跟脑裂问题有关系了,比如回到上文出现脑裂问题的场景:

在这里插入图片描述
当机房中间的网络断掉之后,机房1内的三台服务器会进行领导者选举,但是此时过半机制的条件是set.size() > 3,也就是说至少要4台zkServer才能选出来一个Leader,所以对于机房1来说它不能选出一个Leader,同样机房2也不能选出一个Leader,这种情况下整个集群当机房间的网络断掉后,整个集群将没有Leader。

而如果过半机制的条件是set.size() >= 3,那么机房1和机房2都会选出一个Leader,这样就出现了脑裂。

所以我们就知道了,为什么过半机制中是大于,而不是大于等于。就是为了防止脑裂。

如果假设我们现在只有5台机器,也部署在两个机房:

在这里插入图片描述
此时过半机制的条件是set.size() > 2,也就是至少要3台服务器才能选出一个Leader,此时机房件的网络断开了,对于机房1来说是没有影响的,Leader依然还是Leader,对于机房2来说是选不出来Leader的,此时整个集群中只有一个Leader。

所以,我们可以总结得出,有了过半机制,对于一个Zookeeper集群,要么没有Leader,要么只有1个Leader,这样就避免了脑裂问题。

参考:

关于ZooKeeper集群脑裂及其解决方案
https://blog.csdn.net/suifeng629/article/details/103167651

思考

1.Zookeeper集群节点为什么要部署成奇数?

1.过半机制防止脑裂问题的原因,如果为偶数可能导致选举不出leader

2.资源节约角度
zookeeper 容错指的是:当宕掉几个 zookeeper 节点服务器之后,剩下的个数必须大于宕掉的个数,也就是剩下的节点服务数必须大于 n/2,这样 zookeeper 集群才可以继续使用,无论奇偶数都可以选举 leader。例如5台 zookeeper 节点机器最多宕掉 2 台,还可以继续使用,因为剩下 3 台大于 5/2。那么为什么最好为奇数个节点呢?是在以最大容错服务器个数的条件下,会节省资源。比如,最大容错为 2 的情况下,对应的zookeeper 服务数,奇数为5,而偶数为6,也就是6个zookeeper 服务的情况下最多能宕掉2个服务,所以从节约资源的角度看,没必要部署6(偶数)个 zookeeper 服务节点。

zookeeper 集群有这样一个特性:集群中只要有过半的机器是正常工作的,那么整个集群对外就是可用的。也就是说如果有 2 个 zookeeper 节点,那么只要有1个zookeeper 节点死了,那么 zookeeper 服务就不能用了,因为1没有过半,所以2个 zookeeper 的死亡容忍度为0;同理,要是有3个 zookeeper,一个死了,还剩下2个正常的,过半了,所以3个 zookeeper 的容忍度为1;同理也可以多列举几个:2->0; 3->1; 4->1; 5->2; 6->2 就会发现一个规律,2n 和 2n-1 的容忍度是一样的,都是 n-1,所以为了更加高效,何必增加那一个不必要的 zookeeper 呢。

根据以上可以得出结论:从资源节省的角度来考虑,zookeeper 集群的节点最好要部署成奇数个!

三、zk客户端

客户端与一个单独的服务器建立连接。它们之间通过发送请求、获得回复,获得观察事件和发送心跳来维持一个TCP连接。如果客户端与服务器的TCP连接断开了,那么客户端会去连接另一个服务器。

3.1心跳检测机制

zookeeper提供了“心跳检测”功能,它会定时向各个服务提供者发送一个请求(实际上建立的是一个 Socket 长连接),如果长期没有响应,服务中心就认为该服务提供者已经“挂了”,并将其剔除,比如100.19.20.02这台机器如果宕机了,那么zookeeper上的路径就会只剩/HelloWorldService/1.0.0/100.19.20.01:16888。

自己理解:实质就是一个调度线程定时检查连接是否断开

3.2 ZkClient

ZkClient是Github上一个开源的Zookeeper客户端,是由Datameer的工程师Stefan Groschupf和Peter Voss一起开发的。ZkClient在Zookeeper原生API接口之上进行了包装,是一个更易用的Zookeeper客户端。

同时ZkClient在内部实现了诸如Session超时重连,Watcher反复注册等功能。使得Zookeeper客户端这些繁琐的细节工作对开发人员透明。

四、zk典型应用架构和原理

4.1 Zookeeper实现分布式锁

Zookeeper分布式锁主要应用了到了zk的临时顺序节点和watch机制特性

  1. 获取锁

首先,在Zookeeper当中创建一个持久节点ParentLock。当第一个客户端想要获得锁时,需要在ParentLock这个节点下面创建一个临时顺序节点 Lock1。
在这里插入图片描述
之后,Client1查找ParentLock下面所有的临时顺序节点并排序,判断自己所创建的节点Lock1是不是顺序最靠前的一个。如果是第一个节点,则成功获得锁
在这里插入图片描述

这时候,如果再有一个客户端 Client2 前来获取锁,则在ParentLock下再创建一个临时顺序节点Lock2。

在这里插入图片描述
Client2查找ParentLock下面所有的临时顺序节点并排序,判断自己所创建的节点Lock2是不是顺序最靠前的一个,结果发现节点Lock2并不是最小的。于是,Client2向排序仅比它靠前的节点Lock1注册Watcher,用于监听Lock1节点是否存在。这意味着Client2抢锁失败,进入了等待状态。

这时候,如果又有一个客户端Client3前来获取锁,则在ParentLock下再创建一个临时顺序节点Lock3。

在这里插入图片描述
Client3查找ParentLock下面所有的临时顺序节点并排序,判断自己所创建的节点Lock3是不是顺序最靠前的一个,结果同样发现节点Lock3并不是最小的。

于是,Client3向排序仅比它靠前的节点Lock2注册Watcher,用于监听Lock2节点是否存在。这意味着Client3同样抢锁失败,进入了等待状态。

在这里插入图片描述
这样一来,Client1得到了锁,Client2监听了Lock1,Client3监听了Lock2。这恰恰形成了一个等待队列,很像是Java当中ReentrantLock所依赖的AQS(AbstractQueuedSynchronizer)。

  1. 释放锁

释放锁分为两种情况:

1.任务完成,客户端显示释放
当任务完成时,Client1会显示调用删除节点Lock1的指令。

在这里插入图片描述

2.任务执行过程中,客户端崩溃

获得锁的Client1在任务执行过程中,如果Duang的一声崩溃,则会断开与Zookeeper服务端的链接。根据临时节点的特性,相关联的节点Lock1会随之自动删除。

在这里插入图片描述
由于Client2一直监听着Lock1的存在状态,当Lock1节点被删除,Client2会立刻收到通知。这时候Client2会再次查询ParentLock下面的所有节点,确认自己创建的节点Lock2是不是目前最小的节点。如果是最小,则Client2顺理成章获得了锁。

在这里插入图片描述
同理,如果Client2也因为任务完成或者节点崩溃而删除了节点Lock2,那么Client3就会接到通知。

在这里插入图片描述
最终,Client3成功得到了锁。

在这里插入图片描述

在Apache的开源框架 Apache Curator 中,包含了对Zookeeper分布式锁的实现,有兴趣的小伙伴可以看看源码:
https://github.com/apache/curator/

4.2 Dubbo Zookeeper注册中心

Dubbo Zookeeper架构
在这里插入图片描述

流程说明:

  • 服务提供者启动时: 向 /dubbo/com.foo.BarService/providers 目录下写入自己的 URL 地址
  • 服务消费者启动时: 订阅 /dubbo/com.foo.BarService/providers 目录下的提供者 URL 地址。并向/dubbo/com.foo.BarService/consumers 目录下写入自己的 URL 地址
  • 监控中心启动时: 订阅
    /dubbo/com.foo.BarService 目录下的所有提供者和消费者 URL 地址。

支持以下功能:

  • 当提供者出现断电等异常停机时,注册中心能自动删除提供者信息
  • 当注册中心重启时,能自动恢复注册数据,以及订阅请求
  • 当会话过期时,能自动恢复注册数据,以及订阅请求
  • 当设置 <dubbo:registry check=“false” /> 时,记录失败注册和订阅请求,后台定时重试
  • 可通过 <dubbo:registry username=“admin” password=“1234” /> 设置 zookeeper 登录信息
  • 可通过 <dubbo:registry group=“dubbo” /> 设置 zookeeper 的根节点,不配置将使用默认的根节点。
  • 支持 * 号通配符 <dubbo:reference group=“" version="” />,可订阅服务的所有分组和所有版本的提供者

以下为捕获的一条dubbo调用链路信息:

com.alibaba.dubbo.rpc.RpcException: 
Failed to invoke the method findByUserIdAndSystemCode in the service com.haixue.ucenter.api.service.UcenterUserService. Tried 1 times of the providers [192.168.16.217:23001] (1/2) from the registry test0.zookeeper-common.node1.highso.local:2181 on the consumer 192.168.62.186 using the dubbo version 2.6.2. 
Last error is: Invoke remote method timeout. method: findByUserIdAndSystemCode, provider: dubbo://192.168.16.217:23001/com.haixue.ucenter.api.service.UcenterUserService?anyhost=true&application=haixue-cms-mgt&check=false&default.retries=0&default.service.filter=haixueProviderLogFilter&default.timeout=2000&disabled=false&dubbo=2.6.2&generic=false&group=ucenter&interface=com.haixue.ucenter.api.service.UcenterUserService&logger=slf4j&methods=findById,findByUserIdAndSystemCode&owner=null&pid=1428&register.ip=192.168.62.186&remote.timestamp=1613714524362&revision=2.0.10&serverApplicationName=ucenter&side=consumer&timestamp=1614572806237&version=1.0.0,
cause: Waiting server-side response timeout. start time: 2021-03-01 13:53:56.505, end time: 2021-03-01 13:53:58.512, client elapsed: 0 ms, server elapsed: 2006 ms, timeout: 2000 ms, request: Request [id=1659, version=2.0.0, twoway=true, event=false, broken=false, data=RpcInvocation [methodName=findByUserIdAndSystemCode, parameterTypes=[class java.lang.Long, class java.lang.String], arguments=[121073, hx_cms], attachments={path=com.haixue.ucenter.api.service.UcenterUserService, dubboApplication=haixue-cms-mgt, application=haixue-cms-mgt, _hx_trace_id_=2801467306113236992, interface=com.haixue.ucenter.api.service.UcenterUserService, _consumer_service_name_=haixue-cms-mgt, version=1.0.0, timeout=2000, group=ucenter}]], channel: /192.168.62.186:52799 -> /192.168.16.217:23001

参考资料

1.ZooKeeper官方文档:http://zookeeper.apache.org/doc/current/zookeeperOver.html

2.漫画:什么是ZooKeeper https://blog.csdn.net/moakun/article/details/80389776

3.漫画:如何用Zookeeper实现分布式锁?http://www.ibloger.net/article/3206.html

4.漫画:什么是拜占庭将军问题? https://blog.csdn.net/bjweimengshu/article/details/80222416

5.dubbo官方文档:
https://dubbo.apache.org/zh/docs/v2.7/user/references/registry/zookeeper/

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐