Java分布式注册中心之Zookeeper

Zookeeper是什么

ZooKeeper是一个用于维护配置信息、命名、提供分布式同步和提供组服务的集中式服务,它常作为一个注册中心服务用于分布式项目。

Zookeeper拥有以下几个重要特性

顺序一致性:来自客户端的相关指令会按照顺序执行,不会出现乱序的情况,客户端发送到服务的指令1->2->3->4,那个这些指令就会按照顺序执行;

原子性:更新只有成功和失败,没有中间状态;

可靠性:也可以称之为持久性,节点更新以后,在下次更新之前,它的数据不会发生变更;

准实时性:也可以称之为最终一致性,在zk集群中,一个客户端修改了其中的一个节点,一定时间以后,所有可用的服务对应的节点都会变成更新以后的值。

Zookeeper如何保证数据一致性

Zookeeper的数据一致性是通过ZAP(原子广播)协议来保证的且zk的数据一致性为最终一致性,意思是zk只保证数据在事务前后是一致的并不能保证在过程中全程一致(这是因为zk的数据同步是一个二阶段提交的过程,后面会讲zk数据同步过程)。

整个ZAB协议一共定义了三个阶段

1) 发现:要求zookeeper集群必须选举出一个 Leader 进程,同时 Leader 会维护一个 Follower 可用客户端列表。将来客户端可以和这些 Follower节点进行通信。

2) 同步:Leader 要负责将本身的数据与 Follower 完成同步,做到多副本存储。这样也是体现了CAP中的一致性(C)和分区容错(P)。Follower将队列中未处理完的请求消费完成后,写入本地事务日志中。
3) 广播:Leader 可以接受客户端新的事务Proposal请求,将新的Proposal请求广播给所有的 Follower。

补充:CAP原则

CAP原则又称CAP定理,指的是在一个分布式系统中,一致性(Consistency)、可用性(Availability)、分区容错性(Partition tolerance)。CAP 原则指的是,这三个要素最多只能同时实现两点,不可能三者兼顾。

一致性(C):在分布式系统中的所有数据备份,在同一时刻是否同样的值。(等同于所有节点访问同一份最新的数据副本)
可用性(A):保证每个请求不管成功或者失败都有响应。
分区容忍性(P):系统中任意信息的丢失或失败不会影响系统的继续运作。

ZAB协议有两种模式:一种是消息广播模式,另一种是崩溃恢复模式

  • 消息广播模式:

  当集群中已经有过半的Follower服务器完成了和Leader服务器的状态同步,那么整个服务框架就可以进入消息广播模式, 当一台同样遵守ZAB协议的服务器启动后加入到集群中,如果此时集群中已经存在一个Leader服务器在负责进入消息广播,那么加入的服务器就会自觉地进入数据恢复模式:找到Leader所在的服务器,并与其进⾏数据同步,然后⼀起参与到消息⼴播流程中去。 Zookeeper只允许唯一的一个Leader服务器来进⾏事务请求的处理, Leader服务器在接收到客户端的事务请求后,会生成对应的事务提议并发起一轮广播协议,而如果集群中的其他机器收到客户端的事务请求后,那么这些非Leader服务器会首先将这个事务请求转发给Leader服务器。

  • 崩溃恢复模式:

  当整个服务框架启动过程中,或者是Leader服务器出现网络中断、崩溃退出或重启等异常情况时, ZAB协议就会进入崩溃恢复模式,同时选举产生新的Leader服务器。当选举产生了新的Leader服务器,同时集群中已经有过半的机器与该Leader服务器完成了状态同步之后, ZAB协议就会退出恢复模式,其中,所谓的状态同步 就是指数据同步,用来保证集群中过半的机器能够和Leader服务器的数据状态保持一致。

Zookeeper数据结构

ZK内部的存储方式十分类似于文件存储结构,采用了分层存储结构。但是它和文件存储结构的区别是,它的各个节点中是允许存储数据的,需要注意的是zk的每个节点存储数据不能超过1M。每个节点被称为ZNode。Zookeeper中创建的节点分为两种:永久性节点和临时性节点。

临时节点:临时节点的生命周期和客户端会话绑定在一起,客户端会话失效,则这个节点就会被自动清除。
非临时节点(永久节点):该数据节点被创建后,就会一直存在于zookeeper服务器上,直到有删除操作来主动删除这个节点。

Zookeeper数据同步过程

  1. leader接收到消息请求后,将消息赋予一个全局唯一的64位自增id(zxid)。
  2. leader为每个follower准备了一个FIFO队列(通过TCP协议来实现,以实现了全局有序这一个特点)将带有 zxid的消息作为一个提案(proposal)分发给所有的follower。
  3. 当follower接收到proposal,先把proposal写到磁盘,写入成功以后再向leader回复一个ack。
  4. 当leader接收到合法数量(超过半数节点,大于N/2)的ACK后,leader就会向这些follower发送commit命令,同时会在本地执行该消息。
  5. 当follower收到消息的commit命令以后,会提交该消息。

Zookeeper选举机制

集群机器ID

集群机器ID是指myid,它是每一个集群机器中的编号文件,代表Zookeeper集群服务器的标识,手动生成,全局全一。

事务ID

事务ID是指zxid,Zookeeper会给每个更新请求分配一个事务ID,它是一个64位的数字,由Leader统一进行分配,全局唯一,不断递增,在一个节点的状态信息中可以查看到最新的事务ID信息。

集群服务器角色

Zookeeper集群服务器有以下 3 种角色:
Leader(主)
Follower(从,参与投票)
Observer(观察者,不参与投票)

集群服务器状态

LOOKING:寻找 Leader 状态,当服务器处于该状态时,表示当前集群没有 Leader,因此会进入 Leader 选举状态。
FOLLOWING:跟随者状态,表示当前服务器角色是 Follower。
LEADING:领导者状态,表示当前服务器角色是 Leader。
OBSERVING:观察者状态,表示当前服务器角色是 Observer。

选举场景

1、Zookeeper 集群启动初始化时进行选举:
2、Zookeeper 集群 Leader 失联时重新选举

选举前提条件

1、Zookeeper 服务器处于 LOOKING 竞选状态
此时说明 Zookeeper 服务器集群处于群龙无首状态,另外,观察者状态不能参与竞选投票。


2、Zookeeper集群规模至少要3台机器或以上
集群规则为:2N+1台,N>0,即最少需要3台,因为Zookeeper集群的机制是只要超过半数的节点正常,集群就能正常提供服务。只有在ZK 节点挂得太多,只剩一半或不到一半节点能工作时,集群才会失效。

3个节点的 Cluster 可以挂掉1个节点,此时Leader 可以得到2票,仍然满足大于N/2票(此时为3/2=1.5)
2个节点的 Cluster 就不能挂掉任何 1 个节点了,此时Leader可以得到1票,不满足大于N/2票(此时为2/2=1)

Zookeeper选举流程

1.初始化选举

Zookeeper 在集群启动时会进行选举,这里拿 3 台服务器进行举例:
依次启动3台服务器 zk1, zk2, zk3,初始情况下事务 ID 都为 0。

选举大致流程:

a、初始投票
服务器启动后,每个 Server 都会给自己投上一票,每次投票会包含所投票服务器的 myid 和 zxid,这里使用 Server(myid, zxid)的方式表示,此时的投票结果为:zk1(1, 0),zk2(2, 0),zk3(3, 0)

b、同步投票结果
集群中的服务器在投票后,会将各自的投票结果同步给集群中其他服务器。

c、检查投票有效性
各服务器在收到投票后会检查投票的有效性,如:是否本轮投票,是否来自 LOOKING 状态的服务器的投票等。

d、处理投票
服务器之间会进行投票比对,规则如下:
优先检查 zxid,较大的服务器优先作为 Leader
如果 zxid 相同,则 myid 较大的服务器作为 Leader
如:zk1 和 zk2 进行比对,此时 zk2 胜出,zk1 更新自己的投票为:zk1(2, 0)

e、统计投票结果
每轮投票比对之后都会统计投票结果,确认是否有超过半数的机器都得到相同的投票结果,如果是,则选出 Leader,否则继续投票。
本轮选举中,zk1 和 zk2 都得到了相同的投票结果(2, 0),2 指 zk2,并且超过了半数的机器(2 > 1.5),所以此时 zk2 就成为了本轮选举的 Leader。
所以,即使 zk3 启动了,因为集群已经有了 Leader,所以选举也结束了,zk3 不再参与选举,后面进来的都是小弟。

f、更改服务器状态
一旦选出 Leader,每个服务器就会各自更新自己的状态:

zk1 --> LEADING

zk2 --> FOLLOWING

zk3 --> FOLLOWING

2.集群重新选举

Zookeeper 集群运行期间无法和 Leader 保持正常连接时,即如果 Leader 挂了,或者 Leader 服务器故障都会进行新一轮的 Leader 选举。
这里还是拿 3 台服务器进行举例:
如果作为初始选举的 Leader zk2 挂了,集群就会暂停对外提供服务,从而进行新的 Leader 选举。

选举大致过程:

a、状态变更
既然过去的老大 Leader 不可用了,那所有的 Follower 服务器就需要从 FOLLOWING 状态变更为:LOOKING,开始新的一轮 Leader 选举。

b、开始投票
投票逻辑和启动初始时一致。
zk1, zk3 第一轮投票默认还是会先投给自己,zk2 挂了不能进行投票。
第一轮投票结果为:zk1(1, 66)、zk3(3, 28),zk1 和 zk3 各得一票,这里假设 zk1 事务 ID 比 zk3 更大一点。

c、同步投票结果
同步投票逻辑和启动初始时一致。

d、检查投票有效性
检查投票逻辑和启动初始时一致。

e、处理投票
处理投票逻辑和启动初始时一致。
此时 zk1 和 zk3 进行比对,根据规则,由于 zk1 的事务 ID 更大一点,所以 zk1 胜出,zk3 也更新自己的投票为:zk3(1, 28)

f、统计投票结果
统计投票逻辑和启动初始时一致。
本轮选举中,zk1 和 zk3 都得到了相同的投票结果 zk1,并且超过了半数的机器(2 > 3 / 2),所以此时 zk1 就成为了本轮选举的 Leader。

g、更改服务器状态
更改状态逻辑和启动初始时一致。

总结

Zookeeper 集群按 myid 从小到大依次启动初始化时,在超过半数机器的投票的情况下,谁的 myid 最后,谁就是 Leader,知道这个定律,不同的集群规模我们都可以推算出谁是 Leader。

集群重新选举时,根据 myid 和 zxid 的大小共同决断,zxid 更大的优先成为 Leader。

Watch机制

ZooKeeper 提供了分布式数据发布/订阅功能,一个典型的发布/订阅模型系统定义了一种一对多的订阅关系,能让多个订阅者同时监听某一个主题对象,当这个主题对象自身状态变化时,会通知所有订阅者,使他们能够做出相应的处理。

Watch机制的特点:

1、一次性触发数据发生改变时,一个 watcher event 会被发送到 client,但是 client只会收到一次这样的信息。 

2、watcher event 异步发送 watcher 的通知事件从 server 发送到 client 是异步的,这就存在一个问题,不同的客户端和服务器之间通过 socket进行通信,由于网络延迟或其他因素导致客户端在不通的时刻监听到事件,由于 Zookeeper 本身提供了 ordering guarantee,即客户端监听事件后,才会感知它所监视 znode发生了变化。所以我们使用Zookeeper不能期望能够监控到节点每次的变化。Zookeeper 只能保证最终的一致性,而无法保证强一致性。 

3、数据监视 Zookeeper 有数据监视和子数据监视 getdata() and exists()设置数据监视,getchildren()设置了子节点监视。

4、注册 watcher getData、exists、getChildren

5、触发 watcher create、delete、setData

6、setData()会触发 znode 上设置的 data watch(如果 set 成功的话)。一个成功的create()操作会触发被创建的 znode 上的数据 watch,以及其父节点上的 child watch。而一个成功的 delete()操作将会同时触发一个 znode 的 data watch 和 child watch(因为这样就没有子节点了),同时也会触发其父节点的 child watch。 

7、当一个客户端连接到一个新的服务器上时,watch 将会被以任意会话事件触发。当与一个服务器失去连接的时候,是无法接收到 watch 的。而当 client 重新连接时,如果需要的话,所有先前注册过的 watch,都会被重新注册。通常这是完全透明的。只有在一个特殊情况下,watch 可能会丢失:对于一个未创建的 znode 的 exist watch,如果在客户端断开连接期间被创建了,并且随后在客户端连接上之前又删除了,这种情况下,这个 watch事件可能会被丢失。

8、Watch 是轻量级的,其实就是本地 JVM 的 Callback,服务器端只是存了是否有设置了 Watcher 的布尔类型。

Zookeeper如何实现分布式锁

1、指定一个节点,作为存放节点的根节点。

2、使用临时有序节点,然后每次线程进来就有序创建临时节点。

3、每个节点都去监听前一个节点是否存在,存在则说明还没释放锁。

4、因为有序,所以最小的节点持有锁,用完以后删除临时节点,释放锁。

5、下一节点通过watch机制监控到上级已经删除即释放掉锁,所以可以获取到锁,后续以此类推。

 

posted @ 2023-06-21 11:08  请别耽误我写BUG  阅读(81)  评论(0编辑  收藏  举报