(ROOT)zookeeper

CAP理论

CAP理论是分布式领域中非常重要的一个指导理论,C(Consistency)表示强一致性,A (Availability)表示可用性,P(Partition Tolerance)表示分区容错性,CAP理论指出在目前的硬件 条件下,一个分布式系统是必须要保证分区容错性的,而在这个前提下,分布式系统要么保证CP,要么 保证AP,无法同时保证CAP。
分区容错性表示,一个系统虽然是分布式的,但是对外看上去应该是一个整体,不能由于分布式系统内 部的某个结点挂点,或网络出现了故障,而导致系统对外出现异常。所以,对于分布式系统而言是一定 要保证分区容错性的。
强一致性表示,一个分布式系统中各个结点之间能及时的同步数据,在数据同步过程中,是不能对外提 供服务的,不然就会造成数据不一致,所以强一致性和可用性是不能同时满足的。
可用性表示,一个分布式系统对外要保证可用。

BASE理论

由于不能同时满足CAP,所以出现了BASE理论: 1. BA:Basically Available,表示基本可用,表示可以允许一定程度的不可用,比如由于系统故障, 请求时间变长,或者由于系统故障导致部分非核心功能不可用,都是允许的
2. S:Soft state:表示分布式系统可以处于一种中间状态,比如数据正在同步 3. E:Eventually consistent,表示最终一致性,不要求分布式系统数据实时达到一致,允许在经过一 段时间后再达到一致,在达到一致过程中,系统也是可用的

说说 Zookeeper 是什么?

直译:从名字上直译就是动物管理员,动物指的是 Hadoop 一类的分布式软件,管理员三个字体现
了 ZooKeeper 的特点:维护、协调、管理、监控。ZooKeeper提供了一个类似于Linux文件系统的树形结构,同时提供了对于每个节点的监控与通知机制。

ZooKeeper 是一个典型的分布式数据一致性解决方案,分布式应用程序可以基于 ZooKeeper 实现诸如数据发布/订阅负载均衡命名服务分布式协调/通知集群管理Master 选举分布式锁分布式队列等功能。

ZooKeeper 一个最常用的使用场景就是用于担任服务生产者和服务消费者的注册中心。

服务生产者将自己提供的服务注册到 ZooKeeper 中心,服务的消费者在进行服务调用的时候先到 ZooKeeper 中查找服务,获取到服务生产者的详细信息之后,再去调用服务生产者的内容与数据。

如下图所示,在 Dubbo 架构中 ZooKeeper 就担任了注册中心这一角色。

zk重要概念总结

关于 ZooKeeper 的一些重要概念:

  • ZooKeeper 本身就是一个分布式程序(只要半数以上节点存活,ZooKeeper 就能正常服务)。
  • 为了保证高可用,最好是以集群形态来部署 ZooKeeper,这样只要集群中大部分机器是可用的(能够容忍一定的机器故障),那么 ZooKeeper 本身仍然是可用的。
  • ZooKeeper 将数据保存在内存中,这也就保证了 高吞吐量和低延迟(但是内存限制了能够存储的容量不太大,此限制也是保持 Znode 中存储的数据量较小的进一步原因)。
  • ZooKeeper 是高性能的。在“读”多于“写”的应用程序中尤其地高性能,因为“写”会导致所有的服务器间同步状态。(“读”多于“写”是协调服务的典型场景。)
  • ZooKeeper 有临时节点的概念。当创建临时节点的客户端会话一直保持活动,瞬时节点就一直存在。
    而当会话终结时,瞬时节点被删除。持久节点是指一旦这个 ZNode 被创建了,除非主动进行 ZNode 的移除操作,否则这个 ZNode 将一直保存在 Zookeeper 上。
  • ZooKeeper 底层其实只提供了两个功能:①管理(存储、读取)用户程序提交的数据;②为用户程序提交数据节点监听服务。

下面关于会话(Session)、 Znode、版本、Watcher、ACL 概念的总结都在《从 Paxos 到 ZooKeeper 》第四章第一节以及第七章第八节有提到,感兴趣的可以看看!

会话(Session)

Session 指的是 ZooKeeper 服务器与客户端会话。在 ZooKeeper 中,一个客户端连接是指客户端和服务器之间的一个 TCP 长连接。

客户端启动的时候,首先会与服务器建立一个 TCP 连接,从第一次连接建立开始,客户端会话的生命周期也开始了。

通过这个连接,客户端能够通过心跳检测与服务器保持有效的会话,也能够向 Zookeeper 服务器发送请求并接受响应,同时还能够通过该连接接收来自服务器的 Watch 事件通知。

Session 的 sessionTimeout 值用来设置一个客户端会话的超时时间。

当由于服务器压力太大、网络故障或是客户端主动断开连接等各种原因导致客户端连接断开时,只要在 sessionTimeout 规定的时间内能够重新连接上集群中任意一台服务器,那么之前创建的会话仍然有效。

在为客户端创建会话之前,服务端首先会为每个客户端都分配一个 sessionID。

由于 sessionID 是 Zookeeper 会话的一个重要标识,许多与会话相关的运行机制都是基于这个 sessionID 的。

