Zookeeper应用场景

Zookeeper应用场景,第1张

Zookeeper应用场景 数据发布/订阅(配置中心)

数据发布/订阅(Publish/Subscribe)系统,即所谓的配置中⼼,顾名思义就是发布者将数据发布到 ZooKeeper的⼀个或⼀系列节点上,供订阅者进⾏数据订阅,进⽽达到动态获取数据的⽬的,实现配置 信息的集中式管理和数据的动态更新。

发布/订阅系统⼀般有两种设计模式,分别是推(Push)模式和拉(Pull)模式。

在推模式中,服务端 主动将数据更新发送给所有订阅的客户端

⽽拉模式则是由客户端主动发起请求来获取最新数据,通常客户端都采⽤定时进⾏轮询拉取的⽅式。

ZooKeeper 采⽤的是推拉相结合的⽅式:客户端向服务端注册⾃⼰需要关注的节点,⼀旦该节点的数据发⽣变更,那么服务端就会向相应的客户端发送Watcher事件通知,客户端接收到这个消息通知之后, 需要主动到服务端获取最新的数据。
如果将配置信息存放到ZooKeeper上进⾏集中管理,那么通常情况下,应⽤在启动的时候都会主动到 ZooKeeper服务端上进⾏⼀次配置信息的获取,同时,在指定节点上注册⼀个Watcher监听,这样⼀ 来,但凡配置信息发⽣变更,服务端都会实时通知到所有订阅的客户端,从⽽达到实时获取最新配置信息的⽬的。

命名服务(分布式ID)

⼴义上命名服务的资源定位都不是真正意义的实体资源——在分布式环境中,上层应⽤仅仅 需要⼀个全局唯⼀的名字,类似于数据库中的唯⼀主键。

 

使⽤ZooKeeper⽣成唯⼀ID的基本步骤:

  1. 所有客户端都会根据⾃⼰的任务类型,在指定类型的任务下⾯通过调⽤create()接⼝来创建⼀个 顺序节点,例如创建“job-”节点。

  2. 节点创建完毕后,create()接⼝会返回⼀个完整的节点名,例如“job-0000000003”。

  3. 客户端拿到这个返回值后,拼接上 type 类型,例如“type2-job-0000000003”,这就可以作为⼀个 全局唯⼀的ID了。

当客户端对其创建⼀个顺序⼦节点的时候 ZooKeeper 会⾃动以后缀的形式在其⼦节点上添加⼀个序号,在这个场景中就是利⽤了 ZooKeeper的这个特性

集群管理

Zookeeper的两⼤特性:

  1. 客户端如果对Zookeeper的数据节点注册Watcher监听,那么当该数据节点的内容或是其⼦节点 列表发⽣变更时,Zookeeper服务器就会向订阅的客户端发送变更通知。

  2. 对在Zookeeper上创建的临时节点,⼀旦客户端与服务器之间的会话失效,那么临时节点也会被⾃动删除。

利⽤其两⼤特性,可以实现集群机器存活监控系统,若监控系统在/clusterServers节点上注册⼀个 Watcher监听,那么但凡进⾏动态添加机器的 *** 作,就会在/clusterServers节点下创建⼀个临时节点:/clusterServers/[Hostname],这样,监控系统就能够实时监测机器的变动情况。

 

在/logs/collector节点下创建临时节点可以很好的判断机器是否存活,但是,若机器挂了,其节点会被 删除,记录在节点上的⽇志源机器列表也被清除,所以需要选择持久节点来标识每⼀台机器,同时在节 点下分别创建/logs/collector/[Hostname]/status节点来表征每⼀个收集器机器的状态,这样,既能实 现对所有机器的监控,同时机器挂掉后,依然能够将分配任务还原。

Master选举

⾸先来明确下 Master 选举的需求:在集群的所有机器中选举出⼀台机器作为Master。针对这个需求,通常情况下,我们可以选择常⻅的关系型数据库中的主键特性来实现:集群中的所有机器都向数据库中插⼊⼀条相同主键 ID 的记录,数据库会帮助我们⾃动进⾏主键冲突检查,也就是说,所有进⾏插⼊ *** 作的客户端机器中,只有⼀台机器能够成功——那么, 我们就认为向数据库中成功插⼊数据的客户端机器成为Master。

借助数据库的这种⽅案确实可⾏,依靠关系型数据库的主键特性能够很好地保证在集群中选举出唯⼀的⼀个Master。但是我们需要考虑的另⼀个问题是,如果当前选举出的Master挂了,那么该如何处理? 谁来告诉我Master挂了呢?显然,关系型数据库没法通知我们这个事件。

那么,如果使⽤ZooKeeper是否可以做到这⼀点呢?

ZooKeeper将会保证客户端⽆法重复创建⼀个已经存在的数据节点。也就是说,如果同时有多个客户端请求创建同⼀个节点,那么最终⼀定只有⼀个客户端请求能够创建成功。利⽤这个特性,就能很容易地在分布式环境中进⾏Master选举了。

 

客户端集群每天都会定时往ZooKeeper 上创建⼀个临时节点,例如/master_election/2020-11- 11/binding。在这个过程中,只有⼀个客户端能够成功创建这个节点,那么这个客户端所在的机器就成为了Master。同时,其他没有在ZooKeeper上成功创建节点的客户端,都会在节点/master_election/2020-11-11 上注册⼀个⼦节点变更的 Watcher,⽤于监控当前的 Master 机器是否存活,⼀旦发现当前的 Master 挂了,那么其余的客户端将会重新进⾏Master选举。

分布式锁 排他锁

排他锁(Exclusive Locks,简称 X 锁),⼜称为写锁或ᇿ占锁,是⼀种基本的锁类型。如果事务 T1对 数据对象 O1加上了排他锁,那么在整个加锁期间,只允许事务 T1对 O1进⾏读取和更新 *** 作,其他任何事务都不能再对这个数据对象进⾏任何类型的 *** 作——直到T1释放了排他锁。

排他锁的核⼼是如何保证当前有且仅有⼀个事务获得锁,并且锁被释放后,所有正在等待获取锁的事务都能够被通知到。

借助ZooKeeper实现排他锁:

是通过 ZooKeeper 上的数据节点来表示⼀个锁,例如/exclusive_lock/lock节点就可以被定义为⼀个锁

 

在需要获取排他锁时,所有的客户端都会试图通过调⽤ create()接⼝,在/exclusive_lock节点下创建临时⼦节点/exclusive_lock/lock。ZooKeeper 会保证在所有的客户端中,最终只有⼀个客户端能够创建成功,那么就可以认为该客户端获取了锁。同时,所有没有获取到锁的客户端就需要到/exclusive_lock 节点上注册⼀个⼦节点变更的Watcher监听,以便实时监听到lock节点的变更情况。

当前获取锁的客户端机器发⽣宕机,那么ZooKeeper上的这个临时节点就会被移除。正常执⾏完业务逻辑后,客户端就会主动将⾃⼰创建的临时节点删除。⽆论在什么情况下移除了lock节点,ZooKeeper都会通知所有在/exclusive_lock节点上注册了⼦节点变更Watcher监听的客户端。这些客户端在接收到通知后,再次重新发起分布式锁获取。

共享锁

共享锁(Shared Locks,简称S锁),⼜称为读锁,同样是⼀种基本的锁类型。如果事务T1对数据对象O1加上了共享锁,那么当前事务只能对O1进⾏读取 *** 作,其他事务也只能对这 个数据对象加共享锁——直到该数据对象上的所有共享锁都被释放。

共享锁和排他锁最根本的区别在于,加上排他锁后,数据对象只对⼀个事务可⻅,⽽加上共享锁后,数据对所有事务都可⻅。

借助ZooKeeper来实现共享锁:

和排他锁⼀样,同样是通过 ZooKeeper 上的数据节点来表示⼀个锁,是⼀个类似于 “/shared_lock/[Hostname]-请求类型-序号”的临时顺序节点,例如/shared_lock/host1-R0000000001,那么,这个节点就代表了⼀个共享锁

  

在需要获取共享锁时,所有客户端都会到/shared_lock 这个节点下⾯创建⼀个临时顺序节点,如果当前 是读请求,那么就创建例如/shared_lock/host1-R-0000000001的节点;如果是写请求,那么就创建例 如/shared_lock/host2-W-0000000002的节点。

判断读写顺序:

通过Zookeeper来确定分布式读写顺序,⼤致分为四步:

  1. 创建完节点后,获取/shared_lock节点下所有⼦节点,并对该节点变更注册监听。

  2. 确定⾃⼰的节点序号在所有⼦节点中的顺序。

  3. 对于读请求:若没有⽐⾃⼰序号⼩的⼦节点或所有⽐⾃⼰序号⼩的⼦节点都是读请求,那么表 明⾃⼰已经成功获取到共享锁,同时开始执⾏读取逻辑,若有写请求,则需要等待。对于写请求:若⾃⼰不是序号最⼩的⼦节点,那么需要等待。

  4. 接收到Watcher通知后,重复步骤1

释放锁的流程与独占锁⼀致。

⽺群效应

如果机器规模扩⼤之后,我们着重来看上⾯“判断读写顺序”过程的步骤3,结合下⾯的图,看看实际运⾏中的情况


