Zookeeper的简介
ZooKeeper 是一个开源的分布式协调框架,它的定位是为分布式应用提供一致性服务,是整个大数据体系的管理员。
可以简单的认为 Zookeeper 就是文件系统和监听通知机制。
文件系统
Zookeeper维护一个类似文件系统的树状数据结构,这种特性使得 Zookeeper 不能用于存放大量的数据,每个节点的存放数据上限为 1M。每个子目录项如 NameService 都被称作为 znode(目录节点)。和文件系统一样,我们能够自由的增加、删除 znode,在一个 znode下增加、删除子 znode,唯一的不同在于 znode 是可以存储数据的。默认有四种类型的znode:
- 持久化目录节点 PERSISTENT:客户端与 Zookeeper 断开连接后,该节点依旧存在。
- 持久化顺序编号目录节点 PERSISTENT_SEQUENTIAL:客户端与 Zookeeper 断开连接后,该节点依旧存在,只是 Zookeeper 给该节点名称进行顺序编号。
- 临时目录节点 EPHEMERAL:客户端与 Zookeeper 断开连接后,该节点被删除。
- 临时顺序编号目录节点 EPHEMERAL_SEQUENTIAL:客户端与 Zookeeper 断开连接后,该节点被删除,只是 Zookeeper 给该节点名称进行顺序编号。
监听通知机制
Watcher 监听机制是 Zookeeper 中非常重要的特性,我们基于 Zookeeper 上创建的节点,可以对这些节点绑定监听事件,比如可以监听节点数据变更、节点删除、子节点状态变更等事件,通过这个事件机制,可以基于 Zookeeper 实现分布式锁、集群管理等功能。
Watcher 特性:
当数据发生变化的时候, Zookeeper 会产生一个 Watcher 事件,并且会发送到客户端。但是客户端只会收到一次通知。如果后续这个节点再次发生变化,那么之前设置 Watcher 的客户端不会再次收到消息。(Watcher 是一次性的操作)。可以通过循环监听去达到永久监听效果。
ZooKeeper 的 Watcher 机制,总的来说可以分为三个过程:
- 客户端注册 Watcher,注册 watcher 有 3 种方式,getData、exists、getChildren。
- 服务器处理 Watcher 。
- 客户端回调 Watcher 客户端。
监听流程:
- 首先要有一个 main() 线程
- 在 main 线程中创建 Zookeeper 客户端,这时就会创建两个线程,一个负责网络连接通信(connet),一个负责监听(listener)。
- 通过 connect 线程将注册的监听事件发送给 Zookeeper。
- 在 Zookeeper 的注册监听器列表中将注册的监听事件添加到列表中。
- Zookeeper 监听到有数据或路径变化,就会将这个消息发送给 listener 线程。
- listener线程内部调用了process()方法。
Zookeeper 特点
- 集群:Zookeeper是一个领导者(Leader),多个跟随者(Follower)组成的集群。
- 高可用性:集群中只要有半数以上节点存活,Zookeeper 集群就能正常服务。
- 全局数据一致:每个 Server 保存一份相同的数据副本,Client 无论连接到哪个 Server,数据都是一致的。
- 更新请求顺序进行:来自同一个 Client 的更新请求按其发送顺序依次执行。
- 数据更新原子性:一次数据更新要么成功,要么失败。
- 实时性:在一定时间范围内,Client 能读到最新数据。
- 从设计模式角度来看,zk是一个基于观察者设计模式的框架,它负责管理跟存储大家都关心的数据,然后接受观察者的注册,数据反生变化zk会通知在zk上注册的观察者做出反应。
- Zookeeper是一个分布式协调系统,满足CP性,跟SpringCloud中的Eureka满足AP不一样。
Zookeeper 提供的功能
通过对 Zookeeper 中丰富的数据节点进行交叉使用,配合 Watcher 事件通知机制,可以非常方便的构建一系列分布式应用中涉及的核心功能,比如数据发布/订阅、负载均衡、命名服务、分布式协调/通知、集群管理、Master 选举、分布式锁和分布式队列等功能。
- 数据发布/订阅
当某些数据由几个机器共享,且这些信息经常变化数据量还小的时候,这些数据就适合存储到ZK中。
- 数据存储:将数据存储到 Zookeeper 上的一个数据节点。
- 数据获取:应用在启动初始化节点从 Zookeeper 数据节点读取数据,并在该节点上注册一个数据变更 Watcher
- 数据变更:当变更数据时会更新 Zookeeper 对应节点数据,Zookeeper会将数据变更通知发到各客户端,客户端接到通知后重新读取变更后的数据即可。
- 分布式锁
基于ZooKeeper的分布式锁一般有排他锁和共享锁。
(1)排他锁(保持独占)
核心思想:在 zk 中有一个唯一的临时节点,只有拿到节点的才可以操作数据,没拿到的线程就需要等待。缺点:可能引发羊群效应,第一个用完后瞬间有999个同时并发的线程向 zk 请求获得锁。
(2)共享锁(控制时序)
主要是避免了羊群效应,临时节点已经预先存在,所有想要获得锁的线程在它下面创建临时顺序编号目录节点,编号最小的获得锁,用完删除,后面的依次排队获取。
- 负载均衡
多个相同的jar包在不同的服务器上开启相同的服务,可以通过 nginx 在服务端进行负载均衡的配置。也可以通过 ZooKeeper 在客户端进行负载均衡配置。
ZooKeeper 负载均衡和 Nginx 负载均衡区别:
- ZooKeeper 不存在单点问题,zab 协议保证单点故障可重新选举一个 leader 只负责服务的注册与发现,不负责转发,减少一次数据交换(消费方与服务方直接通信),需要自己实现相应的负载均衡算法。
- Nginx 存在单点问题,单点负载高数据量大,需要通过 KeepAlived + LVS 备机实现高可用。每次负载,都充当一次中间人转发角色,增加网络负载量(消费方与服务方间接通信),自带负载均衡算法。
- 命名服务
命名服务是指通过指定的名字来获取资源或者服务的地址,利用 zk 创建一个全局唯一的路径,这个路径就可以作为一个名字,指向集群中的集群,提供的服务的地址,或者一个远程的对象等等。
- 分布式协调/通知
对于系统调度来说,用户更改 zk 某个节点的 value, ZooKeeper 会将这些变化发送给注册了这个节点的 watcher 的所有客户端,进行通知。
对于执行情况汇报来说,每个工作进程都在目录下创建一个携带工作进度的临时节点,那么汇总的进程可以监控目录子节点的变化获得工作进度的实时的全局情况。
- 集群管理
大数据体系下的大部分集群服务好像都通过 ZooKeeper 管理的,其实管理的时候主要关注的就是机器的动态上下线跟Leader选举。
动态上下线:
比如在 zookeeper 服务器端有一个 znode 叫 /Configuration,那么集群中每一个机器启动的时候都去这个节点下创建一个 EPHEMERAL 类型的节点,比如 server1 创建 /Configuration /Server1,server2 创建 /Configuration /Server1,然后 Server1和 Server2 都 watch /Configuration 这个父节点,那么也就是这个父节点下数据或者子节点变化都会通知到该节点进行 watch 的客户端。
Leader选举:
- 利用 ZooKeeper 的强一致性,能够保证在分布式高并发情况下节点创建的全局唯一性,即:同时有多个客户端请求创建 /Master 节点,最终一定只有一个客户端请求能够创建成功。利用这个特性,就能很轻易的在分布式环境中进行集群选举了。
- 就是动态 Master 选举。这就要用到 EPHEMERAL_SEQUENTIAL 类型节点的特性了,这样每个节点会自动被编号。允许所有请求都能够创建成功,但是得有个创建顺序,每次选取序列号最小的那个机器作为 Master 。
Leader 选举
ZooKeeper集群节点个数一定是奇数个(因为偶数会浪费掉,原因是半数机制),一般3个或者5个就OK。
搭建的 Zookeeper 节点有以下四种状态。
- LOOKING:寻 找 Leader 状态。当服务器处于该状态时会认为当前集群中没有 Leader,因此需要进入 Leader 选举状态。
- FOLLOWING:跟随者状态。处理客户端的非事务请求,转发事务请求给 Leader 服务器,参与事务请求 Proposal(提议) 的投票,参与 Leader 选举投票。
- LEADING:领导者状态。事务请求的唯一调度和处理者,保证集群事务处理的顺序性,集群内部个服务器的调度者(管理follower,数据同步)。
- OBSERVING:观察者状态。3.0 版本以后引入的一个服务器角色,在不影响集群事务处理能力的基础上提升集群的非事务处理能力,处理客户端的非事务请求,转发事务请求给 Leader 服务器,不参与任何形式的投票。
同时在搭建 ZK 集群时会在 myid 文件中给每个节点搞个唯一编号,编号越大在 Leader 选择算法中的权重越大,比如初始化启动时就是根据服务器 ID 进行比较。
ZXID
ZooKeeper 采用全局递增的事务 Id 来标识,所有 proposal(提议)在被提出的时候加上了ZooKeeper Transaction Id ,zxid 是64位的 Long 类型,这是保证事务的顺序一致性的关键。zxid 中高32位表示 epoch,低32位表示事务标识xid。你可以认为 zxid 越大说明存储数据越新。
- 每个 leader 都会具有不同的 epoch 值,表示一个纪元/朝代,用来标识 leader 周期。每个新的选举开启时都会生成一个新的 epoch,新的 leader 产生的话 epoch 会自增,会将该值更新到所有的 zkServer 的 zxid 和 epoch,
- xid 是一个依次递增的事务编号。数值越大说明数据越新,所有 proposal(提议)在被提出的时候加上了 zxid,然后会依据数据库的两阶段过程,首先会向其他的 server 发出事务执行请求,如果超过半数的机器都能执行并且能够成功,那么就会开始执行。
Leader 选举
Leader的选举一般分为启动时选举跟Leader挂掉后的运行时选举。
启动时选举:
我们以上面的5台机器为例,只有超过半数以上,即最少启动3台服务器,集群才能正常工作。
- 服务器1启动,发起一次选举。
服务器1投自己一票。此时服务器1票数一票,不够半数以上(3票),选举无法完成,服务器1状态保持为LOOKING。
- 服务器2启动,再发起一次选举。
服务器1和2分别投自己一票,此时服务器1发现服务器2的id比自己大,更改选票投给服务器2。此时服务器1票数0票,服务器2票数2票,不够半数以上(3票),选举无法完成。服务器1,2状态保持LOOKING。
- 服务器3启动,发起一次选举。
与上面过程一样,服务器1和2先投自己一票,然后因为服务器3的 id 最大,两者更改选票投给为服务器3。此次投票结果:服务器1为0票,服务器2为0票,服务器3为3票。此时服务器3的票数已经超过半数(3票),服务器3当选Leader。服务器1,2更改状态FOLLOWING,服务器3更改状态为LEADING;
- 服务器4启动,发起一次选举。
此时服务器1、2、3已经不是LOOKING状态,不会更改选票信息,交换选票信息结果。服务器3为3票,服务器4为1票。此时服务器4服从多数,更改选票信息为服务器3,服务器4并更改状态为FOLLOWING。
- 服务器5启动,发起一次选举
同4一样投票给3,此时服务器3一共5票,服务器5为0票。服务器5并更改状态为FOLLOWING;
- 最终
Leader是服务器3,状态为LEADING。其余服务器是Follower,状态为FOLLOWING。
***运行时***选举
运行时候如果Master节点崩溃了会走恢复模式,新Leader选出前会暂停对外服务,大致可以分为四个阶段 选举、发现、同步、广播。
- 每个 Server 会发出一个投票,第一次都是投自己,其中投票信息 = (myid,ZXID)
- 收集来自各个服务器的投票
- 处理投票并重新投票,处理逻辑:优先比较ZXID,然后比较myid。
- 统计投票,只要超过半数的机器接收到同样的投票信息,就可以确定 leader,注意 epoch 的增加跟同步。
- 改变服务器状态 Looking 变为 Following 或 Leading。
- 当 Follower 链接上 Leader 之后,Leader 服务器会根据自己服务器上最后被提交的 ZXID 和 Follower 上的 ZXID 进行比对,比对结果要么回滚,要么和 Leader 同步,保证集群中各个节点的事务一致。
- 集群恢复到广播模式,开始接受客户端的写请求。
参考资料:
倪超.从Paxos到Zookeeper分布式一致性原理与实践[M].电子工业出版社.
https://blog.csdn.net/java_66666/article/details/81015302
https://blog.csdn.net/wx1528159409/article/details/84622762
https://mp.weixin.qq.com/s/TwV9Tk-2S-EXHEI9aT6iZA
其他系列文章: