【面试题】Zookeeper

1. Zookeeper内部的数据模型

1.1 zk是如何保存数据的

zk中的数据是保存在节点上的,节点就是znode,多个znode之间构成⼀颗树的⽬录结构。(像⽂件系统的⽬录)
但是,不同于树的节点,Znode 的引⽤⽅式是路径引⽤,类似于⽂件路径:

/动物//汽⻋/宝⻢

在这里插入图片描述

强调一句:ZooKeeper 主要是用来协调服务的,而不是用来存储业务数据的,所以不要放比较大的数据在 znode 上,ZooKeeper给出的上限是每个结点的数据大小最大是 1M。

1.2 zk中的znode是什么样的结构

zk中的znode,包含了四个部分:
data:保存数据
acl:权限,定义了什么样的⽤户能够操作这个节点,且能够进⾏怎样的操作。
stat:描述当前znode的元数据
child:当前节点的⼦节点

1.3 zk中节点znode的类型

持久(PERSISTENT)节点 :一旦创建就一直存在即使 ZooKeeper 集群宕机,直到将其删除。
临时(EPHEMERAL)节点 :临时节点的生命周期是与 客户端会话(session) 绑定的,会话消失则节点消失 。并且,临时节点只能做叶子节点 ,不能创建子节点。
持久顺序(PERSISTENT_SEQUENTIAL)节点 :除了具有持久(PERSISTENT)节点的特性之外, 子节点的名称还具有顺序性。比如 /node1/app0000000001 、/node1/app0000000002 。
临时顺序(EPHEMERAL_SEQUENTIAL)节点 :除了具备临时(EPHEMERAL)节点的特性之外,子节点的名称还具有顺序性。
Container节点(3.5.3版本新增):Container容器节点,当容器中没有任何⼦节点,该容器节点会被zk定期删除(60s)。
在这里插入图片描述
在为客户端创建会话之前,服务端首先会为每个客户端都分配一个 sessionID。由于 sessionID是 ZooKeeper 会话的一个重要标识,许多与会话相关的运行机制都是基于这个 sessionID 的,因此,无论是哪台服务器为客户端分配的 sessionID,都务必保证全局唯一。

1.4 数据的持久化

zk的数据是运⾏在内存中,zk提供了两种持久化机制:
事务⽇志
zk把执⾏的命令以⽇志形式保存在dataLogDir指定的路径中的⽂件中(如果没有指定dataLogDir,则按dataDir指定的路径)。
数据快照
zk会在⼀定的时间间隔内做⼀次内存数据的快照,把该时刻的内存数据保存在快照⽂件中。

zk通过两种形式的持久化,在恢复时先恢复快照⽂件中的数据到内存中,再⽤⽇志⽂件中的数据做增量恢复,这样的恢复速度更快。

2. zk中的锁

2.1 zk中锁的种类

读锁:⼤家都可以读,要想上读锁的前提:之前的锁没有写锁
写锁:只有得到写锁的才能写。要想上写锁的前提是,之前没有任何锁

2.2 zk如何上读锁

  1. 创建⼀个临时序号节点,节点的数据是read,表示是读锁
  2. 获取当前zk中序号⽐⾃⼰⼩的所有节点
  3. 判断最⼩节点是否是读锁:
    如果不是读锁的话,则上锁失败,为最⼩节点设置监听。阻塞等待,zk的watch机制会当最⼩节点发⽣变化时通知当前节点,于是再执⾏第⼆步的流程
    如果是读锁的话,则上锁成功(最小节点假如是读锁,后面不可能会有写锁)
    在这里插入图片描述

2.3 zk如何上写锁

  1. 创建⼀个临时序号节点,节点的数据是write,表示是 写锁
  2. 获取zk中所有的⼦节点
  3. 判断⾃⼰是否是最⼩的节点:
    如果是,则上写锁成功。(写锁之前不能有任何锁)
    如果不是,说明前⾯还有锁,则上锁失败,监听最⼩的节点,如果最⼩节点有变化,则回到第⼆步。
    在这里插入图片描述

2.4 ⽺群效应(分布式锁)