可以看到,host1客户端在移除⾃⼰的共享锁后,Zookeeper发送了⼦节点更变Watcher通知给所有机 器,然⽽除了给host2产⽣影响外,对其他机器没有任何作⽤。⼤量的Watcher通知和⼦节点列表获取 两个 *** 作会重复运⾏,这样不仅会对zookeeper服务器造成巨⼤的性能影响影响和⽹络开销,更为严重 的是,如果同⼀时间有多个节点对应的客户端完成事务或是事务中断引起节点消失,ZooKeeper服务器 就会在短时间内向其余客户端发送⼤量的事件通知,这就是所谓的⽺群效应。

上⾯这个ZooKeeper分布式共享锁实现中出现⽺群效应的根源在于,没有找准客户端真正的关注点。我们再来回顾⼀下上⾯的分布式锁竞争过程,它的核⼼逻辑在于:判断⾃⼰是否是所有⼦节点中序号最⼩的。于是,很容易可以联想到,每个节点对应的客户端只需要关注⽐⾃⼰序号⼩的那个相关节点的变更情况就可以了——⽽不需要关注全局的⼦列表变更情况。

可以有如下改动来避免⽺群效应。

每个锁竞争者,只需要关注/shared_lock节点下序号⽐⾃⼰⼩的那个节点是否存在即可

  1. 客户端调⽤create接⼝常⻅类似于/shared_lock/[Hostname]-请求类型-序号的临时顺序节点。

  2. 客户端调⽤getChildren接⼝获取所有已经创建的⼦节点列表(不注册任何Watcher)。

  3. 如果⽆法获取共享锁,就调⽤exist接⼝来对⽐⾃⼰⼩的节点注册Watcher。对于读请求:向⽐⾃⼰序号⼩的最后⼀个写请求节点注册Watcher监听。对于写请求:向⽐⾃⼰序号⼩的最后⼀个节点注册Watcher监听。

  4. 等待Watcher通知,继续进⼊步骤2

此⽅案改动主要在于:每个锁竞争者,只需要关注/shared_lock节点下序号⽐⾃⼰⼩的那个节点是否存在即可。

分布式队列

分布式队列可以简单分为两⼤类:⼀种是常规的FIFO先⼊先出队列模型,还有⼀种是等待队列元素聚集后统⼀安排处理执⾏的Barrier模型。

FIFO先⼊先出

FIFO(First Input First Output,先⼊先出), FIFO 队列是⼀种⾮常典型且应⽤⼴泛的按序执⾏的队列模型:先进⼊队列的请求 *** 作先完成后,才会开始处理后⾯的请求。

使⽤ZooKeeper实现FIFO队列,和之前提到的共享锁的实现⾮常类似。FIFO队列就类似于⼀个全写的共享锁模型,⼤体的设计思路其实⾮常简单:所有客户端都会到/queue_fifo 这个节点下⾯创建⼀个临时顺序节点,例如/queue_fifo/host1-00000001

 

创建完节点后,根据如下4个步骤来确定执⾏顺序。

  1. 通过调⽤getChildren接⼝来获取/queue_fifo节点的所有⼦节点,即获取队列中所有的元素。

  2. 确定⾃⼰的节点序号在所有⼦节点中的顺序。

  3. 如果⾃⼰的序号不是最⼩,那么需要等待,同时向⽐⾃⼰序号⼩的最后⼀个节点注册Watcher监 听。

  4. 接收到Watcher通知后,重复步骤1。

Barrier:分布式屏障

Barrier原意是指障碍物、屏障,⽽在分布式系统中,特指系统之间的⼀个协调条件,规定了⼀个队列的元素必须都集聚后才能统⼀进⾏安排,否则⼀直等待。这往往出现在那些⼤规模分布式并⾏计算的应⽤场景上:最终的合并计算需要基于很多并⾏计算的⼦结果来进⾏。这些队列其实是在 FIFO 队列的基础上进⾏了增强,⼤致的设计思想如下:开始时,/queue_barrier 节点是⼀个已经存在的默认节点,并且将其节点的数据内容赋值为⼀个数字n来代表Barrier值,例如n=10表示只有当/queue_barrier节点下的⼦节点个数达到10后,才会打开Barrier。之后,所有的客户端都会到/queue_barrie节点下创建⼀个临 时节点,例如/queue_barrier/host1

  
  1. 通过调⽤getData接⼝获取/queue_barrier节点的数据内容:10。

  2. 通过调⽤getChildren接⼝获取/queue_barrier节点下的所有⼦节点,同时注册对⼦节点变更的 Watcher监听。

  3. 统计⼦节点的个数。

  4. 如果⼦节点个数还不⾜10个,那么需要等待。

  5. 接受到Wacher通知后,重复步骤2

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/zaji/5676505.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-17
下一篇 2022-12-17

发表评论

登录后才能评论

评论列表(0条)

保存