Zookeeper实践篇-Zookeeper经典场景实践
Posted m0_60707708
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Zookeeper实践篇-Zookeeper经典场景实践相关的知识,希望对你有一定的参考价值。
在前面的文章中,我们学习过zk创建节点的Api特性,其中很重要的一点就是,zk自身具有强一致性,可以保证在分布式环境下高并发创建节点一定可以保证全局唯一。即无法重复的创建一个已经存在的数据节点。如果有多个客户端同时请求创建该节点,那么最终只有一个客户端能创建成功,利用这个特性,就可以简单的实现Master选举等功能。
例如,我们在系统中创建一个日期节点,例如‘2013-09-20’,如图:
而客户端每天定时往zk的对应日期节点中创建一个临时节点,例如图中的**/master_election/2013-09-20/binding**节点,只有一个客户端可以创建这个节点,而其他客户端都是创建失败,此时创建成功的客户端自然而然可以视为master,具有一定的管理权。而创建失败的客户端则可以选择对该节点注册一个watch,用来监控master节点,一旦发现master挂了,所有的客户端收到通知后,再次去创建binding节点,重复上述操作即可保证master节点的选举操作了。
分布式锁
分布式锁是控制分布式系统之间同步访问资源的一种方式,如果在分布式系统下,需要共享访问一个或者一组资源,那么访问这些资源的时候,往往需要一些互斥的手段来保证一致性,这种场景下就需要使用分布式锁了。而zk可以实现的分布式锁一般分为共享锁和排他锁两种。
排他锁
排他锁又称写锁或者独占锁,是一种基本的锁类型。如果对客户端对数据对象O1添加了排他锁,那么在整个持有锁的过程中,只允许当前客户端对O1进行读取和更新操作。在zk中,我们可以通过节点来标识一个锁,例如**/exclusive_lock/lock**节点被定义为一个锁,如图所示:
在获取排他锁的时候,所有的客户端都会试图通过create()方法去在/exclusive_lock下面创建临时节点**/lock**,而zk的创建方法最终能保证只有一个客户端创建成功,此客户端则认为成功获取到了锁,其他客户端创建失败以后,则可以选择注册一个watch监听。我们创建的锁节点属于临时节点,因此在以下两种情况下,都有可能被释放:
1.由于临时节点的特性,不会进行持久化保存,而是和当前连接的session关联,因此当服务器出现宕机的情况下会被释放
2.在创建临时节点成功后,客户端执行需要处理的业务操作完毕后,主动调用临时节点的删除操作
因此无论什么情况下移除了/lock节点,zk都会将事件通知给所有的watch监听的客户端。而这个时候所有的客户端则可以在收到通知后,再次发起创建节点的请求操作,重复上面的操作。整个排他锁的流程如图所示:
共享锁
我们在日常开发过程中,还会使用到一个共享锁。所谓共享锁,又称之为读锁,即如果当前资源O1被挂载了共享锁,那么其他的客户端只能对当前的资源O1进行读取的操作,而不是进行更新操作,但是当前资源可以同时挂载多个共享锁,如果想要更新当前资源,则需要挂载的共享锁全部释放。与排他锁不同的是,排他锁仅仅对一个客户端可见,而共享锁则是可以对多个客户端同时可见。
我们同样可以使用zk的临时节点来实现共享锁操作,我们指定一个节点/sharedlock为共享锁的节点,而每个客户端则是在当前节点下创建临时顺序节点,例如**/shared****lock/192.168.1.1-R-000000001**,节点名称规则为/shared_lock/ip-读写类型-临时节点序号,如图:
当所有的客户端都创建完属于自己类型的共享锁后,我们则需要进行读写规则判断,确定当前各个客户端对资源的操作属于读还是写操作,大概如下几个步骤:
1.每个客户端都在当前节点下创建对应规则类型的临时顺序节点,并且对该节点注册一个子节点变更的watch监听
2.当创建节点成功以后,每个客户端需要判断创建的节点在所有的子节点中的顺序
3.并且判断在当前节点的类型的操作
-
如果是读请求的节点,在此之前的所有的子节点类型都是读类型的,那么当前客户端也可以执行读操作;如果之前的节点有写请求类型,那么在写请求之后的所有客户端都需要进行等待写操作完成。
-
如果当前的节点是写请求操作,那么除非当前节点在第一个,否则进入等待
4.当接收到watch通知操作后,每个客户端都会重复创建watch监听操作
整个共享锁的逻辑基于排他锁的思想,但是逻辑较复杂,整体流程如图所示:
羊群效应
上面我们创建的两种分布式锁,一般在规模不大的情况下,性能还算不错,但是我们不禁考虑一个问题,每次有子节点变化的时候,都会将所有的watch进行通知,但是每次也只有一个客户端在收到通知后拥有锁的操作权,其他的客户端其实是没有作用的,依然是重新注册watch监听,等待下一次通知。如果在频繁的事件监听触发情况下,我们不免发现有着大量的重复通知,有多少个客户端监听,每次触发就会通知到多少客户端,这种重复通知操作称之为羊群效应(惊群效应)。
那么我们在使用过程中能不能优化分布式锁,避免触发羊群效应呢?
其实是可以的,我们不妨思考触发羊群效应的原因,即每个客户端注册了watch监听都是对父节点的子节点变化的监听,但是我们知道每次触发监听,其实就是排在第一位的临时节点被该客户端释放,那么我们可以在每次注册watch监听的时候,找到当前客户端创建的临时节点的前一个节点,进行监听,这样,无论前面的锁如何变化,每个客户端只关心上一个客户端的节点是否被释放,一旦释放,则会触发通知给当前客户端,避免了所有客户端都收到通知的情况
分布式队列
提到队列,我们最先想到的会是FIFO的队列,即先进先出的顺序队列,在分布式环境下想要实现FIFO的队列很简单,我们可以利用类似共享锁的的实现。例如,所有的客户端都会在/queuefifo这个节点下创建一个临时顺序节点,如**/queue****fifo/192.168.1.1-0000000001**,/queue_fifo/192.168.1.1-0000000002等,如图所示:
创建完节点后,我们将根据以下4个步骤来确定执行的顺序:
1.通过调用getChildren()接口来获取/queue_fifo节点下的所有子节点,即获取了当前队列中的所有元素
2.确定自己的节点序号在所有子节点中的顺序
3.如果自己不是序号最小的子节点,即不是最前面的子节点,那么则需要进入继续等待,同时这里我们需要向比当前序号小的最后一个节点注册watch监听
4.接受到watch监听后,重新重复步骤1即可
整个的过程大概如图所示:
而在分布式开发环境中,除了FIFO队列以外,我们往往还存在一个分布式队列,即等待一个队列的元素都聚集以后才进行下一步统一的安排,否则一直进行等待操作。而这种操作往往存在于分布式环境的并行计算或者并行操作中,而该队列则是基于FIFO队列的一个增强实现,我们可以通过注册一个已经存在的节点,如**/queuebarrier**,我们将需要等待的数量n作为值写入当前节点中,例如n=10,则代表当前节点下存在10个子节点的时候才会开始下一步操作,而这个过程中,每个客户端都会在该节点下创建一个临时节点,如**/queue****barrier/192.168.1.1**,如图所示:
创建完节点以后,我们可以通过以下五个步骤来确定执行顺序:
1.每个客户端通过调用getData()来获取/queue_barrier节点中规定的子节点数量
2.通过调用getChildren()接口获取/queuebarrier节点下的所有子节点,然后对/queuebarrier节点添加一个子节点列表变化的watch
3.每次触发watch的时候计算子节点数量
4.如果当前数量不足规定的数量,则继续添加监听,进入等待
5.如果数量刚好满足,则可以执行下一步的业务逻辑操作
列表变化的watch
3.每次触发watch的时候计算子节点数量
4.如果当前数量不足规定的数量,则继续添加监听,进入等待
5.如果数量刚好满足,则可以执行下一步的业务逻辑操作
以上是关于Zookeeper实践篇-Zookeeper经典场景实践的主要内容,如果未能解决你的问题,请参考以下文章
2月22日 《从Paxos到Zookeeper 分布式一致性原理与实践》读后感
zookeeper原理与实践(一)----zookeeper的基本功能
一场事故告诉你zookeeper和nacos谁更适合做注册中心