十个线程都要加入一个写锁,这时候最小节点不是自己,所有线程都监听最小节点,如果最小节点释放,则这10个线程都会收到监听,如果是上万个线程呢,这个就是羊群效应,也称惊群效应。

如果⽤上述的上锁⽅式,只要有节点发⽣变化,就会触发其他节点的监听事件,这样的话对zk的压⼒⾮常⼤,——⽺群效应。可以调整成链式监听。解决这个问题。
在这里插入图片描述

3. zk的watch机制

当这个 Znode 发⽣改变,也就是调⽤了 create , delete , setData ⽅法的时候,将会触发 Znode 上注册的对应事件,请求 Watch 的客户端会接收到异步通知。
具体交互过程如下:
在这里插入图片描述
在这里插入图片描述

4. ZAB协议

ZAB协议解决了Zookeeper的崩溃恢复和主从数据同步的问题。

4.1 ZAB协议定义的四种节点状态

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

4.2 广播模式

Zookeeper 使用单一的主进程 Leader 来接收和处理客户端所有事务请求,并采用 ZAB 协议的原子广播协议,将事务请求以 Proposal 提议广播到所有 Follower 节点,当集群中有过半的Follower 服务器进行正确的 ACK 反馈,那么Leader就会再次向所有的 Follower 服务器发送commit 消息,将此次提案进行提交。这个过程可以简称为 2pc 事务提交,整个流程可以参考下图,注意 Observer 节点只负责同步 Leader 数据,不参与 2PC 数据同步过程。
在这里插入图片描述
在这里插入图片描述
ZAB 需要让 Follower 和 Observer 保证顺序性 。何为顺序性,比如我现在有一个写请求A,此时 Leader 将请求A广播出去,因为只需要半数同意就行,所以可能这个时候有一个 Follower F1因为网络原因没有收到,而 Leader 又广播了一个请求B,因为网络原因,F1竟然先收到了请求B然后才收到了请求A,这个时候请求处理的顺序不同就会导致数据的不同,从而 产生数据不一致问题 。

所以在 Leader 这端,它为每个其他的 zkServer 准备了一个 队列 ,采用先进先出的方式发送消息。由于协议是通过 TCP来进行网络通信的,保证了消息的发送顺序性,接受顺序性也得到了保证。

除此之外,在 ZAB 中还定义了一个 全局单调递增的事务ID ZXID ,它是一个64位long型,其中高32位表示 epoch 年代,低32位表示事务id。epoch 是会根据 Leader 的变化而变化的,当一个 Leader 挂了,新的 Leader 上位的时候,年代(epoch)就变了。而低32位可以简单理解为递增的事务id。
定义这个的原因也是为了顺序性,每个 proposal 在 Leader 中生成后需要 通过其 ZXID 来进行排序 ,才能得到处理。

4.3 崩溃恢复

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

5. 集群

5.1 集群角色( Leader、Follower 和 Observer)

5.2 Leader选举过程

介绍zk的选举流程之前需要先解释两个概念:zxid以及myid。zxid指的是当前节点的事物id,通俗点说就是当前节点完成的数据同步情况,该值越大,越能说明该节点的数据同步情况越完整,丢失数据的情况越小或者丢失数据越少。myid是在创建zk集群的时候,我们给它的赋值。
zk的follower节点和leader节点是通过心跳,来查看服务是否可用。在这其中,只要有有一台follower节点发现主节点挂掉,他就开始向其它follower节点发送选主请求,整个集群进入选主流程,不再向外提供服务。

先假设现在有4个zk节点,分别为node1,node2,node3,node4,他们的myid分别为1,2,3,4选主流程主要分为以下两种情况:

  1. 初始启动,在启动阶段时,此时各个服务节点的zxid都为0,只与myid有关。假设启动顺序为node1->node2->node3->node4,当启动动1和2的时候,该zk集群是不可用状态,因为zk的选主必须是过半服务节点同意(包含自己),最低需要启动三个节点才可以进行选举,因此只有node1和node2启动的时候,此时只有两台服务,不满足条件,当第三台节点启动以后,才满足了选主的最低条件,然后进入到选举流程,因为node3的myid最大,所以此时3号节点为leader,然后启动node4,由于此时已经选举出3位leader节点并且过半通过,则不再选取新的主节点。则该集群的leader节点为node3。

  2. 运行过程中,初始启动过程中的leader(node3)节点挂掉,假设此时只有node4节点发现leader已经挂掉,node1和node2的Zxid都是10,node4的Zxid为9,选主的时候需要比较zxid和myid,需要注意他们的优先级,zxid为第一优先级,myid为第二优先级,选举流程大致分为以下几步:

2.1 node4节点给自己投票,然后将自己的zxid和myid发送给node1和node2节点:

在这里插入图片描述

2.2 node1和node2通过比较zxid和myid,发现node4不能成为leader节点,将各自的zxid和myid发送给node4,然后node4接收到以后,发现node1和node2都比自己适合成为leader节点,会给它们进行投票.

在这里插入图片描述

2.3 node1和node2反驳完node4的选主请求以后,开始进行各自的选主流程,起过程与node4的过程一致,通过上面的优先级,我们可以知道最终node2会成为leader节点(优先zxid大的,zxid相同的情况下,优先myid大的),那么以node2为例说一下接下来的流程。node2首先给自己投票,然后将自己zxid和myid推送给node1和node4,此时会发现node2适合成为主节点,则会给node2节点进行投票,最终选出node2成为主节点,zk集群恢复成可用状态。

5.3 崩溃恢复时的Leader选举

Leader建⽴完后,Leader周期性地不断向Follower发送⼼跳(ping命令,没有内容的socket)。当Leader崩溃后,Follower发现socket通道已关闭,于是Follower开始进⼊到Looking状态,重新回到上⼀节中的Leader选举过程,此时集群不能对外提供服务。

5.4 ZooKeeper 集群为啥最好奇数台?

2n 和 2n-1 的容忍度是一样的。
比如假如我们有 3 台,那么最大允许宕掉 1 台 ZooKeeper 服务器,如果我们有 4 台的的时候也同样只允许宕掉 1 台。 假如我们有 5 台,那么最大允许宕掉 2 台 ZooKeeper 服务器,如果我们有 6 台的的时候也同样只允许宕掉 2 台。

5.5 ZooKeeper 选举的过半机制防止脑裂

何为集群脑裂?
对于一个集群,通常多台机器会部署在不同机房,来提高这个集群的可用性。保证可用性的同时,会发生一种机房间网络线路故障,导致机房间网络不通,而集群被割裂成几个小集群。这时候子集群各自选主导致“脑裂”的情况。
过半机制是如何防止脑裂现象产生的?
ZooKeeper 的过半机制导致不可能产生 2 个 leader,因为少于等于一半是不可能产生 leader 的,这就使得不论机房的机器如何分配都不可能发生脑裂。

5.6 主从服务器之间的数据同步

在这里插入图片描述

7. 分布式锁的实现原理(写锁)

zk 中不需要向 redis 那样考虑锁得不到释放的问题了,因为当客户端挂了,节点也挂了,锁也释放了。
系统A、B、C都去访问/locks节点,访问的时候会创建带顺序号的临时/短暂(EPHEMERAL_SEQUENTIAL)节点,比如,系统A创建了id_000000节点,系统B创建了id_000002节点,系统C创建了id_000001节点。
在这里插入图片描述

接着,拿到/locks节点下的所有子节点(id_000000,id_000001,id_000002),判断自己创建的是不是最小的那个节点
如果是,则拿到锁。
释放锁:执行完操作后,把创建的节点给删掉
如果不是,则监听比自己要小1的节点变化
举个例子:
系统A拿到/locks节点下的所有子节点,经过比较,发现自己(id_000000),是所有子节点最小的。所以得到锁
系统B拿到/locks节点下的所有子节点,经过比较,发现自己(id_000002),不是所有子节点最小的。所以监听比自己小1的节点id_000001的状态
系统C拿到/locks节点下的所有子节点,经过比较,发现自己(id_000001),不是所有子节点最小的。所以监听比自己小1的节点id_000000的状态
……
等到系统A执行完操作以后,将自己创建的节点删除(id_000000)。通过监听,系统C发现id_000000节点已经删除了,发现自己已经是最小的节点了,于是顺利拿到锁
….系统B如上

8. 集群管理和注册中心

服务提供者 在 zookeeper 中创建一个临时节点并且将自己的 ip、port、调用方式 写入节点,当 服务消费者 需要进行调用的时候会 通过注册中心找到相应的服务的地址列表(IP端口什么的) ,并缓存到本地(方便以后调用),当消费者调用服务时,不会再去请求注册中心,而是直接通过负载均衡算法从地址列表中取一个服务提供者的服务器调用服务。
在这里插入图片描述
当服务提供者的某台服务器宕机或下线时(下线是通过会话机制Session实现),相应的地址会从服务提供者地址列表中移除。同时,注册中心会将新的服务地址列表发送给服务消费者的机器并缓存在消费者本机(当然你可以让消费者进行节点监听,我记得 Eureka 会先试错,然后再更新)。
在这里插入图片描述

8.1. 会话机制(服务注册与发现)

8.1.1 为什么会有会话机制Session

在这里插入图片描述
首先我们看下ZooKeeper的架构图,client跟ZooKeeper集群中的某一台server保持连接,发送读/写请求,读请求直接由当前连接的server处理,写请求由于是事务请求,由当前server转发给leader进行处理。同时,client还能接收来自server端的watcher通知。

而所有的这些交互,都是基于client和ZooKeeper的server之间的TCP长连接,也称之为Session会话。ZooKeeper对外的服务端口默认是2181,客户端启动时,首先会与服务器建立一个TCP连接,从第一次连接建立开始,客户端会话的生命周期也开始了,通过这个连接,客户端能够通过心跳检测和服务器保持有效的会话,也能够向ZooKeeper服务器发送请求并接受响应,同时还能通过该连接接收来自服务器的Watch事件通知。Session的SessionTimeout值用来设置一个客户端会话的超时时间。当由于服务器压力太大、网络故障或是客户端主动断开连接等各种原因导致客户端连接断开时,只要在SessionTimeout规定的时间内能够重新连接上集群中任意一台服务器,那么之前创建的会话仍然有效。

8.1.2 SessionID的初始化

sessionID:会话ID,用来唯一标识一个会话,每次客户端创建会话的时候,ZooKeeper都会为其分配一个全局唯一的sessionID
TimeOut:会话超时时间,如果客户端与服务器之间因为网络闪断导致断开连接,并在TimeOut时间内未连上其他server,则此次会话失效,此次会话创建的临时节点将被清理
ExpirationTime:下次会话超时时间点。ZooKeeper会为每个会话标记一个下次会话超时时间点,便于对会话进行“分桶管理”,同时也是为了低耗的实现会话的超时检查与清理。其值接近于当前时间+TimeOut,但不完全相等,稍后会介绍。

8.1.3 会话超时管理(分桶策略+会话激活)

zookeeper 的 leader 服务器再运行期间定时进行会话超时检查,时间间隔是 ExpirationInterval,单位是毫秒,默认值是 tickTime,每隔 tickTime 进行一次会话超时检查。
在这里插入图片描述

在 zookeeper 运行过程中,客户端会在会话超时过期范围内向服务器发送请求(包括读和写)或者 ping 请求,俗称心跳检测完成会话激活,从而来保持会话的有效性。
在这里插入图片描述
在这里插入图片描述

8.1.4 会话何时激活

分以下两种情况:
只要client向server发送请求,包括读或写请求,就会触发一次激活;
如果client发现在sessionTimeOut / 3 时间内未尚和server进行任何通信,就会主动发起一次PING请求,进而触发激活;

ZK选主过程

session会话管理:https://www.runoob.com/w3cnote/zookeeper-session.html
https://blog.csdn.net/MuErHuoXu/article/details/86218115
https://snailclimb.gitee.io/javaguide/#/?id=rpc
视频链接:https://www.bilibili.com/video/BV1to4y1C7gw?p=32&vd_source=b901ef0e9ed712b24882863596eab0ca
ZAB协议视频链接:https://www.bilibili.com/video/BV1to4y1C7gw?p=32&vd_source=b901ef0e9ed712b24882863596eab0ca

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值