【Zookeeper】数据结构、集群原理、选举机制
- 1、Zookeeper的数据结构是怎么样的?
-
- [1.1 如何用Zookeeper实现分布式锁?](#1.1 如何用Zookeeper实现分布式锁?)
- [1.2 Zookeeper的watch机制是如何工作的?](#1.2 Zookeeper的watch机制是如何工作的?)
- [1.3 怎样使用Zookeeper实现服务发现?](#1.3 怎样使用Zookeeper实现服务发现?)
- 2、Zookeeper集群
-
- [2.1 Zookeeper集群中的角色有哪些?有什么区别?](#2.1 Zookeeper集群中的角色有哪些?有什么区别?)
- [2.2 Zookeeper是选举机制是怎样的?](#2.2 Zookeeper是选举机制是怎样的?)
- [2.3 Zookeeper是CP的还是AP的?](#2.3 Zookeeper是CP的还是AP的?)
- [2.4 什么是脑裂?如何解决?](#2.4 什么是脑裂?如何解决?)
- [2.5 Zookeeper是如何保证创建的节点是唯一的?](#2.5 Zookeeper是如何保证创建的节点是唯一的?)
- [2.6 Zookeeper如何保证数据的一致性?](#2.6 Zookeeper如何保证数据的一致性?)
- [2.7 Zookeeper的缺点有哪些?](#2.7 Zookeeper的缺点有哪些?)
1、Zookeeper的数据结构是怎么样的?
ZK中数据是以目录结构的形式存储的。其中的每一个存储数据的节点都叫做Znode,每个Znode都有一个唯一的路径标识。和目录结构类似,每一个节点都可以可有子节点(临时节点除外)。节点中可以存储数据和状态信息,每个Znode上可以配置监视器(watcher),用于监听节点中的数据变化。节点不支持部分读写,而是一次性完整读写。
Znode有四种类型:PERSISTENT(持久节点)、PERSISTENT_SEQUENTIAL(持久顺序节点)、EPHEMERAL(临时节点)、EPHEMERAL_SEQUENTIAL(临时顺序节点)。Znode的类型在创建时确定并且之后不能再修改。
1、EPHEMERAL(临时节点)
临时节点的生命周期和客户端会话绑定。也就是说,如果客户端会话失效,那么这个节点就会自动被清除掉。临时节点也不能有子节点。
java
String root = "/ephemeral";
String createdPath = zk.create(root, root.getBytes(),
Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
System.out.println("createdPath = " + createdPath);
String path = "/ephemeral/test01" ;
createdPath = zk.create(path, path.getBytes(),
Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
System.out.println("createdPath = " + createdPath);
Thread.sleep(1000 * 20); // 等待20秒关闭ZooKeeper连接
zk.close(); // 关闭连接后创建的临时节点将自动删除
2、PERSISTENT(持久节点)
所谓持久节点,是指在节点创建后,就一直存在,直到有删除操作来主动清除这个节点------不会因为创建该节点的客户端会话失效而消失。
java
String root = "/computer";
String createdPath = zk.create(root, root.getBytes(),
Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
System.out.println("createdPath = " + createdPath);
3、EPHEMERAL_SEQUENTIAL(临时顺序节点)
临时节点的生命周期和客户端会话绑定。也就是说,如果客户端会话失效,那么这个节点就会自动被清除掉。注意创建的节点会自动加上编号。
java
String root = "/ephemeral";
String createdPath = zk.create(root, root.getBytes(),
Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
System.out.println("createdPath = " + createdPath);
String path = "/ephemeral/test01" ;
createdPath = zk.create(path, path.getBytes(),
Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
System.out.println("createdPath = " + createdPath);
Thread.sleep(1000 * 20); // 等待20秒关闭ZooKeeper连接
zk.close(); // 关闭连接后创建的临时节点将自动删除
输出结果:
java
type = None
createdPath = /ephemeral/test0000000003
createdPath = /ephemeral/test0000000004
createdPath = /ephemeral/test0000000005
createdPath = /ephemeral/test0000000006
4、PERSISTENT_SEQUENTIAL(持久顺序节点)
这类节点的基本特性和持久节点类型是一致的。额外的特性是,在ZooKeeper中,每个父节点会为他的第一级子节点维护一份时序,会记录每个子节点创建的先后顺序。基于这个特性,在创建子节点的时候,可以设置这个属性,那么在创建节点过程中,ZooKeeper会自动为给定节点名加上一个数字后缀,作为新的节点名。这个数字后缀的范围是整型的最大值。
java
String root = "/computer";
String createdPath = zk.create(root, root.getBytes(),
Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
System.out.println("createdPath = " + createdPath);
for (int i=0; i<5; i++) {
String path = "/computer/node";
String createdPath = zk.create(path, path.getBytes(),
Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT_SEQUENTIAL);
System.out.println("createdPath = " + createdPath);
}
zk.close();
运行结果:
java
createdPath = /computer
createdPath = /computer/node0000000000
createdPath = /computer/node0000000001
createdPath = /computer/node0000000002
createdPath = /computer/node0000000003
createdPath = /computer/node0000000004
结果中的0000000000~0000000004都是自动添加的序列号
5、ACL
每个znode被创建时都会带有一个ACL列表,用于决定谁可以对它执行何种操作。
6、观察(watcher)
Watcher 在 ZooKeeper 是一个核心功能,Watcher 可以监控目录节点的数据变化以及子目录的变化,一旦这些状态发生变化,服务器就会通知所有设置在这个目录节点上的 Watcher,从而每个客户端都很快知道它所关注的目录节点的状态发生变化,而做出相应的反应
可以设置观察的操作:exists,getChildren,getData 可以触发观察的操作:create,delete,setData
znode以某种方式发生变化时,"观察"(watch)机制可以让客户端得到通知。可以针对ZooKeeper服务的"操作"来设置观察,该服务的其他 操作可以触发观察。比如,客户端可以对某个客户端调用exists操作,同时在它上面设置一个观察,如果此时这个znode不存在,则exists返回 false,如果一段时间之后,这个znode被其他客户端创建,则这个观察会被触发,之前的那个客户端就会得到通知。
1.1 如何用Zookeeper实现分布式锁?
基于zookeeper临时有序节点可以实现的分布式锁。
大致思想即为:每个客户端对某个方法加锁时,在zookeeper上的与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点。 判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。 当释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。
来看下Zookeeper能不能解决以下问题。
1、锁无法释放问题?使用Zookeeper可以有效的解决锁无法释放的问题,因为在创建锁的时候,客户端会在ZK中创建一个临时节点,一旦客户端获取到锁之后突然挂掉(Session连接断开),那么这个临时节点就会自动删除掉。其他客户端就可以再次获得锁。
2、非阻塞锁问题?使用Zookeeper可以实现阻塞的锁,客户端可以通过在ZK中创建顺序节点,并且在节点上绑定监听器,一旦节点有变化,Zookeeper会通知客户端,客户端可以检查自己创建的节点是不是当前所有节点中序号最小的,如果是,那么自己就获取到锁,便可以执行业务逻辑了。
3、不可重入问题?使用Zookeeper也可以有效的解决不可重入的问题,客户端在创建节点的时候,把当前客户端的主机信息和线程信息直接写入到节点中,下次想要获取锁的时候和当前最小的节点中的数据比对一下就可以了。如果和自己的信息一样,那么自己直接获取到锁,如果不一样就再创建一个临时的顺序节点,参与排队。
4、单点问题?使用Zookeeper可以有效的解决单点问题,ZK是集群部署的,只要集群中有半数以上的机器存活,就可以对外提供服务。
使用ZK实现的分布式锁好像完全符合了我们对一个分布式锁的所有期望。其实并不是,Zookeeper实现的分布式锁其实存在一个缺点,那就是性能上可能并没有缓存服务那么高。因为每次在创建锁和释放锁的过程中,都要动态创建、销毁瞬时节点来实现锁功能。ZK中创建和删除节点只能通过Leader服务器来执行,然后将数据同步到所有的Follower机器上。
其实,使用Zookeeper也有可能带来并发问题,只是并不常见而已。考虑这样的情况,由于网络抖动,客户端和ZK集群的session连接断了,那么zk以为客户端挂了,就会删除临时节点,这时候其他客户端就可以获取到分布式锁了。就可能产生并发问题。这个问题不常见是因为zk有重试机制,一旦zk集群检测不到客户端的心跳,就会重试,Curator客户端支持多种重试策略。多次重试之后还不行的话才会删除临时节点。(所以,选择一个合适的重试策略也比较重要,要在锁的粒度和并发之间找一个平衡。)
1.2 Zookeeper的watch机制是如何工作的?
在Zookeeper中,watch机制是一种非常重要的特性,它能够让应用程序监听Zookeeper上节点的变化,从而及时做出响应。
Zookeeper的watch机制实现中,涉及到多个概念,首先是客户端和服务端,这个好理解,Zookeeper的集群就是服务端,调用ZK服务的机器就是客户端。
还有两个模块,分别叫做WatchManager和ZkWatcherManager。
WatchManager是Zookeeper服务端内部的一个模块,用于管理所有watcher的相关操作,包括watcher的注册、注销、触发等。
而ZkWatcherManager是Zookeeper客户端中的一个模块,用于管理客户端中watcher的相关操作,包括创建watcher、注册watcher、处理watcher事件等。
ZK的watch机制是如何工作的:
-
客户端连接到Zookeeper服务端,客户端创建一个ZkWatcherManager实例,用于管理客户端中所有的watcher。
-
当客户端想要监控某个znode节点时,它可以调用ZkWatcherManager中的方法创建watcher并将其注册到客户端中。客户端将watcher的信息发送到Zookeeper服务端。
-
Zookeeper服务端接收到客户端发送的watcher信息后,会将该watcher信息交给WatchManager处理。WatchManager会将该watcher注册到相应的znode节点上,并将watcher相关的信息保存在内存中。

-
当znode节点发生变化时,WatchManager会通知Zookeeper Server
-
Zookeeper Server会根据变化类型通知相应的客户端,告知它们发生了哪些变化。
-
当客户端接收到Zookeeper Server的通知后,ZkWatcherManager会根据watcher的类型(data watcher或child watcher)来触发相应的事件处理方法,例如data watcher会触发processDataChanged()方法,child watcher会触发processChildChanged()方法等。

1.3 怎样使用Zookeeper实现服务发现?
服务发现是ZK的重要用途之一,想要基于zk实现服务发现时,一般可以参考以下步骤:
-
向Zookeeper注册服务
服务提供者需要在Zookeeper上创建一个临时节点来注册自己的服务。节点的名称通常是服务名称和版本号等信息的组合,节点的数据可以包含服务的地址、端口、协议等信息。因为是临时节点,所以当服务提供者关闭或崩溃时,该节点将自动从Zookeeper中删除。
-
客户端订阅服务
服务消费者需要在Zookeeper上订阅自己所需的服务。它可以监听服务节点的变化,一旦节点发生变化,就会接收到通知。服务消费者可以根据需要选择自己感兴趣的服务节点。
服务消费者还可以通过Zookeeper提供的API获取当前可用的服务列表。它可以从服务节点的子节点中获取服务的地址和端口等信息,然后根据自己的业务逻辑选择一个合适的服务节点。还可以在服务节点上设置监听器,一旦节点发生变化,就会收到通知。这样可以保证服务消费者随时获取到最新的服务列表。
2、Zookeeper集群
2.1 Zookeeper集群中的角色有哪些?有什么区别?
ZK中主要有以下角色:
领导者(leader):负责进行投票的发起和决议,更新系统状态。为客户端提供读和写服务。
跟随者(follower):用于接受客户端请求并响应客户端返回结果,在选主过程中参与投票。为客户端提供读服务。
观察者(observer):可以接受客户端连接,将写请求转发给leader,但observer不参加投票过程,只同步leader的状态,observer的目的是为了扩展系统,提高读取速度。
客户端(client):请求发起方

2.2 Zookeeper是选举机制是怎样的?
ZooKeeper的选举机制是其实现分布式协调一致性的核心部分,它确保在ZooKeeper集群中选择一个Leader节点来协调和处理客户端请求。
一次完整的选举大概要经历以下几个步骤:
1、初始化阶段
在一个ZooKeeper集群中,每个Follower节点都可以成为Leader。初始状态下,所有Follower节点都是处于"LOOKING"状态,即寻找Leader。每个节点都会监视集群中的其他节点,以侦听Leader选举消息。
2、提名和投票
当一个节点启动时,它会向其他节点发送投票请求,称为提名。节点收到提名后可以选择投票支持这个提名节点,也可以不投票。每个节点只能在一个选举周期内投出一票。
在Zookeeper中,通过数据是否足够新来判断这个节点是不是够强。在 Zookeeper 中以事务id(zxid)来标识数据的新旧程度,节点的zxid越大代表这个节点的数据越新,也就代表这个节点能力越强。
那么在投票过程中,节点首先会认为自己是最强的,所以他会在投票时先投自己一票,然后把自己的投票信息广播出去,这里面包含了zxid和sid,zxid就是自己的事务ID,sid就是标识出自己是谁的唯一标识。
这样集群中的节点们就会不断收到别人发过来的投票结果,然后这个节点就会拿别人的zxid和自己的zxid进行比较,如果别人的zxid更大, 说明他的数据更新,那么就会重新投票,把zxid和sid都换成别人的信息再发出去。
3、选举过程
选举过程分为多个轮次,每个轮次被称为一个"选举周期"。在每个选举周期中,节点根据投票数来选择新的Leader候选者。如果一个候选者获得了大多数节点(超过半数)的投票,那么它就会成为新的Leader。否则,没有候选者能够获得足够的投票,那么这个选举周期失败,所有节点会继续下一个选举周期。
4、Leader确认
一旦一个候选者获得了大多数节点的投票,它就会成为新的Leader。这个Leader会向其他节点发送Leader就绪消息,告知它们自己已经成为Leader,并且开始处理客户端的请求。
5、集群同步
一旦新的Leader选举完成,其他节点会与新Leader同步数据,确保所有节点在一个一致的状态下运行。这个同步过程也包括了未完成的客户端请求,以保证数据的一致性。
2.3 Zookeeper是CP的还是AP的?
ZooKeeper作为分布式协调服务,它的职责是保证数据在其管辖下的所有服务之间保持同步、一致。所以,可以认为Zookeeper是一个CP的分布式系统。所以他会牺牲可用性,也就是在极端环境下,ZooKeeper可能会丢弃一些请求,消费者程序需要重新请求才能获得结果。
这个 CP体现在以下几个情况下:
1、如果集群中的存活节点数低于总结点数的一半,那么整个集群将无法接受新的写请求。
2、在 ZK 的 master 选举过程中,在新的Master被选举出来之前,整个集群也无法接受新的写请求。
如果 ZooKeeper下所有节点都断开了,或者集群中出现了网络分割的故障(注:由于交换机故障导致交换机底下的子网间不能互访);那么ZooKeeper 会将它们都从自己管理范围中剔除出去,外界就不能访问到这些节点了,即便这些节点本身是"健康"的,可以正常提供服务的;所以导致到达这些节点的服务请求被丢失了。
但是,这里面的一致性,他确实是强一致性,但是,Zookeeper保证的是强一致模型中的顺序一致性而不是线性一致性。

Zookeeper是保证的顺序一致性,也就是说,ZooKeeper不保证在每个时间点,两个不同的客户端将具有相同的ZooKeeper数据视图。但是他能保证我们在每个节点上读取到的一定是他最后一次更新的内容。
具体的案例就是,当Zookeeper在进行数据同步的过程中,如果半数节点同步成功,它就提交当前事务,但此时集群内还有可能有节点没有同步到数据,如果此时读请求发送到没有同步到数据的节点,那么就会读到旧的数据。但是Zookeeper是会保证这个节点最终也会按照顺序执行成功的。
想要让Zookeeper真正的保证强一致性,或者说保证线性一致性也是有办法的,那就是通过sync命令。
当我们对一个Follower调用sync命令的时候,会使得他和Leader节点进行数据同步,并等待服务器同步完成之后再返回。这样下一次的read就能保证拿到的是最新数据了。(不发生脑裂的情况下)
2.4 什么是脑裂?如何解决?
脑裂是在分布式系统中经常出现的问题之一,它指的是由于网络或节点故障等原因,导致一个分布式系统被分为多个独立的子系统,每个子系统独立运行,无法相互通信,同时认为自己是整个系统的主节点,这就会导致整个系统失去一致性和可用性。
可以通过设置合适的选举超时时间、设置合适的节点数量等方式来减少脑裂的可能性。同时,可以使用ZooKeeper提供的Watch机制来监听节点状态的变化,及时发现并处理异常情况,从而避免脑裂的发生。
Zookeeper集群中的脑裂出现的原因通常有以下2种情况:
-
网络分区
当Zookeeper集群中的某些节点无法与其他节点通信时,就会出现网络分区现象。这时,无法确定哪个节点是主节点,容易导致多个主节点的情况。
-
主节点宕机
当Zookeeper集群中的主节点宕机时,其他节点可能会重新选举新的主节点。如果宕机的主节点恢复后,会与其他节点产生不一致,可能导致脑裂。
针对Zookeeper集群中的脑裂问题,可以采取以下几种方式进行恢复脑裂:
-
自动恢复机制
当Zookeeper集群中出现脑裂时,Zookeeper会自动发现并尝试恢复。当大多数节点恢复后,会重新选举主节点,并将状态同步给其他节点。
-
手动恢复
手动恢复可以通过在网络分区的节点上运行一个Zookeeper服务实例,并将其配置为独立的集群,等待分区恢复后将其重新合并。在主节点宕机时,可以使用手动恢复来恢复脑裂。这种方式需要手动干预,比较复杂,需要考虑数据同步、节点选举等问题。
2.5 Zookeeper是如何保证创建的节点是唯一的?
Zookeeper通过两个手段来保证节点创建的唯一性:
1、所有的写请求都会由Leader进行,即使是请求到Follower节点,也会被转发到Leader节点上执行。
2、在Leader上写入数据的时候,先是通过synchronized锁,将父节点锁住,然后再在锁里面判断是否已经存在节点,如果已存在,直接抛异常,如果不存在,则向维护了节点的map------NodeHashMap中添加当前节点。保证了并发情况下只有一个线程可以添加成功。
2.6 Zookeeper如何保证数据的一致性?
作为一个分布式一致性协调组件,他的主要目标就是保证数据一致性。他主要的实现原理就是通过ZAB算法来实现强一致性。ZAB算法是ZooKeeper 自研的一种一致性协议,类似于 Paxos / Raft,但针对其"读多写少"的场景做了优化。
ZAB的核心思想:
1、所有 写操作都由 Leader 节点处理并广播给所有 Follower。
2、所有节点按相同顺序接收并应用写操作,保证数据一致。
3、一次写操作必须获得过半节点(Quorum)确认才能提交。
ZooKeeper 通过 ZAB 协议 + Leader单节点写入 + Leader选举机制 + 多数写入确认 + 日志快照机制,确保分布式节点间的强一致性(顺序一致性),即所有节点在任意时刻看到的都是相同的数据状态。
2.7 Zookeeper的缺点有哪些?
ZooKeeper 是一个分布式协调组件,在分布式系统中被广泛应用,但是其实他还是存在很多缺点的。
最重要的就是性能问题,ZooKeeper 设计是为了高一致性,其 ZAB 协议需要同步写操作到大多数节点,导致写操作的性能较低。在高并发写入场景下,ZooKeeper 的性能会成为瓶颈。
第二个就是ZooKeeper 的所有写操作必须经过 Leader 节点处理,所有写请求都需要通过 Leader 同步到其他节点。那么这个Leader就是整个集群的瓶颈,如果 Leader 负载过高,就可能会导致性能下降。如果Leader挂了,就会无法工作(直到新的leader选举成功)
第三,ZooKeeper 将数据存储在内存中,以提供快速访问性能。这意味着它不适合存储大量数据,通常只适合存储小型的元数据和配置信息。如果数据量过大,会导致内存不足或性能下降。
还有就是,ZooKeeper 假设节点之间的网络和节点状态变化是少见的。如果网络分区或节点频繁故障,会频繁触发 Leader 选举,影响可用性。
相较于一些现代分布式协调工具(如 etcd、Consul),ZooKeeper 的易用性和工具生态略显不足,运维和管理复杂度较高。
参考链接:
1、https://www.yuque.com/hollis666/wk6won/vwxvpehfhu0ppkhx
2、https://www.yuque.com/hollis666/wk6won/lxznb86av97adwt6
3、https://www.yuque.com/hollis666/wk6won/tsfqf463g4mbh41k