1、ZooKeeper 介绍
- 高可用性:ZooKeeper 通过多节点集群实现高可用性,纵然部门节点故障,服务仍然可用。
- 一致性:ZooKeeper 利用 ZAB(ZooKeeper Atomic Broadcast)协议包管数据的一致性。
- 顺序性:ZooKeeper 包管客户端的操纵顺序与请求顺序一致。
- 高性能:ZooKeeper 的设计目标是高吞吐量和低延迟。
- ZooKeeper 的数据模型类似于文件系统的树形结构,每个节点称为 ZNode。ZNode 可以存储数据,而且可以有子节点。
- 长期(PERSISTENT)节点 :一旦创建就一直存在纵然 ZooKeeper 集群宕机,直到将其删除。
- 暂时(EPHEMERAL)节点 :暂时节点的生命周期是与 客户端会话(session) 绑定的,会话消失则节点消失 。而且,暂时节点只能做叶子节点 ,不能创建子节点。
- 长期顺序(PERSISTENT_SEQUENTIAL)节点 :除了具有长期(PERSISTENT)节点的特性之外, 子节点的名称还具有顺序性。比如 /node1/app0000000001 、/node1/app0000000002 。
- 暂时顺序(EPHEMERAL_SEQUENTIAL)节点 :除了具备暂时(EPHEMERAL)节点的特性之外,子节点的名称还具有顺序性
- ZooKeeper 集群通常由多个节点组成,其中一个节点是 Leader,其他节点是 Follower。
- Leader:负责处理写请求,并将写操纵广播给 Follower。
- Follower:处理读请求,并到场 Leader 选举和写操纵的投票。
- 设置管理
- ZooKeeper 可以用于存储和管理分布式系统的设置信息。
- 示例:HBase 利用 ZooKeeper 存储 RegionServer 的设置。
- 命名服务
- ZooKeeper 可以用于实现分布式系统中的命名服务。
- 示例:Kafka 利用 ZooKeeper 管理 Broker 的命名和元数据。
- 分布式锁
- ZooKeeper 可以用于实现分布式锁,确保多个节点之间的互斥访问。
- 示例:利用暂时顺序节点实现分布式锁。
- 领导者选举
- ZooKeeper 可以用于实现分布式系统中的领导者选举。
- 示例:HBase 利用 ZooKeeper 选举 Master 节点。
- 服务发现
- ZooKeeper 可以用于实现服务发现,动态管理服务的注册和发现。
- 示例:Dubbo 利用 ZooKeeper 实现服务注册和发现。
2、znode 节点里面的存储
Stat 类中包含了一个数据节点的全部状态信息的字段,包括事务 ID(cZxid)、节点创建时间(ctime) 和子节点个数(numChildren) 等等,如下:
- data : znode 存储业务数据信息。
- acl : 记录客户端对 znode 节点访问权限,如 IP 等。
- ZooKeeper 采取 ACL(AccessControlLists)策略来进行权限控制,类似于 UNIX 文件系统的权限控制。对于 znode 操纵的权限,ZooKeeper 提供了以下 5 种:
- CREATE : 能创建子节点
- READ :能获取节点数据和列出其子节点
- WRITE : 能设置/更新节点数据
- DELETE : 能删除子节点
- ADMIN : 能设置节点 ACL 的权限
- 其中尤其需要注意的是,CREATE 和 DELETE 这两种权限都是针对 子节点 的权限控制。对于身份认证,提供了以下几种方式:
- world : 默认方式,全部用户都可无条件访问。
- auth :不利用任何 id,代表任何已认证的用户。
- digest :用户名:密码认证方式: username:password 。
- ip : 对指定 ip 进行限定
- child : 当前节点子节点引用。
3、znode 节点上监听机制
Watcher 为事件监听器,是 zk 非常重要的一个特性,许多功能都依赖于它,它有点类似于订阅的方式,即客户端向服务端注册指定的 watcher ,当服务端符合了 watcher 的某些事件或要求则会向客户端发送事件关照 ,客户端收到关照后找到自己界说的 Watcher 然后实验相应的回调方法 。
可以把 Watcher 理解成客户端注册在某个 Znode 上触发器,当这个 Znode 节点发生厘革时(增删改查),就会触发 Znode 对应注册事件,注册客户端就会收到异步关照,然后做出业务改变。
zookeeper 监听原理
zookeeper的监听事件有四种
- nodedatachanged 节点数据改变
- nodecreate 节点创建事件
- nodedelete 节点删除事件
- nodechildrenchanged 子节点改变事件
ZooKeeper Watcher 机制重要包括客户端线程、客户端WatcherManager、Zookeeper 服务器三部门。
- 客户端向 ZooKeeper 服务器注册 Watcher 同时,会将 Watcher 对象存储在客户端 WatchManager 中。
- 当 zookeeper 服务器触发 watcher 事件后,会向客户端发送关照, 客户端线程从 WatcherManager 中取出对应Watcher 对象来实验回调逻辑。
4、ZooKeeper 集群部署
为了包管高可用,最好是以集群形态来部署 ZooKeeper,这样只要集群中大部门机器是可用的(能够容忍一定的机器故障),那么 ZooKeeper 本身仍然是可用的。通常 3 台服务器就可以构成一个 ZooKeeper 集群了。ZooKeeper 官方提供的架构图就是一个 ZooKeeper 集群团体对外提供服务。
上图中每一个 Server 代表一个安装 ZooKeeper 服务的服务器。组成 ZooKeeper 服务的服务器都会在内存中维护当前的服务器状态,而且每台服务器之间都互相保持着通信。集群间通过 ZAB 协议来保持数据的一致性。
最典型集群模式: Master/Slave 模式(主备模式)。在这种模式中,通常 Master 服务器作为主服务器提供写服务,其他的 Slave 服务器从服务器通过异步复制的方式获取 Master 服务器最新的数据提供读服务。
ZooKeeper 集群角色
ZooKeeper 集群中的全部机器通过一个 Leader 选举过程 来选定一台称为 “Leader” 的机器,Leader 既可以为客户端提供写服务又能提供读服务。除了 Leader 外,Follower 和 Observer 都只能提供读服务。Follower 和 Observer 唯一的区别在于 Observer 机器不到场 Leader 的选举过程,也不到场写操纵的“过半写乐成”策略,因此 Observer 机器可以在不影响写性能的情况下提拔集群的读性能。
5、ZooKeeper 选举机制
ZooKeeper 的选举机制 是包管其高可用性和一致性的核心部门。ZooKeeper 利用 ZAB(ZooKeeper Atomic Broadcast)协议 来实现领导者选举和数据同步。
选举的触发条件
集群启动:当 ZooKeeper 集群启动时,全部节点会到场选举。
Leader 失效:当 Leader 节点失效时,Follower 节点会重新选举 Leader。
① 选举初始化
- 当 ZooKeeper 集群启动或 Leader 节点失效时,集群中的节点会进入选举状态。每个节点会实验选举自己为 Leader。
② 投票
- 每个节点会投票给自己,并将投票信息(包括节点的 ID 和事务 ID)广播给其他节点。节点收到其他节点的投票后,会比力投票信息:
- 如果收到的投票的事务 ID 更大,则更新自己的投票。
- 如果事务 ID 雷同,则比力节点 ID,选择更大的节点 ID。
③ 确定 Leader
- 当某个节点收到超过半数的投票时,该节点被选举为 Leader。Leader 会向其他节点发送确认消息,其他节点确认后成为 Follower。
④ 数据同步
- Leader 会将自己的数据状态同步给 Follower,确保集群中的数据一致性。同步完成后,集群进入正常工作状态。
6、作甚集群脑裂
对于一个集群,想要提高这个集群的可用性,通常会采取多机房部署,比如现在有一个由6台zkServer所组成的一个集群,部署在了两个机房:
正常情况下,此集群只会有一个Leader,那么如果机房之间的网络断了之后,两个机房内的zkServer照旧可以相互通信的,如果不考虑过半机制,那么就会出现每个机房内部都将选出一个Leader。
这就相当于本来一个集群,被分成了两个集群,出现了两个“大脑”,这就是脑裂。
对于这种情况,我们也可以看出来,本来应该是统一的一个集群对外提供服务的,现在变成了两个集群同时对外提供服务,如果过了一会,断了的网络忽然联通了,那么此时就会出现标题了,两个集群刚刚都对外提供服务了,数据该怎么合并,数据冲突怎么办理等等标题。
刚刚在阐明脑裂场景时,有一个前提条件就是没有考虑过半机制,以是实际上Zookeeper集群中是不会出现脑裂标题的,而不会出现的原因就跟过半机制有关。
过半机制
举个简单的例子: 如果现在集群中有6台zkServer,也就是说至少要4台zkServer才能选出来一个Leader,才会符合过半机制,才能选出来一个Leader。
以是对于机房1来说它不能选出一个Leader,同样机房2也不能选出一个Leader,这种情况下整个集群当机房间的网络断掉后,整个集群将没有Leader。
如果假设我们现在只有5台机器,也部署在两个机房:
也就是至少要3台服务器才能选出一个Leader,此时机房件的网络断开了,对于机房1来说是没有影响的,Leader依然照旧Leader,对于机房2来说是选不出来Leader的,此时整个集群中只有一个Leader。
以是,我们可以总结得出,有了过半机制,对于一个Zookeeper集群,要么没有Leader,要没只有1个Leader,这样就避免了脑裂标题。
过半机制是怎样防止脑裂现象产生的
ZooKeeper 的过半机制导致不可能产生 2 个 leader,由于少于等于一半是不可能产生 leader 的,这就使得岂论机房的机器怎样分配都不可能发生脑裂。
ZooKeeper 集群为啥最好奇数台
ZooKeeper 集群在宕掉几个 ZooKeeper 服务器之后,如果剩下的 ZooKeeper 服务器个数大于宕掉的个数的话整个 ZooKeeper 才依然可用。假如我们的集群中有 n 台 ZooKeeper 服务器,那么也就是剩下的服务数必须大于 n/2。先说一下结论,2n 和 2n-1 的容忍度是一样的,都是 n-1,大家可以先自己仔细想一想,这应该是一个很简单的数学标题了。
比如假如我们有 3 台,那么最大允许宕掉 1 台 ZooKeeper 服务器,如果我们有 4 台的的时间也同样只允许宕掉 1 台。 假如我们有 5 台,那么最大允许宕掉 2 台 ZooKeeper 服务器,如果我们有 6 台的的时间也同样只允许宕掉 2 台。
综上,何须增长那一个不必要的 ZooKeeper 呢?
7、怎样包管数据一致性
在 ZooKeeper 集群中,全部客户端的请求都是写入到 Leader 进程中的,然后,由 Leader 同步到其他节点,称为 Follower。在集群数据同步的过程中,如果出现 Follower 节点崩溃大概 Leader 进程崩溃时,都会通过 Zab 协议来包管数据一致性。
ZAB 协议是一种原子广播协议,用于在分布式系统中实现数据的一致性和可靠性。它的核心头脑包括:
- 原子广播:确保全部写操纵以雷同的顺序被全部节点吸收和实验。
- 领导者选举:在集群启动或 Leader 失效时,选举新的 Leader 来和谐写操纵。
- 数据同步:确保 Leader 和 Follower 之间的数据一致性。
ZAB 协议的工作流程分为两个阶段:
- 选举阶段:
- 当集群启动或 Leader 失效时,ZooKeeper 会进入选举阶段。
- 通过投票机制选举新的 Leader,确保集群中只有一个 Leader。
- 广播阶段:
- Leader 负责吸收客户端的写请求,并将写操纵广播给全部 Follower。
- Follower 吸收到写操纵后,将其应用到本地状态,并向 Leader 发送确认。
- 当 Leader 收到多数 Follower 的确认后,提交写操纵并关照客户端。
消息广播阶段,ZooKeeper中的一个节点被选为leader节点,它吸收来自客户端的事务提交请求,并将这些请求作为proposal广播给其他follower节点。每个follower节点收到proposal后会进行反馈,leader节点根据收集到的反馈决定是否实验commit操纵。为了包管数据一致性,ZooKeeper利用了quorum选举机制来决定大多数节点上的commit效果。
client端发起请求,读请求由follower和observer直接返回,写请求由它们转发给leader。Leader 首先为这个事务分配一个全局单调递增的唯一事务ID (即 ZXID )。然后发起proposal给follower,Leader 会为每一个 Follower 都各自分配一个单独的队列,然后将需要广播的事务 Proposal 依次放入这些队列中去,而且根据 FIFO策略进行消息发送。每一个 Follower 在吸收到这个事务 Proposal 之后,都会首先将其以事务日志的形式写入到本地磁盘中去,而且在乐成写入后反馈给 Leader 服务器一个 Ack 相应。当 Leader 服务器吸收到超过半数 Follower 的 Ack 相应后,就会广播一个Commit 消息给全部的 Follower 服务器以关照其进行事务提交,同时Leader 自身也会完成对事务的提交。
8、讲一下 zk 分布式锁实现原理吧
实现分布式锁要借助暂时顺序节点和watch,首先我们要有一个长期节点,客户端获取锁就是在长期节点下创建暂时顺序节点。客户端创建的暂时顺序节点创建乐成后会判断节点是不是最小节点,如果是最小节点那么获取锁乐成,否则回去锁失败。如果获取锁失败,则阐明有其他客户端已乐成获得锁,这时间也不需要循环实验去加锁,而是给前一个节点注册一个事件监听器,这个监听器作用就是当前一个节点开释后,也就是节点删除后关照自己让自己获得锁,这样的利益是不会关照到全部的节点去争取锁(避免无效自旋)。以是利用Zookeeper实现的分布式锁是公平锁。
为什么要用暂时顺序节点
暂时节点相比长期节点,最重要的是对会话失效的情况处理不一样,暂时节点会话消失则对应的节点消失。这样的话,如果客户端发生异常导致没来得及开释锁也没关系,会话失效节点自动被删除,不会发存亡锁的标题。
利用 Redis 实现分布式锁的时间,我们是通过逾期时间来避免锁无法被开释导致死锁标题的,而 ZooKeeper 直接利用暂时节点的特性即可。
假设不适用顺序节点的话,全部实验获取锁的客户端都会对持有锁的子节点加监听器。当该锁被开释之后,势必会造成全部实验获取锁的客户端来争取锁,这样对性能不友爱。利用顺序节点之后,只需要监听前一个节点就好了,对性能更友爱。
为什么要设置对前一个节点的监听
同一时间段内,可能会有许多客户端同时获取锁,但只有一个可以获取乐成。如果获取锁失败,则阐明有其他的客户端已经乐成获取锁。获取锁失败的客户端并不会不停地循环去实验加锁,而是在前一个节点注册一个事件监听器。
这个事件监听器的作用是: 当前一个节点对应的客户端开释锁之后(也就是前一个节点被删除之后,监听的是删除事件),关照获取锁失败的客户端(叫醒等待的线程,Java 中的 wait/notifyAll ),让它实验去获取锁,然后就乐成获取锁了。
原生API 分布式锁
- package com.example.test.other.zk;
- import org.apache.zookeeper.*;
- import org.apache.zookeeper.data.Stat;
- import java.io.IOException;
- import java.util.Collections;
- import java.util.List;
- import java.util.concurrent.CountDownLatch;
- public class ZookepeerLock1 {
- private final String connectionString = "127.0.0.1:2181,127.0.0.1:2182,127.0.0.1:2183";
- private final int sessionTimeout = 2000;
- private final ZooKeeper zk;
- private CountDownLatch countDownLatch = new CountDownLatch(1);
- private CountDownLatch waitLatch = new CountDownLatch(1);
- private String waitPath;
- private String currentMode;
- public ZookepeerLock1() throws IOException, InterruptedException, KeeperException {
- // 获取连接
- zk = new ZooKeeper(connectionString, sessionTimeout, new Watcher() {
- @Override
- public void process(WatchedEvent watchedEvent) {
- // connectLatch 如果连接上zk 可以释放
- if (watchedEvent.getState() == Event.KeeperState.SyncConnected) {
- countDownLatch.countDown();
- }
- // waitLatch 需要释放
- if (watchedEvent.getType() == Event.EventType.NodeDeleted && watchedEvent.getPath().equals(waitPath)) {
- waitLatch.countDown();
- }
- }
- });
- // 等待zk正常连接后,往下走程序
- countDownLatch.await();
- // 判断根节点/locks是否存在
- Stat stat = zk.exists("/lockZookeeper", false);
- if (stat == null) {
- // 创建根节点,这是⼀个完全开放的ACL,持久节点
- zk.create("/lockZookeeper", "lockZookeeper".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
- }
- }
- // 对zk加锁
- public void zkLock() {
- try {
- // 创建对应的临时顺序节点
- currentMode =
- zk.create("/lockZookeeper/" + "seq-", null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
- // 判断创建的节点是否是序号最小的节点,如果是获取到锁,如果不是,监听他序号前一个节点
- List<String> children = zk.getChildren("/lockZookeeper", false);
- if (children.size() == 1) {
- return;
- } else {
- //[seq-0000000016, seq-0000000017]
- Collections.sort(children);
- // 获取节点名称 /locks/seq-0000000017 -> seq-0000000017
- String thisNode = currentMode.substring("/lockZookeeper/".length());
- // 通过seq-0000000017获取该节点在children集合的位置
- int index = children.indexOf(thisNode);
- // 判断
- if (index == -1) {
- System.out.println("数据异常");
- } else if (index == 0) {
- // 就一个节点,可以获取锁了
- return;
- } else {
- // 需要监听 他前一个结点的变化
- waitPath = "/lockZookeeper/" + children.get(index - 1);
- zk.getData(waitPath, true, null);
- // 等待监听
- waitLatch.await();
- }
- }
- } catch (KeeperException e) {
- e.printStackTrace();
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- // 解锁
- public void unZkLock() {
- // 删除节点
- try {
- zk.delete(currentMode, -1);
- } catch (InterruptedException e) {
- e.printStackTrace();
- } catch (KeeperException e) {
- e.printStackTrace();
- }
- }
- }
复制代码 Curator 分布式锁
- package com.example.test.other.zk;
- import org.apache.curator.RetryPolicy;
- import org.apache.curator.framework.CuratorFramework;
- import org.apache.curator.framework.CuratorFrameworkFactory;
- import org.apache.curator.framework.recipes.locks.InterProcessLock;
- import org.apache.curator.framework.recipes.locks.InterProcessMutex;
- import org.apache.curator.framework.recipes.locks.InterProcessSemaphoreMutex;
- import org.apache.curator.retry.ExponentialBackoffRetry;
- import org.apache.zookeeper.KeeperException;
- import org.springframework.context.annotation.Bean;
- import org.springframework.context.annotation.Configuration;
- import java.io.IOException;
- /**
- * Description: 1、@Bean单独注解方法时,每次调用方法都是执行方法内的逻辑并返回新创建的对象bean,而且SpringIOC并没有该bean的存在。
- * 2、@Bean + @Configuration ,在调用@Bean注解的方法时返回的实例bean是从IOC容器获取的,已经注入的,且是单例的,而不是新创建的。
- * 3、@Bean + @Component,虽然@Bean注解的方法返回的实例已经注入到SpringIOC容器中,但是每次调用@Bean注解的方法时,都会创建新的对象实例bean返回,并不会从IOC容器中获取。
- * Author: yangjj_tc
- * Date: 2023/5/18 13:25
- */
- @Configuration
- public class ZookeperLock1Test {
- private final String ZOOKEEPER_ADDRESS = "127.0.0.1:2181,127.0.0.1:2182,127.0.0.1:2183";
- /**
- * Description: Author:
- * yangjj_tc
- * 1、会话连接是异步的,需要自己去处理。比如使用CountDownLatch
- * 2、Watch需要重复注册,不然就不能生效
- * 3、开发的复杂性还是比较高的
- * 4、不支持多节点删除和创建。需要自己去递归
- * Date: 2023/5/18 12:48
- */
- @Bean
- public CuratorFramework getCuratorFramework(){
- // 重试策略,重试间隔时间为1秒,重试次数为3次。curator管理了zookeeper的连接,在操作zookeeper的过程中出现连接问题会自动重试。
- RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
- // 初始化客户端,通过工厂创建连接
- // zk地址 会话超时时间,默认60秒 连接超时时间,默认15秒 重试策略
- CuratorFramework zkClient = CuratorFrameworkFactory.newClient(ZOOKEEPER_ADDRESS, 5000, 15000, retryPolicy);
- // 开始连接
- zkClient.start();
- return zkClient;
- }
- public static void main(String[] args) throws InterruptedException, IOException, KeeperException {
- // 获得两个客户端
- CuratorFramework client1 = new ZookeperLock1Test().getCuratorFramework();
- CuratorFramework client2 = new ZookeperLock1Test().getCuratorFramework();
- // 可重入锁, 意味着同一个客户端在拥有锁的同时,可以多次获取,不会被阻塞。如想重入,则需要使用同一个InterProcessMutex对象。
- final InterProcessLock lock1 = new InterProcessMutex(client1, "/lockCurator");
- final InterProcessLock lock2 = new InterProcessMutex(client2, "/lockCurator");
- // 不可重入锁,区别在于该锁是不可重入的,在同一个线程中不可重入
- final InterProcessSemaphoreMutex lock3 = new InterProcessSemaphoreMutex(client1, "/lockCurator");
- final InterProcessSemaphoreMutex lock4 = new InterProcessSemaphoreMutex(client2, "/lockCurator");
- // 模拟两个线程
- new Thread(() -> {
- try {
- // 线程加锁
- lock3.acquire();
- lock3.acquire();
- System.out.println("线程1获取锁");
- // 线程沉睡
- Thread.sleep(5 * 1000);
- // 线程解锁
- lock1.release();
- System.out.println("线程1释放了锁");
- } catch (Exception e) {
- e.printStackTrace();
- }
- }).start();
- // 线程2
- new Thread(() -> {
- // 线程加锁
- try {
- lock2.acquire();
- System.out.println("线程2获取到锁");
- // 线程沉睡
- Thread.sleep(5 * 1000);
- lock2.release();
- System.out.println("线程2释放锁");
- } catch (Exception e) {
- e.printStackTrace();
- }
- }).start();
- }
- }
复制代码 9、Eureka 与 Zk 有什么区别
- Eureka采取 AP 模型,优先包管可用性,允许短暂的数据不一致。通过心跳机制和客户端缓存实现最终一致性。ZooKeeper采取 CP 模型,在分区故障时可能牺牲可用性。利用 ZAB 协议包管强一致性。
- Eureka是对等架构:Eureka 节点之间是对等的,每个节点都可以吸收注册和查询请求。客户端缓存,客户端会缓存服务注册表,纵然 Eureka 服务器不可用,客户端仍然可以获取服务信息。ZooKeeper主从架构:ZooKeeper 集群中有 Leader 和 Follower 节点,写操纵由 Leader 处理。强一致性:全部节点数据保持一致,客户端总是读取到最新数据。
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |