zk中的数据是保存在节点上的,节点就是znode,多个znode之间构成⼀颗树的⽬录结构。(像⽂件系统的⽬录)
但是,不同于树的节点,Znode 的引⽤⽅式是路径引⽤,类似于⽂件路径:
/动物/猫
/汽⻋/宝⻢
强调一句:ZooKeeper 主要是用来协调服务的,而不是用来存储业务数据的,所以不要放比较大的数据在 znode 上,ZooKeeper给出的上限是每个结点的数据大小最大是 1M。
zk中的znode,包含了四个部分:
data:保存数据
acl:权限,定义了什么样的⽤户能够操作这个节点,且能够进⾏怎样的操作。
stat:描述当前znode的元数据
child:当前节点的⼦节点
持久(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,都务必保证全局唯一。
zk的数据是运⾏在内存中,zk提供了两种持久化机制:
事务⽇志
zk把执⾏的命令以⽇志形式保存在dataLogDir指定的路径中的⽂件中(如果没有指定dataLogDir,则按dataDir指定的路径)。
数据快照
zk会在⼀定的时间间隔内做⼀次内存数据的快照,把该时刻的内存数据保存在快照⽂件中。
zk通过两种形式的持久化,在恢复时先恢复快照⽂件中的数据到内存中,再⽤⽇志⽂件中的数据做增量恢复,这样的恢复速度更快。
读锁:⼤家都可以读,要想上读锁的前提:之前的锁没有写锁
写锁:只有得到写锁的才能写。要想上写锁的前提是,之前没有任何锁
十个线程都要加入一个写锁,这时候最小节点不是自己,所有线程都监听最小节点,如果最小节点释放,则这10个线程都会收到监听,如果是上万个线程呢,这个就是羊群效应,也称惊群效应。
如果⽤上述的上锁⽅式,只要有节点发⽣变化,就会触发其他节点的监听事件,这样的话对zk的压⼒⾮常⼤,——⽺群效应。可以调整成链式监听。解决这个问题。
当这个 Znode 发⽣改变,也就是调⽤了 create , delete , setData ⽅法的时候,将会触发 Znode 上注册的对应事件,请求 Watch 的客户端会接收到异步通知。
具体交互过程如下:
ZAB协议解决了Zookeeper的崩溃恢复和主从数据同步的问题。
Looking :选举状态。
Following :Follower 节点(从节点)所处的状态。
Leading :Leader 节点(主节点)所处状态。
Observing:观察者节点所处的状态。
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 来进行排序 ,才能得到处理。
Zookeeper集群中的节点在上线时,将会进⼊到Looking状态,也就是选举Leader的状态,这个状态具体会发⽣什么?
Leader建⽴完后,Leader周期性地不断向Follower发送⼼跳(ping命令,没有内容的socket)。当Leader崩溃后,Follower发现socket通道已关闭,于是Follower开始进⼊到Looking状态,重新回到上⼀节中的Leader选举过程,此时集群不能对外提供服务。
2n 和 2n-1 的容忍度是一样的。
比如假如我们有 3 台,那么最大允许宕掉 1 台 ZooKeeper 服务器,如果我们有 4 台的的时候也同样只允许宕掉 1 台。 假如我们有 5 台,那么最大允许宕掉 2 台 ZooKeeper 服务器,如果我们有 6 台的的时候也同样只允许宕掉 2 台。
何为集群脑裂?
对于一个集群,通常多台机器会部署在不同机房,来提高这个集群的可用性。保证可用性的同时,会发生一种机房间网络线路故障,导致机房间网络不通,而集群被割裂成几个小集群。这时候子集群各自选主导致“脑裂”的情况。
过半机制是如何防止脑裂现象产生的?
ZooKeeper 的过半机制导致不可能产生 2 个 leader,因为少于等于一半是不可能产生 leader 的,这就使得不论机房的机器如何分配都不可能发生脑裂。
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如上
服务提供者 在 zookeeper 中创建一个临时节点并且将自己的 ip、port、调用方式 写入节点,当 服务消费者 需要进行调用的时候会 通过注册中心找到相应的服务的地址列表(IP端口什么的) ,并缓存到本地(方便以后调用),当消费者调用服务时,不会再去请求注册中心,而是直接通过负载均衡算法从地址列表中取一个服务提供者的服务器调用服务。
当服务提供者的某台服务器宕机或下线时(下线是通过会话机制Session实现),相应的地址会从服务提供者地址列表中移除。同时,注册中心会将新的服务地址列表发送给服务消费者的机器并缓存在消费者本机(当然你可以让消费者进行节点监听,我记得 Eureka 会先试错,然后再更新)。
首先我们看下ZooKeeper的架构图,client跟ZooKeeper集群中的某一台server保持连接,发送读/写请求,读请求直接由当前连接的server处理,写请求由于是事务请求,由当前server转发给leader进行处理。同时,client还能接收来自server端的watcher通知。
而所有的这些交互,都是基于client和ZooKeeper的server之间的TCP长连接,也称之为Session会话。ZooKeeper对外的服务端口默认是2181,客户端启动时,首先会与服务器建立一个TCP连接,从第一次连接建立开始,客户端会话的生命周期也开始了,通过这个连接,客户端能够通过心跳检测和服务器保持有效的会话,也能够向ZooKeeper服务器发送请求并接受响应,同时还能通过该连接接收来自服务器的Watch事件通知。Session的SessionTimeout
值用来设置一个客户端会话的超时时间。当由于服务器压力太大、网络故障或是客户端主动断开连接等各种原因导致客户端连接断开时,只要在SessionTimeout规定的时间内能够重新连接上集群中任意一台服务器,那么之前创建的会话仍然有效。
sessionID:会话ID,用来唯一标识一个会话,每次客户端创建会话的时候,ZooKeeper都会为其分配一个全局唯一的sessionID
TimeOut:会话超时时间,如果客户端与服务器之间因为网络闪断导致断开连接,并在TimeOut时间内未连上其他server,则此次会话失效,此次会话创建的临时节点将被清理
ExpirationTime:下次会话超时时间点。ZooKeeper会为每个会话标记一个下次会话超时时间点,便于对会话进行“分桶管理”,同时也是为了低耗的实现会话的超时检查与清理。其值接近于当前时间+TimeOut,但不完全相等,稍后会介绍。
zookeeper 的 leader 服务器再运行期间定时进行会话超时检查,时间间隔是 ExpirationInterval,单位是毫秒,默认值是 tickTime,每隔 tickTime 进行一次会话超时检查。
在 zookeeper 运行过程中,客户端会在会话超时过期范围内向服务器发送请求(包括读和写)或者 ping 请求,俗称心跳检测完成会话激活,从而来保持会话的有效性。
分以下两种情况:
只要client向server发送请求,包括读或写请求,就会触发一次激活;
如果client发现在sessionTimeOut / 3 时间内未尚和server进行任何通信,就会主动发起一次PING请求,进而触发激活;
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