因此,无论是哪台服务器为客户端分配的 sessionID,都务必保证全局唯一。

Znode

在谈到分布式的时候,我们通常说的“节点"是指组成集群的每一台机器。

然而,在 ZooKeeper 中,“节点"分为两类:

  • 第一类同样是指构成集群的机器,我们称之为机器节点。
  • 第二类则是指数据模型中的数据单元,我们称之为数据节点一ZNode。

ZooKeeper 将所有数据存储在内存中,数据模型是一棵树(Znode Tree),由斜杠(/)的进行分割的路径,就是一个 Znode,例如/foo/path1。每个上都会保存自己的数据内容,同时还会保存一系列属性信息。

在 Zookeeper 中,Node 可以分为持久节点和临时节点两类。所谓持久节点是指一旦这个 ZNode 被创建了,除非主动进行 ZNode 的移除操作,否则这个 ZNode 将一直保存在 ZooKeeper 上。

而临时节点就不一样了,它的生命周期和客户端会话绑定,一旦客户端会话失效,那么这个客户端创建的所有临时节点都会被移除。

另外,ZooKeeper 还允许用户为每个节点添加一个特殊的属性:SEQUENTIAL。

一旦节点被标记上这个属性,那么在这个节点被创建的时候,ZooKeeper 会自动在其节点名后面追加上一个整型数字,这个整型数字是一个由父节点维护的自增数字。

版本

在前面我们已经提到,Zookeeper 的每个 ZNode 上都会存储数据,对应于每个 ZNode,Zookeeper 都会为其维护一个叫作 Stat 的数据结构。

Stat 中记录了这个 ZNode 的三个数据版本,分别是:

  • version(当前 ZNode 的版本)
  • cversion(当前 ZNode 子节点的版本)
  • aversion(当前 ZNode 的 ACL 版本)

Watcher

Watcher(事件监听器),是 ZooKeeper 中的一个很重要的特性。

ZooKeeper 允许用户在指定节点上注册一些 Watcher,并且在一些特定事件触发的时候,ZooKeeper 服务端会将事件通知到感兴趣的客户端上去,该机制是 ZooKeeper 实现分布式协调服务的重要特性。

ACL

ZooKeeper 采用 ACL(AccessControlLists)策略来进行权限控制,类似于 UNIX 文件系统的权限控制。

权限模式(Scheme)

(1)IP:从 IP 地址粒度进行权限控制

(2)Digest:最常用,用类似于 username:password 的权限标识来进行权限配置,便于区分不同应用来进行权限控制

(3)World:最开放的权限控制方式,是一种特殊的 digest 模式,只有一个权限标识“world:anyone”

(4)Super:超级用户
————————————————
版权声明:本文为CSDN博主「Happy编程」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/wanghaiping1993/article/details/125396988

ZooKeeper 定义了 5 种权限,如下图:

其中尤其需要注意的是,CREATE 和 DELETE 这两种权限都是针对子节点的权限控制。

数据一致性模型有哪些 

  • 强一致性:当更新操作完成之后,任何多个后续进程的访问都会返回最新的更新过的值,这种是对 用户 最友好的,就是用户上一次写什么,下一次就保证能读到什么。根据 CAP理论,这种实现需 要牺牲可用性。
  • 弱一致性:系统在数据写入成功之后,不承诺立即可以读到最新写入的值,也不会具体的承诺多久 之后 可以读到。用户读到某一操作对系统数据的更新需要一段时间,我们称这段时间为“不一致性 窗口”。
  • 最终一致性:最终一致性是弱一致性的特例,强调的是所有的数据副本,在经过一段时间的同步之 后, 最终都能够达到一个一致的状态。因此,最终一致性的本质是需要系统保证最终数据能够达到 一致,而 不需要实时保证系统数据的强一致性。到达最终一致性的时间 ,就是不一致窗口时间, 在没有故障发生的前提下,不一致窗口的时间主要受通信延迟,系统负载和复制副本的个数影响。 最终一致性模型根据其提供的不同保证可以划分为更多的模型,包括因果一致性和会话一致性等。

zookeeper特点

  • 最终一致性:客户端看到的数据最终是一致的。
  • 实时性:ZooKeeper 不能保证两个客户端同时得到刚更新的数据。
  • 独立性(等待无关):不同客户端直接互不影响。
  • 原子性:更新要不成功要不失败,没有第三个状态。
  • 顺序一致性:从同一客户端发起的事务请求,最终将会严格地按照顺序被应用到 ZooKeeper 中去。
  • 单一系统映像:无论客户端连到哪一个 ZooKeeper 服务器上,其看到的服务端数据模型都是一致的。
  • 可靠性:一旦一次更改请求被应用,更改的结果就会被持久化,直到被下一次更改覆盖。

zk存储结构

zookeeper中的数据是存储在内存当中的,因此它的效率十分高效。它内部的存储方式十分类似于文件存储结构,采用了分层存储结构。但是它和文件存储结构的区别是,它的各个节点中是允许存储数据的,需要注意的是zk的每个节点存储数据不能超过1M。它的内存数据结果如下图:

zk API

 zk只提供了几个简单的api,但是我们可以通过灵活使用这些api的组合,来实现我们复杂的业务要求:

        1)create:创建一个新节点,通过指定路径的方式创建节点,例如创建路径为/A/A1/demo,则会在A1节点下创建一个demo节点;

        2)delete:删除节点,通过路径的方式删除节点,如果删除路径为/A/A1/demo,则会删除A1节点下的demo节点;

        3)exists:判断指定路径下的节点是否存在,例如判断路径为/A/A1/demo,则会判断A1节点下的demo节点是否存在;

        4)get:获取指定路径下某个节点的值是什么,例如获取路径为/A/A1/demo,则会获取A1节点下的demo节点的值什么;

        5)set:为指定路径的节点进行赋值操作,例如修改路径为/A/A1/demo,则会修改A1节点下的demo节点的值;

        6)get children:获取指定路径节点下的子节点信息,例如获取路径为/A,则会获取A节点下的A1和A2节点;

        7)sync:获取到同步数据,这个涉及到了zk的原理,zk集群属于最终一致性,调用该方法,可以获取到最终的结果值,如果不使用该方法,在查询的时候可能获取到的值是中间值;

永久性节点和临时性节点

  zk中创建的节点分为两种:永久性节点和临时性节点。永久性节点即创建以后,在不执行delete命令的前提下,该节点是永久存在的;而临时节点与session有关,每个客户端与zk建立链接的时候会生成一个session,这个session不会因为链接zk服务器节点的变化而变化,只有当客户端断开连接以后,该session才会消失,而临时节点会随着session的消失而消失。

角色

 首先来看一下zk的集群模式,如下图:

follower的写请求会交给leader处理

paxos -> zab

Paxos 算法可以说是 ZooKeeper 的灵魂了。但是,ZooKeeper 并没有完全采用 Paxos 算法 ,而是使用 ZAB 协议作为其保证数据一致性的核心算法。

另外,在 ZooKeeper 的官方文档中也指出,ZAB 协议并不像 Paxos 算法那样,是一种通用的分布式一致性算法,它是一种特别为 ZooKeeper 设计的崩溃可恢复的原子消息广播算法。

Paoxs协议


问题背景:假设我们有下图的系统,想要在server1,server2,server3选一个master。


prepare阶段 
  1. 每个server向proposer发送消息,表示自己要当leader,假设proposer收到消息的时间不一样,顺序是: proposer2 -> proposer1 -> proposer3,消息编号依次为1、2、3。 
  紧接着,proposer将消息发给acceptor中超过半数的子成员(这里选择两个),如图所示,proposer2向acceptor2和acceptor3发送编号为1的消息,proposer1向acceptor1和accepto2发送编号为2的消息,proposer3向acceptor2和acceptor3发送编号为3的消息。 
   2. 假设这时proposer1发送的消息先到达acceptor1和acceptor2,它们都没有接收过请求,所以接收该请求并返回【pok,null,null】给proposer1,同时acceptor1和acceptor2承诺不再接受编号小于2的请求; 
  紧接着,proposer2的消息到达acceptor2和acceptor3,acceptor3没有接受过请求,所以返回proposer2 【pok,null,null】,acceptor3并承诺不再接受编号小于1的消息。而acceptor2已经接受proposer1的请求并承诺不再接收编号小于2的请求,所以acceptor2拒绝proposer2的请求; 
  最后,proposer3的消息到达acceptor2和acceptor3,它们都接受过提议,但编号3的消息大于acceptor2已接受的2和acceptor3已接受的1,所以他们都接受该提议,并返回proposer3 【pok,null,null】; 
  此时,proposer2没有收到过半的回复,所以重新取得编号4,并发送给acceptor2和acceptor3,此时编号4大于它们已接受的提案编号3,所以接受该提案,并返回proposer2 【pok,null,null】。

accept阶段 

  Proposer3收到半数以上(两个)的回复,并且返回的value为null,所以,proposer3提交了【3,server3】的提案。 
  Proposer1也收到过半回复,返回的value为null,所以proposer1提交了【2,server1】的提案。 
  Proposer2也收到过半回复,返回的value为null,所以proposer2提交了【4,server2】的提案。 

   (这里要注意,并不是所有的proposer都达到过半了才进行第二阶段,这里只是一种特殊情况)

  Acceptor1和acceptor2接收到proposer1的提案【2,server1】,acceptor1通过该请求,acceptor2承诺不再接受编号小于4的提案,所以拒绝; 
  Acceptor2和acceptor3接收到proposer2的提案【4,server2】,都通过该提案; 
  Acceptor2和acceptor3接收到proposer3的提案【3,server3】,它们都承诺不再接受编号小于4的提案,所以都拒绝。

所以proposer1和proposer3会再次进入第一阶段,但这时候 Acceptor2和acceptor3已经通过了提案(AcceptN = 4,AcceptV=server2),并达成了多数,所以proposer会递增提案编号,并最终改变其值为server2。最后所有的proposer都肯定会达成一致,这就迅速的达成了一致。
原文链接:https://blog.csdn.net/u013679744/article/details/79222103

分布式共识算法之Paxos图解 - 简书 (jianshu.com)

存在问题:

活锁问题:假如提案者1因为不能获得过半响应而将自己的编号变为maxN+1=3,而此时提案者2的提案号是2<3,因此也会将自己的编号maxN+1=4,提案者1发现自己的3 < 4,将maxN + 1 = 5 。。。 造成活锁问题

ZAB 协议介绍

ZAB(ZooKeeper Atomic Broadcast 原子广播)协议是为分布式协调服务 ZooKeeper 专门设计的一种支持崩溃恢复的原子广播协议。

在 ZooKeeper 中,主要依赖 ZAB 协议来实现分布式数据一致性,基于该协议,ZooKeeper 实现了一种主备模式的系统架构来保持集群中各个副本之间的数据一致性。

三个阶段

Zab协议要求每个 Leader 都要经历三个阶段:发现,同步,广播

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

  2. 同步:Leader 要负责将本身的数据与 Follower 完成同步,做到多副本存储。这样也是提现了CAP中的高可用和分区容错。Follower将队列中未处理完的请求消费完成后,写入本地事务日志中。

  3. 广播:Leader 可以接受客户端新的事务Proposal请求,将新的Proposal请求广播给所有的 Follower。

两种基本模式

崩溃恢复(选主)和消息广播(同步)

当整个服务框架在启动过程中,或是当 Leader 服务器出现网络中断、崩溃退出与重启等异常情况时或与半数 follower失去同步(syncLimit),ZAB 协议就会进入崩溃恢复模式并选举产生新的 Leader 服务器

当选举产生了新的 Leader 服务器,同时集群中已经有过半的机器与该 Leader 服务器完成了状态同步之后,ZAB 协议就会退出恢复模式

其中,所谓的状态同步是指数据同步,用来保证集群中存在过半的机器能够和 Leader 服务器的数据状态保持一致

当集群中已经有过半的 Follower 服务器完成了和 Leader 服务器的状态同步,那么整个服务框架就可以进人消息广播模式了。

当一台同样遵守 ZAB 协议的服务器启动后加入到集群中时,如果此时集群中已经存在一个 Leader 服务器在负责进行消息广播。

那么新加入的服务器就会自觉地进人数据恢复模式:找到 Leader 所在的服务器,并与其进行数据同步,然后一起参与到消息广播流程中去。

正如上文介绍中所说的,ZooKeeper 设计成只允许唯一的一个 Leader 服务器来进行事务请求的处理。

leader 服务器在接收到客户端的事务请求后,会生成对应的事务提案并发起一轮广播协议

而如果集群中的其他机器接收到客户端的事务请求,那么这些非 Leader 服务器会首先将这个事务请求转发给 Leader 服务器

注:过半机制用于防止脑裂

崩溃恢复

崩溃恢复的主要任务就是选举Leader(Leader Election)Leader选举分两个场景: Zookeeper服务器启动时Leader选举。 Zookeeper集群运行过程中Leader崩溃后的Leader选举

Zab 协议崩溃恢复要求满足以下两个要求

(如何解决见下面)

1)确保已经被 Leader 提交的 Proposal 必须最终被所有的 Follower 服务器提交

2)确保丢弃已经被 Leader 提出的但是没有被提交的 Proposal

zxid

全局一致性事务id,具有顺序性。zk通过zxid保证事务顺序一致性

通俗点说就是当前节点完成的数据同步情况,该值越大,越能说明该节点的数据同步情况越完整,丢失数据的情况越小或者丢失数据越少。

 zxid 实际上是一个 64 位数字。高 32 位是 epoch 用来标识 Leader 是否发生了改变,如果有新的Leader 产生出来, epoch 会自增。 低 32 位用来递增计数。

  • 低32位是一个简单的单调递增的计数器。针对客户端的每一个事务请求,所有的 proposal (提议)都在被提出的时候加上了zxid,都会对该计数器进行加1操作。当新产生的 proposal 的时候,会依据数据库的两阶段过程,首先会向其他的 Server 发出事务执行请求,如果超过半数的机器都能执行并且能够成功,那么就会开始执行。
  • 高32位是 leader 周期纪元的编号(epoch)。每当选举产生一个新的 leader,就会从这个 leader 的本地日志中,取出max_zxid,并从该 zxid 中解析出对应的纪元值,对其进行加1操作后,以此编号作为新的纪元值。此时,低 32 位会被置零

myid

是在创建zk集群的时候,我们给它的赋值。

逻辑时钟

  • 用于标识投票的轮数
  • 当第一次选举没有选出 Leader 时,会发起第二轮选举

4种选举状态

竞选 Leader 时,发送到集群的信息中所包含的数据

Looking:正在竞选状态(集群刚启动时选举/崩溃恢复开始时)
Following:跟随态,同步 leader 的状态,参与投票
Observing:观察态,同步 leader 的状态,不参与投票,集群较大时会出现,当集群较大时,并非所有的节点都参与选举,全部节点都参与选举会效率较低且消耗资源
Leading:领导者

1、选举阶段(Leader Election)

节点在一开始都处于选举节点(状态为LOOKING),只要有一个节点得到超过半数节点的票数,它就可以当选准 Leader,只有到达第三个阶段(也就是同步阶段),这个准 Leader 才会成为真正的 Leader。

Zookeeper 规定所有有效的投票都必须在同一个 轮次 中,每个服务器在开始新一轮投票时,都会对自己维护的 logicalClock (轮次)进行自增操作

每个服务器在广播自己的选票前,会将自己的投票箱(recvset)清空。该投票箱记录了所受到的选票。

例如:Server_2 投票给 Server_3,Server_3 投票给 Server_1,则Server_1的投票箱为(2,3)、(3,1)、(1,1)。

前一个数字表示投票者,后一个数字表示被选举者。票箱中只会记录每一个投票者的最后一次投票记录,如果投票者更新自己的选票,则其他服务器收到该新选票后会在自己的票箱中更新该服务器的选票。

这一阶段的目的就是为了选出一个准 Leader ,然后进入下一个阶段。

协议并没有规定详细的选举算法,实现中使用Fast Leader Election

2、发现阶段(Descovery)

在这个阶段,Followers 和上一轮选举出的准 Leader 进行通信,同步 Followers 最近接收的事务 Proposal 。

一个 Follower 只会连接一个 Leader,如果一个 Follower 节点认为另一个 Follower 节点为leader,则会在尝试连接时被拒绝。被拒绝之后,该节点就会进入 Leader Election阶段。

这个阶段的主要目的是发现当前大多数节点接收的最新 Proposal,并且准 Leader 生成新的 epoch ,让 Followers 接收,更新它们的 acceptedEpoch

3、同步阶段(Synchronization)

同步阶段主要是利用 Leader 前一阶段获得的最新 Proposal 历史,同步集群中所有的副本

只有当 quorum(超过半数的节点) 都同步完成,准 Leader 才会成为真正的 Leader。Follower 只会接收 zxid 比自己 lastZxid 大的 Proposal。

Fast Leader Election(快速选举)

前面提到的 FLE 会选举拥有最新Proposal history (lastZxid最大)的节点作为 Leader,这样就省去了发现最新提议的步骤。这是基于拥有最新提议的节点也拥有最新的提交记录

成为 Leader 的条件:

1.1)选 epoch 最大的(zxid前32位)

1.2)若 epoch 相等,选 zxid 最大的(zxid后32位)

2)若 epoch 和 zxid 相等,选择 server_id 最大的(zoo.cfg中的myid)

协议实现 Fast Leader Election(FLE)

协议的 Java 版本实现跟上面的定义略有不同,选举阶段使用的是 Fast Leader Election(FLE)。(在3.4.0版本后只保留了这个TCP版本的FLE算法)

实际的实现将发现和同步阶段合并为 Recovery Phase(恢复阶段),所以,Zab 的实现实际上有三个阶段。

Zab协议三个阶段:

1)选举(Fast Leader Election)

2)恢复(Recovery Phase)

3)广播(Broadcast Phase)

节点在选举开始时,都默认投票给自己,当接收其他节点的选票时,会根据上面的Leader条件判断并且更改自己的选票,然后重新发送选票给其他节点。当有一个节点的得票超过半数,该节点会设置自己的状态为 Leading ,其他节点会设置自己的状态为 Following

Recovery Phase(恢复阶段)

这一阶段 Follower 发送他们的 lastZxid 给 Leader,Leader 根据 lastZxid 决定如何同步数据。这里的实现跟前面的 Phase 2 有所不同:Follower 收到 TRUNC 指令会终止 L.lastCommitedZxid 之后的 Proposal ,收到 DIFF 指令会接收新的 Proposal。

history.lastCommitedZxid:最近被提交的 Proposal zxid

history.oldThreshold:被认为已经太旧的已经提交的 Proposal zxid

4、广播阶段(Broadcast)

zookeeper中的一次Create请求

一些资料中会提到zookeeper在执行CRUD请求时,使用的是2PC,而 实际上它使用的是容错共识算法。我们以Create请求的流程为例(如下图),来加深和记忆这一知识

  1. 客户端发 create 请求到 Leader,即使请求没落到 Leader 上,那么其他节点也会将写请求转发到 Leader
  2. Leader 会先发一个 提议(proposal)请求给各个 Follower,且自己将数据写到本地文件
  3. Follower 集群收到 proposal 请求后会将数据写到本地文件,写成功后返回给 Leader 一个 ack回复
  4. Leader 发现收到 ack 回复的数量为 法定人数(过半,包含当前 Leader 节点)时,则提交一个 commit 请求给各个 Follower 节点。发送 commit 请求就代表该数据在集群内同步情况没有问题,并且 可以对外提供访问 了,此时Leader会把数据写到内存中
  5. Follower 收到 commit 请求后也会将数据写到各自节点的内存中,同时Leader会将数据发给 Observer集群,通知 Observer集群 将数据写到内存

注:Leader 服务器与每一个 Follower 服务器之间都维护了一个单独的 FIFO 消息队列进行收发消息,使用队列消息可以做到异步解耦,保证有序性。 Leader 和 Follower 之间只需要往队列中发消息即可。如果使用同步的方式会引起阻塞,性能要下降很多。

ZAB算法需要解决的两大问题如何解决的

1. 已经被处理的消息不能丢

这一情况会出现在以下场景:当 leader 收到合法数量 follower 的 ACKs 后,就向各个 follower 广播 COMMIT 命令,同时也会在本地执行 COMMIT 并向连接的客户端返回「成功」。但是如果在各个 follower 在收到 COMMIT 命令前 leader 就挂了,导致剩下的服务器并没有执行都这条消息。

为了实现已经被处理的消息不能丢这个目的,Zab 的恢复模式使用了以下的策略:

  1. 选举拥有 proposal 最大值(即 zxid 最大) 的节点作为新的 leader:由于所有提案被 COMMIT 之前必须有合法数量的 follower ACK,即必须有合法数量的服务器的事务日志上有该提案的 proposal,因此,只要有合法数量的节点正常工作,就必然有一个节点保存了所有被 COMMIT 的 proposal。 而在选举Leader的过程中,会比较zxid,因此选举出来的Leader必然会包含所有被COMMIT的proposal。
  2. 新的 leader 将自己事务日志中 proposal 但未 COMMIT 的消息处理。
  3. 新的 leader 与 follower 建立先进先出的队列, 先将自身有而 follower 没有的 proposal 发送给 follower,再将这些 proposal 的 COMMIT 命令发送给 follower,以保证所有的 follower 都保存了所有的 proposal、所有的 follower 都处理了所有的消息。

2. 被丢弃的消息不能再次出现

这一情况会出现在以下场景:当 leader 接收到消息请求生成 proposal 后就挂了,其他 follower 并没有收到此 proposal,因此经过恢复模式重新选了 leader 后,这条消息是被跳过的。 此时,之前挂了的 leader 重新启动并注册成了 follower,他保留了被跳过消息的 proposal 状态,与整个系统的状态是不一致的,需要将其删除。

Zab 通过巧妙的设计 zxid 来实现这一目的。一个 zxid 是64位,高 32 是纪元(epoch)编号,每经过一次 leader 选举产生一个新的 leader,新 leader 会将 epoch 号 +1。低 32 位是消息计数器,每接收到一条消息这个值 +1,新 leader 选举后这个值重置为 0。这样设计的好处是旧的 leader 挂了后重启,它不会被选举为 leader,因为此时它的 zxid 肯定小于当前的新 leader。当旧的 leader 作为 follower 接入新的 leader 后,新的 leader 会让它将所有的拥有旧的 epoch 号的未被 COMMIT 的 proposal 清除。

Zab 协议设计的优秀之处有两点,一是简化二阶段提交,提升了在正常工作情况下的性能;二是巧妙地利用率自增序列,简化了异常恢复的逻辑,也很好地保证了顺序处理这一特性

zk节点间通信

Zookeeper如何网络通信+监听?看一看Watch机制 - 简书 (jianshu.com)

zookeeper中的BIO和NIO应用

  • NIO:
    • 用于被客户端连接的2181端口,使用的是NIO和客户端建立连接
    • 客户端开启watch是,也使用NIO,等到zk服务器回调
  • BIO:
    • 集群选举时,多个节点之间的投票通信端口,使用BIO通信。

watch机制

Zookeeper 允许客户端向服务端的某个 znode 注册一个 Watcher 监听,当服务端的一些指定事件,触发了这个 Watcher ,服务端会向指定客户端发送一个事件通知来实现分布式的通知功能,然后客户端根据 Watcher 通知状态和事件类型做出业务上的策略。

zookeeper进阶(watcher机制、数据同步流程、分布式锁)_zookeeper数据同步流程图-CSDN博客

大致分为三个步骤:

客户端注册 Watcher

1、调用 getData、getChildren、exist 三个 API ,传入Watcher 对象

2、标记请求request ,封装 Watcher 到 WatchRegistration 。

3、封装成 Packet 对象,发服务端发送request 。

4、收到服务端响应后,将 Watcher 注册到 ZKWatcherManager 中进行管理。

5、请求返回,完成注册。

服务端处理 Watcher

1、服务端接收 Watcher 并存储。

2、Watcher 触发

3、调用 process 方法来触发 Watcher 。

客户端回调 Watcher

1,客户端 SendThread 线程接收事件通知,交由 EventThread 线程回调Watcher 。

2,客户端的 Watcher 机制同样是一次性的,无论是服务端还是客户端,一旦一个 Watcher 被触发, Zookeeper 都会将其从相应的存储中移除。这样的设计有效的减轻了服务端的压力,不然对于更新非常频繁的节点,服务端会不断的向客户端发送事件通知,无论对于网络还是服务端的压力都非常大。

❤:client 端会对某个 znode 建立一个 watcher 事件,当该 znode 发生变化时,这些 client 会收到 zk 的通知,然后 client 可以根据 znode 变化来做出业务上的改变等。

ZooKeeper 的持久化机制

数据,存到磁盘或者文件当中。机器重启后,数据不会丢失。内存 -> 磁盘的映射,和序列化有些像。

ZooKeeper 的持久化采用全量 + 增量方式。

SnapShot 快照,记录内存中的全量数据,TxnLog 增量事务日志,记录每一条增删改记录(查不是事务日志,不会引起数据变化)

快照的缺点:文件太大,而且快照文件不会是最新的数据。

增量事务日志的缺点:运行时间长了,日志太多了,加载太慢。

二者结合最好。快照模式:将 ZooKeeper 内存中以 DataTree 数据结构存储的数据定期存储到磁盘中。由于快照文件是定期对数据的全量备份,所以快照文件中数据通常不是最新的。见图片:


————————————————
版权声明:本文为CSDN博主「Happy编程」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/wanghaiping1993/article/details/125396988

java API

ZooKeeper Java Api 操作_zk获取节点数据-CSDN博客

分布式锁

大致如下:

1. 大家都是上来直接创建一个锁节点下的一个接一个的临时有序节点
2. 如果自己不是第一个节点,就对自己上一个节点加监听器
3. 只要上一个节点释放锁,自己就排到前面去了,相当于是一个排队机制。而且用临时顺序节的

另外一个用意就是,如果某个客户端创建临时顺序节点之后,不小心自己宕机了也没关系,Zookeeper 感知到那个客户端宕机,会自动删除对应的临时顺序节点,相当于自动释放锁,或者是自动取消自己的排队。

本地锁,可以用 JDK 实现,但是分布式锁就必须要用到分布式的组件。比如 ZooKeeper、Redis。
————————————————
版权声明:本文为CSDN博主「Happy编程」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/wanghaiping1993/article/details/125396988

已有成熟解决方案:

zookeeper进阶(watcher机制、数据同步流程、分布式锁)_zookeeper数据同步流程图-CSDN博客

Zookeeper 分布式锁 - 图解 - 秒懂_docker运维面试题-CSDN博客

package com.msb.zookeeper.locks;

import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;

import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;

/**
 * @author: 马士兵教育
 * @create: 2019-09-20 18:03
 */
public class WatchCallBack implements Watcher, AsyncCallback.StringCallback, AsyncCallback.Children2Callback,AsyncCallback.StatCallback,AsyncCallback.DataCallback {

    ZooKeeper zk;
    CountDownLatch cc = new CountDownLatch(1);
    String lockName ;
    String threadName;

    public String getThreadName() {
        return threadName;
    }

    public void setThreadName(String threadName) {
        this.threadName = threadName;
    }

    public ZooKeeper getZk() {
        return zk;
    }

    public void setZk(ZooKeeper zk) {
        this.zk = zk;
    }


    public void tryLock() {
        //重入
        try {
            zk.create("/lock", threadName.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL, this, threadName );
            cc.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void getRootData() throws KeeperException, InterruptedException {
        byte[] data = zk.getData("/", false, new Stat());
        System.out.println(new String(data));
    }

    public void unLock(){
        try {
            zk.delete("/"+lockName,-1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (KeeperException e) {
            e.printStackTrace();
        }
    }

    //getChileden....
    @Override
    public void processResult(int rc, String path, Object ctx, List<String> children, Stat stat) {

        //获得所目录的所有有序节点,然后排序,然后取自己在有序list中的index
        if(children == null){
            System.out.println(ctx.toString() + "list null");
        }else{
            try {
                Collections.sort(children);
                int i = children.indexOf(lockName);
                if(i<1){
                    System.out.println(threadName+" i am first...");
                    zk.setData("/",threadName.getBytes(),-1);
                    cc.countDown();
                }else{
                    System.out.println(threadName+" watch "+children.get(i-1));
                    zk.exists("/"+children.get(i-1),this);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }


    }

    //create....
    @Override
    public void processResult(int rc, String path, Object ctx, String name) {

        //每个线程启动后创建锁,然后get锁目录的所有孩子,不注册watch在锁目录
        System.out.println(ctx.toString()+" create path: "+ name);
        lockName = name.substring(1);
        zk.getChildren("/", false, this, ctx );
    }


    @Override
    public void process(WatchedEvent event) {

        Event.EventType type = event.getType();
        switch (type) {

            case NodeDeleted:
                zk.getChildren("/", false, this, "");
                break;

            case NodeChildrenChanged:
                break;
        }

    }


    @Override
    public void processResult(int rc, String path, Object ctx, byte[] data, Stat stat) {
    }
    @Override
    public void processResult(int rc, String path, Object ctx, Stat stat) {

        //监控失败了怎么办
    }
}
package com.msb.zookeeper.locks;

import com.msb.zookeeper.configurationcenter.DefaultWatch;
import com.msb.zookeeper.configurationcenter.ZKConf;
import com.msb.zookeeper.configurationcenter.ZKUtils;
import org.apache.zookeeper.ZooKeeper;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

/**
 * @author: 马士兵教育
 * @create: 2019-09-20 16:14
 */
public class TestLock {


    ZooKeeper zk;
    ZKConf zkConf;
    DefaultWatch defaultWatch;

    @Before
    public void conn(){
        zkConf = new ZKConf();
        zkConf.setAddress("192.168.150.11:2181,192.168.150.12:2181,192.168.150.13:2181,192.168.150.14:2181/testLock");
        zkConf.setSessionTime(1000);
        defaultWatch = new DefaultWatch();
        ZKUtils.setConf(zkConf);
        ZKUtils.setWatch(defaultWatch);
        zk = ZKUtils.getZK();
    }

    @After
    public void close(){
        ZKUtils.closeZK();
    }

    @Test
    public void testlock(){
        for (int i = 0; i < 10; i++) {
            new Thread(){
                @Override
                public void run() {
                    WatchCallBack watchCallBack = new WatchCallBack();
                    watchCallBack.setZk(zk);
                    String name = Thread.currentThread().getName();
                    watchCallBack.setThreadName(name);

                    try {
                        //tryLock
                        watchCallBack.tryLock();
                        System.out.println(name + " at work");
                        watchCallBack.getRootData();
//                        Thread.sleep(1000);
                        //unLock
                        watchCallBack.unLock();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }

                }
            }.start();
        }
       while(true){

       }
    }
}

应用场景

A、数据发布与订阅

 发布与订阅即所谓的配置管理,顾名思义就是将数据发布到ZooKeeper节点上,供订阅者动态获取
数据,实现配置信息的集中式管理和动态更新。例如全局的配置信息,地址列表等就非常适合使
用。
数据发布/订阅的一个常见的场景是配置中心,发布者把数据发布到 ZooKeeper 的一个或一系列的
节点上,供订阅者进行数据订阅,达到动态获取数据的目的。
配置信息一般有几个特点:
1. 数据量小的KV
2. 数据内容在运行时会发生动态变化
3. 集群机器共享,配置一致
ZooKeeper 采用的是推拉结合的方式。


1. 推: 服务端会推给注册了监控节点的客户端 Wathcer 事件通知
2. 拉: 客户端获得通知后,然后主动到服务端拉取最新的数据

B、命名服务

       作为分布式命名服务,命名服务是指通过指定的名字来获取资源或者服务的地址,利用ZooKeeper创建一个全局的路径,这个路径就可以作为一个名字,指向集群中的集群,提供的服务的地址,或者一个远程的对象等等。统一命名服务的命名结构图如下所示:
1、在分布式环境下,经常需要对应用/服务进行统一命名,便于识别不同服务。类似于域名与IP之间对应关系,IP不容易记住,而域名容易记住。通过名称来获取资源或服务的地址,提供者等信息。
2、按照层次结构组织服务/应用名称。可将服务名称以及地址信息写到ZooKeeper上,客户端通过ZooKeeper获取可用服务列表类。

C、集群管理

所谓集群管理就是:是否有机器退出和加入、选举master。
集群管理主要指集群监控和集群控制两个方面。前者侧重于集群运行时的状态的收集,后者则是对
集群进行操作与控制。开发和运维中,面对集群,经常有如下需求:
1. 希望知道集群中究竟有多少机器在工作
2. 对集群中的每台机器的运行时状态进行数据收集
3. 对集群中机器进行上下线的操作

1、分布式环境中,实时掌握每个节点的状态是必要的,可根据节点实时状态做出一些调整。
2、可交由ZooKeeper实现。
可将节点信息写入ZooKeeper上的一个Znode。
监听这个Znode可获取它的实时状态变化。

D、分布式通知与协调

1、分布式环境中,经常存在一个服务需要知道它所管理的子服务的状态。
a)NameNode需知道各个Datanode的状态。
b)JobTracker需知道各个TaskTracker的状态。
2、心跳检测机制可通过ZooKeeper来实现。
3、信息推送可由ZooKeeper来实现,ZooKeeper相当于一个发布/订阅系统。

E、分布式锁

处于不同节点上不同的服务,它们可能需要顺序的访问一些资源,这里需要一把分布式的锁。
分布式锁具有以下特性:写锁、读锁、时序锁。
写锁:在zk上创建的一个临时的无编号的节点。由于是无序编号,在创建时不会自动编号,导致只
能客户端有一个客户端得到锁,然后进行写入。
读锁:在zk上创建一个临时的有编号的节点,这样即使下次有客户端加入是同时创建相同的节点
时,他也会自动编号,也可以获得锁对象,然后对其进行读取。
时序锁:在zk上创建的一个临时的有编号的节点根据编号的大小控制锁。

F、分布式队列

分布式队列分为两种:
1、当一个队列的成员都聚齐时,这个队列才可用,否则一直等待所有成员到达,这种是同步队
列。
a)一个job由多个task组成,只有所有任务完成后,job才运行完成。
b)可为job创建一个/job目录,然后在该目录下,为每个完成的task创建一个临时的Znode,一旦
临时节点数目达到task总数,则表明job运行完成。
2、队列按照FIFO方式进行入队和出队操作,例如实现生产者和消费者模型。
————————————————
版权声明:本文为CSDN博主「Happy编程」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/wanghaiping1993/article/details/125396988

面试题

28道Zookeeper面试题及答案-CSDN博客

zookeeper_zookeeper持久化_材料小菜鸟的博客-CSDN博客

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值