ZooKeeper详解(三):ZooKeeper的典型应用场景
四、ZooKeeper的典型应用场景1、数据发布/订阅数据发布/订阅系统,即所谓的配置中心,就是发布者将数据发布到ZooKeeper的一个或一系列节点上,供订阅者进行数据订阅,进而达到动态获取数据的目的,实现配置信息的集中式管理和数据的动态更新发布/订阅系统一般有两种设计模式,分别是推(Push)模式和拉(Pull)模式。在推模式中,服务端主动将数据更新发送给所有订阅的客户端;而拉模式则是由...
四、ZooKeeper的典型应用场景
1、数据发布/订阅
数据发布/订阅系统,即所谓的配置中心,就是发布者将数据发布到ZooKeeper的一个或一系列节点上,供订阅者进行数据订阅,进而达到动态获取数据的目的,实现配置信息的集中式管理和数据的动态更新
发布/订阅系统一般有两种设计模式,分别是推(Push)模式和拉(Pull)模式。在推模式中,服务端主动将数据更新发送给所有订阅的客户端;而拉模式则是由客户端主动发起请求来获取最新数据,通常客户端都采用定时进行轮询拉取的方式。而ZooKeeper采用的是推拉相结合的方式:客户端向服务端注册自己需要关注的节点,一旦该节点的数据发生变更,那么服务端就会向相应的客户端发送Watcher事件通知,客户端接收到这个消息通知之后,需要主动到服务端获取最新的数据
如果将配置信息存放到ZooKeeper上进行集中管理,那么通常情况下,应用在启动的时候都会主动到ZooKeeper服务端上进行一次配置信息的获取,同时,在指定节点上注册一个Watcher监听,这样一来,但凡配置信息发生变更,服务端都会实时通知到所有订阅的客户端,从而达到实时获取最新配置信息的目的
接下来就以一个数据库切换的应用场景展开,看看如何使用ZooKeeper来实现配置管理:
配置存储
在进行配置管理之前,首先需要将初始化配置存储到ZooKeeper上去。一般情况下,我们可以在ZooKeeper上选取一个数据节点用于配置的存储,例如/app1/database_config
。将需要集中管理的配置信息写入到该数据节点中去
配置获取
集群中每台机器在启动初始化阶段,首先会从上面提到的ZooKeeper配置节点上读取数据库信息,同时,客户端还需要在该配置节点上注册一个数据变更的Watcher监听,一旦发生节点数据变更,所有订阅的客户端都能够获取到数据变更通知
配置变更
在系统运行过程中,可能会出现需要进行数据库切换的情况,这个时候就需要进行配置变更。借助ZooKeeper,我们只需要对ZooKeeper上配置节点的内容进行更新,ZooKeeper就能够帮我们将数据变更的通知发送到各个客户端,每个客户端在接收到这个变更通知后,就可以重新进行最新数据的获取
2、负载均衡
负载均衡又来对多个计算机(计算机集群)、网络连接、CPU、磁盘驱动器或其他资源进行分配负载,以达到优化资源使用、最大化吞吐率、最小化响应时间和避免过载的目的
1)、一种动态的DNS服务
DNS是域名系统(Domain Name System)的缩写,可以看作是一个超大规模的分布式映射表,用于将域名和IP地址进行一一映射,进而方便人们通过域名来访问互联网站点。在实际开发中,往往使用本地HOST绑定来实现域名解析的工作
基于ZooKeeper实现的动态DNS方案(简称为DDNS,Dynamic DNS):
域名配置
和配置管理一样,首先需要在ZooKeeper上创建一个节点来进行域名配置,例如/DNNS/app1/server.app1.company1.com
(域名节点),每个应用都可以创建一个属于自己的数据节点作为域名配置的根节点,例如/DDNS/app1
,在这个节点上,每个应用都可以将自己的域名配置上去
域名解析
在传统的DNS解析中,域名的解析过程都交给了操作系统的域名和IP地址映射机制(本地HOST绑定)或是专门的域名解析服务器(由域名注册服务商提供)。在DDNS中,域名的解析过程都是由每一个应用自己负责的。通常应用都会首先从域名节点中获取一份IP地址和端口的配置,进行自行解析。同时,每个应用还会在域名节点上注册一个数据变更Watcher监听,以便及时收到域名变更的通知
域名变更
在运行过程中,难免会碰上域名对于的IP地址或是端口变更,这个时候就需要进行域名变更操作。在DDNS中,只需要对指定的域名节点进行更新操作,ZooKeeper就会向订阅的客户端发送这个事件通知,应用在接收到这个事件通知后,就会再次进行域名配置的获取
2)、自动化的DNS服务
动态的DNS服务在域名变更环节中,当域名对应的IP地址发生变更的时候,还是需要人为地介入去修改域名节点上的IP地址和端口。接下来看一下使用ZooKeeper实现的更为自动化的DNS服务。自动化的DNS服务系统主要是为了实现服务的自动化定位,整个系统架构如下图所示:
- Register集群负责域名的动态注册
- Dispatcher集群负责域名解析
- Scanner集群负责检测以及维护服务状态(探测服务的可用性、屏蔽异常服务节点等)
- SDK提供各种语言的系统接入协议,提供服务注册以及查询接口
- Monitor负责收集服务信息以及对DDNS自身状态的监控
- Controller是一个后台管理的Console,负责授权管理、流量控制、静态配置服务和手动屏蔽服务等功能。另外,系统的运维人员也可以在上面管理Register、Dispatcher和Scanner等集群
整个系统的核心是ZooKeeper集群,负责数据的存储以及一系列分布式协调。在这个架构模型中,将那些目标IP地址和端口抽象为服务的提供者,而那些需要使用域名解析的客户端则被抽象成服务的消费者
域名注册
域名注册主要是针对服务提供者来说的,每个服务提供者在启动的过程中,都会把自己的域名信息注册到Register集群中去
1)服务提供者通过SDK提供的API接口,将域名、IP地址和端口发送给Register集群
2)Register获取到域名、IP地址和端口配置后,提供域名将信息写入相对于的ZooKeeper域名节点中
域名解析
域名解析是针对服务消费者来说的,服务消费者在使用域名的时候,会向Dispatcher发出域名解析请求。Dispatcher收到请求后,会从ZooKeeper上的指定域名节点读取相应的IP:PORT列表,通过一定的策略选取其中一个返回给服务消费者
域名探测
域名探测是指DDNS系统需要对域名下所有注册的IP地址和端口的可用性进行检测,俗称健康度检测。健康度检测一般有两种方式,第一种是服务端主动发起健康度心跳检测,这种方式一般需要在服务端和客户端之间建立起一个TCP长连接;第二种则是客户端主动向服务端发起健康度心跳检测。在DDNS架构中的域名探测,使用的是服务提供者主动向Scanner汇报自己的状态
Scanner会负责记录每个服务提供者最近一次的状态汇报时间,一旦超过5秒没有收到状态汇报,那么就认为该IP地址和端口已经不可用,于是开始进行域名清理过程。在域名清理过程中,Scanner会在ZooKeeper中找到该域名对应的域名节点,然后将改地址和端口配置从节点内容中移除
3、命名服务
使用ZooKeeper来实现一套分布式全局唯一ID的分配机制:
所谓ID就是一个能够唯一标识某个对象的标识符。在关系型数据库中,各个表都需要一个主键来唯一标识每条数据库记录,这个主键就是这样的唯一ID。在过去的单库单表系统中,通常可以使用数据库字段自带的auto_increment属性来自动为每条记录生成一个唯一的ID,数据会保证生成的这个ID在全局唯一。但是随着数据库数据规模的不断增大,分库分表随之出现,而auto_increment属性仅能针对单一表中的记录自动生成ID,因此在这种情况下,就无法再依赖数据库的auto_increment属性来唯一标识一条记录了
UUID是通用唯一识别码的简称,一个标准的UUID是一个包含32位字符和4个短线的字符串,但它也存在很多缺陷:
1)长度过长
UUID最大的问题就在于生成的字符串过长,和数据库中的INT类型相比,存储一个UUID需要花费更多的空间
2)含义不明
结合一个分布式任务调度系统来看看如何使用ZooKeeper来实现这类全局唯一ID的生成:
通过调用ZooKeeper节点创建的API接口可以创建一个顺序节点,并且在API返回值中会返回这个节点的完整名字。利用这个特性,就可以借助ZooKeeper来生成全局唯一的ID了
对于一个任务列表的主键,使用ZooKeeper生成唯一ID的基本步骤如下:
1)所有客户端都会根据自己的任务类型,在指定类型的任务下面通过调用create()接口来创建一个顺序节点,例如创建job-
节点
2)节点创建完毕后,create()接口会返回一个完整的节点名,例如job-0000000003
3)客户端拿到这个返回值后,拼接上type类型,例如type2-job-0000000003
,这就可以作为一个全局唯一的ID了
在ZooKeeper中,每一个数据节点都能维护一份子节点的顺序序列,当客户端对其创建一个顺序子节点的时候ZooKeeper会自动以后缀的形式在其子节点上添加一个序号,在这个场景中就是利用了ZooKeeper的这个特性
4、分布式协调/通知
1)、MySQL数据复制总线:Mysql_Replicator
MySQL数据复制总线是一个实时数据复制框架,用于在不同的MySQL数据库实例之间进行异步数据复制和数据变化通知。整个系统是一个由MySQL数据库集群、消息队列系统、任务管理监控平台以及ZooKeeper集群等组件共同构成的一个包含数据生产者、复制管道和数据消费者等部分的数据总线系统
在该系统中,ZooKeeper主要负责进行一系列的分布式协调工作,在具体的实现上,根据功能将数据复制组件划分为三个核心子模块:Core、Server和Monitor,每个模块分别为一个单独的进程,通过ZooKeeper进行数据交换
- Core实现了数据复制的核心逻辑,其将数据复制封装成管道,并抽象出生产者和消费者两个概念,其中生产者通常是MySQL数据库的Binlog日志
- Server负责启动和停止复制任务
- Monitor负责监控任务的运行状态,如果在数据复制期间发生异常或出现故障会进行告警
三个子模块之间的关系如下图:
每个模块作为独立的进程运行在服务端,运行时的数据和配置信息均保存在ZooKeeper上,Web控制台通过ZooKeeper上的数据获取到后台进程的数据,同时发布控制信息
任务注册
Core进程在启动的时候,首先会向/mysql_replicator/tasks
节点(任务列表节点)注册任务。例如,对于一个复制热门商品的任务,Task所在机器在启动的时候,会首先在任务列表节点上创建一个子节点,例如/mysql_replicator/tasks/copy_hot_item
(任务节点),如上图所示。如果在注册过程中发现该子节点已经存在,说明已经有其他Task机器注册了该任务,因此自己不需要再创建该节点了
任务热备份
为了应对复制任务故障或者复制任务所在主机故障,复制组件采用热备份的容灾方式,即将同一个复制任务部署在不同的主机上,称这样的机器为任务机器,主、备任务机器通过ZooKeeper互相检测运行健康状况
为了实现上述热备方案,无论在第一步中是否创建了任务节点,每台任务机器都需要在/mysql_replicator/tasks/copy_hot_item/instances
节点上见自己的主机名注册上去。注意,这里注册的节点类型很特殊,是一个临时的顺序节点。在注册完这个子节点后,通常一个完整的节点名如下:/mysql_replicator/tasks/copy_hot_item/instances/[Hostname]-1
在完成该子节点的创建后,每台任务机器都可以获取到自己创建的节点的完成节点名以及所有子节点的列表,然后通过对比判断自己是否是所有子节点中序号最小的。如果自己是序号最小的子节点,那么就将自己的运行状态设置为RUNNING,其余的任务机器则将自己设置为STANDBY——这样的热备份策略称为小序号优先策略
热备切换
完成运行状态的标识后,任务的客户端机器就能够正常工作了,其中标记为RUNNING的客户端机器进行正常的数据复制,而标记为STANDBY的客户端机器则进入待命状态。这里所谓待命状态,就是说一旦标记为RUNNING的机器出现故障停止了任务执行,那么就需要在所有标记为STANDBY的客户端机器中再次按照小序号优先策略来选出RUNNING机器来执行,具体的做法就是标记为STANDBY的机器都需要在/mysql_replicator/tasks/copy_hot_item/instances
节点上注册一个子节点列表变更的Watcher监听,用来订阅所有任务执行机器的变化情况——一旦RUNNING机器宕机与ZooKeeper断开连接后,对应的节点都会消失,于是其他机器也就接收到了这个变更通知,从而开始新一轮的RUNNING选举
记录执行状态
既然使用了热备份,那么RUNNING任务机器就需要将运行时的上下文状态保留给STANDBY任务机器。在这个场景中,最主要的上下文状态就是数据复制过程中的一些进度信息,例如Binlog日志的消费位点,因此需要将这些信息保存到ZooKeeper上以便共享。在Mysql_Replicator的设计中,选择了/mysql_replicator/tasks/copy_hot_item/lastCommit
作为Binlog日志消费位点的存储节点,RUNNING任务机器会定时向这个节点写入当前的Binlog日志消费位点
控制台协调
在Mysql_Replicator中,Server主要的工作就是进行任务的控制,通过ZooKeeper来对不同的任务进行控制与协调。Server会将每个复制任务对应生产者的元数据,即库名、表名、用户名与密码等数据库信息以及消费者的相关信息以配置的形式写入任务节点/mysql_replicator/tasks/copy_hot_item
中去,以便该任务的所有任务机器都能够共享该复制任务的配置
冷备切换
和热备份中比较大的区别在于,Core进程被配置了所属的Group(组)。举个例子来说,加入一个Core进程被标记了group1,那么在Core进程启动后,会到对应的ZooKeeper group1节点下面获取所有的Task列表,加入找到了任务copy_hot_item之后,就会遍历这个Task列表的instances节点,单反还没有子节点的,则会创建一个临时的顺序节点:/mysql_replicator/tasks-groups/group1/copy_hot_item/instances/[Hostname]-1
——当然,在这个过程中,其他Core进程也会在这个instances节点下创建类似的子节点。和热备份中的小序号优先策略一样,顺序小的Core进程将自己标记为RUNNING,不同之处在于,其他Core进程则会自动将自己创建的子节点删除,然后继续遍历下一个Task节点——将这样的过程称为冷备份扫描。就这样,所有Core进程在一个扫描周期内不断地对相应的Group下面的Task进行冷备份扫描。整个过程可以通过下图所示的流程图来表示:
冷热备份对比
在热备份的方案中,针对一个任务使用了两台机器进行热备份,借助ZooKeeper的Watcher通知机制和临时顺序节点的特性,能够非常实时地进行互相协调,但缺陷就是机器资源消耗比较大。而在冷备份方案中,采用了扫描机制,虽然降低了任务协调的实时性,但是节省了机器资源
2)、一种通用的分布式系统机器间通信方式
心跳检测
机器间的心跳检测是指在分布式环境中,不同机器之间需要检测到彼此是否在正常运行,例如A机器需要知道B机器是否正常运行。在传统的开发中,通常是通过主机之间是否可以相互PING通来判断,更复杂一点的话,则会通过在机器之间建立长连接,通过TCP连接固有的心跳检测机制来实现上层机器的心跳检测,这些都是一些非常常见的心跳检测方法
基于ZooKeeper的临时节点特性,可以让不同的机器都在ZooKeeper的一个指定节点下创建临时子节点,不同的机器之间可以根据这个临时节点来判断对应的客户端机器是否存活。通过这种方式,检测系统和被检测系统之间并不需要直接相关联,而是通过ZooKeeper上的某个节点进行关联,大大减少了系统耦合
工作进度汇报
在一个常见的任务分发系统中,通常任务被分发到不同的机器上执行后,需要实时地将自己的任务执行进度汇报给分发系统。这个时候就可以通过ZooKeeper来实现。在ZooKeeper上选择一个节点,每个任务客户端都在这个节点下面创建临时子节点,这样便可以实现两个功能:
- 通过判断临时节点是否存在来确定任务机器是否存活
- 各个任务机器会实时地将自己的任务执行进度写到这个临时节点上去,以便中心系统能够实时地虎丘到任务的执行进度
系统调度
使用ZooKeeper能够实现另一种系统调度模式:一个分布式系统由控制台和一些客户端系统两部分组成,控制台的职责就是需要将一些指令信息发送到所有的客户端,以控制它们进行相应的业务逻辑。后台管理人员在控制上做的一些操作,实际上就是修改了ZooKeeper上某些节点的数据,而ZooKeeper进一步把这些数据变更以事件通知的形式发送给了对应的订阅客户端
5、集群管理
所谓集群管理,包括集群监控与集群控制两大块,前者侧重对集群运行时状态的收集,后者则是对集群进行操作与控制
ZooKeeper具有以下两大特性:
- 客户端如果对ZooKeeper的一个数据节点注册Watcher注册监听,那么当该数据节点的内容或是其子节点列表发生变更时,ZooKeeper服务器就会向订阅的客户端发送变更通知
- 对在ZooKeeper上创建的临时节点,一旦客户端与服务器之间的会话失效,那么该临时节点也就被自动清除
利用ZooKeeper的这两大特性,就可以实现另一种集群机器存活性监控的系统。例如,监控系统在/clusterServers
节点上注册一个Watcher监听,那么但凡进行动态添加机器的操作,就会在/clusterServers
节点下创建一个临时节点:/clusterServers/[Hostname]
。这样一来,监控系统就能够实时检测到机器的变动情况,至于后续处理就是监控系统的业务了
1)、分布式日志收集系统
在一个典型的日志系统的架构设计中,整个日志系统会把所有需要收集的日志机器(日志源机器)分为多个组别,每个组别对应一个收集器(收集器机器),用于收集日志
注册收集器机器
使用ZooKeeper来进行日志系统收集器的注册,典型做法是在ZooKeeper上创建一个节点作为收集器的根节点,例如/logs/collector
(收集器节点),每个收集器机器在启动的时候,都会在收集器节点下创建自己的节点,例如/logs/collector/[Hostname]
任务分发
待所有收集器机器都创建好自己对应的节点后,系统根据收集器节点下子节点的个数,将所有日志源机器分成对应的若干组,然后将分组后的机器列表分别写到这些收集器机器创建的子节点(例如/logs/collector/host1
)上去。这样一来,每个收集器机器都能够从自己对应的收集器节点上获取日志源机器列表,进而开始进行日志收集工作
状态汇报
完成收集器机器的注册以及任务分发后,还要考虑到这些机器随时都有挂掉的可能。因此,针对这个问题,需要有一个收集器的状态汇报机制:每个收集器机器在创建完自己的专属节点后,还需要在对应的子节点上创建一个状态子节点,例如/logs/collector/host1/status
,每个收集器机器都需要定期向该节点写入自己的状态信息。可以把这种策略看作是一种心跳检测机制,通常收集器都会在这个节点中写入日志收集进度信息。日志系统根据该状态子节点的最后更新时间来判断对应的收集器机器是否存活
动态分配
如果收集器机器挂掉或是扩容了,就需要动态地进行收集任务的分配。在运行过程中,日志系统始终关注着/logs/collector
这个节点下所有子节点的变更,一旦检测到有收集器机器停止汇报或是有新的收集器机器加入,就要开始进行任务的重新分配。无论是针对收集器机器停止汇报还是新机器加入的情况,日志系统都需要将之前分配给该收集器的所有任务进行转移。为了解决这个问题,通常有两种做法:
1)全局动态分配
在出现收集器机器挂掉或是新机器加入的时候,日志系统需要根据新的收集器机器列表,立即对所有的日志源机器重新进行一次分组,然后将其分配给剩下的收集器机器
缺陷:一个或部分收集器机器的变更就会导致全局动态任务的分配,影响面比较大
2)局部动态分配
局部动态分配就是在小范围内进行任务的动态分配。在这种策略中,每个收集器机器在汇报自己日志收集状态的同时,也会把自己的负载汇报上去。如果一个收集器机器挂了,那么日志系统就会把之前分配给这个机器的任务重新分配到那些负载较低的机器上去。同样,如果有新的收集器机器加入,会从哪些负载高的机器上转移部分任务给这个新加入的机器
注意事项
1)节点类型
/logs/collector
节点下面的所有子节点都代表了每个收集器机器,那么初步认为这些子节点必须选择临时节点,原因是日志系统可以根据这些临时节点来判断收集器机器的存活性。但是,同时还需要注意的一点是,在分布式日志收集这个场景中,收集器节点上还会存放所有已经分配给该收集器机器的日志源机器列表,如果只是简单地依靠ZooKeeper自身的临时节点机制,那么当一个收集器机器挂掉或是当这个收集器机器中断心跳汇报的时候,待该收集器节点的会话失效后,ZooKeeper就会立即删除该节点。于是,记录在该节点上的所有日志源机器列表也就随之被清除掉了
从上面的描述中可以知道,临时节点显然无法满足这里的业务需求,所以我们选择了使用持久节点来标识每一个收集器机器,同时在这个持久节点下面分别创建/logs/collector/[Hostname]/status
节点来表征每一个收集器机器的状态。这样一来,既能实现日志系统对所有收集器的监控,同时在收集器机器挂掉后,依然能够准确地将分配于其中的任务还原
2)日志系统节点监听
在实际生产运行过程中,每一个收集器机器更改自己状态节点的频率可能非常高,而且收集器的数量可能非常大,如果日志系统监听所有这些节点变化,那么通知的消息量可能会非常大。另一方面,在收集器机器正常工作的情况下,日志系统没有必要去实时地接收每次节点状态变更,因此大部分这些状态变更通知都是无用的。因此我们考虑放弃监听设置,而是采用日志系统主动轮询收集器节点的策略,这样就节省了不少网卡流量,唯一的缺陷就是有一定的延时
6、Master选举
在分布式环境中,经常会碰到这样的场景:集群中的所有系统单元需要对前端业务提供数据,比如一个商品ID,或者是一个网站轮播广告的广告ID等,而这些商品ID或是广告ID往往需要从一系列的海量数据处理中计算得到——这通常是一个非常消耗I/O和CPU资源的过程。鉴于该计算过程的复杂性,如果让集群中的所有机器都执行这个计算逻辑的话,那么将耗费非常多的资源。一种比较好的方法就是只让集群中的部分,甚至只让其中的一台机器去处理数据计算,一旦计算出数据结果,就可以共享给整个集群中的其他所有客户端机器,这样可以大大减少重复劳动,提升性能
这里以一个简单的广告投放系统后台场景为例来讲解这个模型。整个系统大体上可以分成客户端集群、分布式缓存系统、海量数据处理总线和ZooKeeper四个部分,如下图所示:
整个系统的运行机制是这样的,Client集群每天定时会通过ZooKeeper来实现Master选举。选举产生Master客户端之后,这个Master就会负责一系列的海量数据处理,最终计算得到一个数据结果,并将其放置在一个内存/数据库中。同时,Master还需要通知集群中其他所有的客户端从这个内存/数据库中共享计算结果
利用ZooKeeper的强一致性,能够很好地保证在分布式高并发情况下节点的创建一定能够保证全局唯一性,即ZooKeeper将会保证客户端无法重复创建一个已经存在的数据节点。也就是说,如果同时有多个客户端请求创建同一个节点,那么最终一定只有一个客户端请求能够创建成功。利用这个特性,就能很容易地在分布式环境中进行Master选举了
在这个系统中,首先会在ZooKeeper上创建一个日期节点,例如2013-09-20
客户端集群每天都会定时往ZooKeeper上创建一个临时节点,例如/master_election/2013-09-20/binding
。在这个过程中,只有一个客户端能够成功创建这个节点,那么这个客户端所在的机器就成为了Master。同时,其他没有在ZooKeeper上成功创建节点的客户端,都会在节点/master_election/2013-09-20
上注册一个子节点变更的Watcher,用于监控当前的Master机器是否存活,一旦发现发现当前的Master挂了,那么其余的客户端将会重新进行Master选举
7、分布式锁
分布式锁是控制分布式系统之间同步访问共享资源的一种方式。如果不同的系统或是同一个系统的不同主机宅男共享了一个或一组资源,那么访问这些资源的时候,往往需要通过一些互斥手段来防止彼此之间的干扰,以保证一致性,在这种情况下,就需要使用分布式锁了
1)、排他锁
排他锁又称为写锁或独占锁,是一种基本的锁类型,如果事务T1对数据对象O1加上了排他锁,那么在整个加锁期间,只允许事务T1对O1进行读取和更新操作,其他任何事务都不能再对这个数据对象进行任何类型的操作——直到T1释放了排他锁
排他锁的核心是如何保证当前有且仅有一个事务获得锁,并且锁被释放后,所有正在等待获取锁的事务都能够被通知到
定义锁
通过在ZooKeeper上创建一个子节点来表示一个锁,例如/exclusive_lock/lock节点就可以被定义为一个锁
获取锁
在需要获取排他锁时,所有的客户端都会试图通过调用create()接口,在/exclusive_lock节点下创建临时子节点/exclusive_lock/lock。ZooKeeper会保证在所有的客户端中,最终只有一个客户能够创建成功,那么就可以认为该客户端获取了锁。同时,所有没有获取到锁的客户端就需要到/exclusive_lock节点上注册一个子节点变更的Watcher监听,以便实时监听到lock节点的变更情况
释放锁
/exclusive_lock/lock是一个临时节点,因此在以下两种情况下,都有可能释放锁
- 当前获取锁的客户端机器发生宕机,那么ZooKeeper上的这个临时节点就会被移除
- 正常执行完业务逻辑后,客户端就会主动将自己创建的临时节点删除
无论在什么情况下移除了lock节点,ZooKeeper都会通知所有在/exclusive_lock节点上注册了子节点变更Watcher监听的客户端。这些客户端在接收到通知后,再次重新发起分布式锁获取,即重复获取锁过程
整个排他锁的获取和释放流程如下图:
2)、羊群效应
上面的排他锁的实现可能引发羊群效应:当一个特定的ZNode改变的时候ZooKeeper触发了所有Watcher的事件,由于通知的客户端很多,所以通知操作会造成ZooKeeper性能突然下降,这样会影响ZooKeeper的使用
改进后的分布式锁实现
获取锁
首先,在Zookeeper当中创建一个持久节点ParentLock。当第一个客户端想要获得锁时,需要在ParentLock这个节点下面创建一个临时顺序节点Lock1
之后,Client1查找ParentLock下面所有的临时顺序节点并排序,判断自己所创建的节点Lock1是不是顺序最靠前的一个。如果是第一个节点,则成功获得锁
这时候,如果再有一个客户端Client2前来获取锁,则在ParentLock下再创建一个临时顺序节点Lock2
Client2查找ParentLock下面所有的临时顺序节点并排序,判断自己所创建的节点Lock2是不是顺序最靠前的一个,结果发现节点Lock2并不是最小的
于是,Client2向排序仅比它靠前的节点Lock1注册Watcher,用于监听Lock1节点是否存在。这意味着Client2抢锁失败,进入了等待状态
这时候,如果又有一个客户端Client3前来获取锁,则在ParentLock下再创建一个临时顺序节点Lock3
Client3查找ParentLock下面所有的临时顺序节点并排序,判断自己所创建的节点Lock3是不是顺序最靠前的一个,结果同样发现节点Lock3并不是最小的
于是,Client3向排序仅比它靠前的节点Lock2注册Watcher,用于监听Lock2节点是否存在。这意味着Client3同样抢锁失败,进入了等待状态
这样一来,Client1得到了锁,Client2监听了Lock1,Client3监听了Lock2。这恰恰形成了一个等待队列,很像是Java当中ReentrantLock所依赖的AQS(AbstractQueuedSynchronizer)
释放锁
获得锁的Client1在任务执行过程中,如果崩溃了,则会断开与Zookeeper服务端的链接。根据临时节点的特性,相关联的节点Lock1会随之自动删除
由于Client2一直监听着Lock1的存在状态,当Lock1节点被删除,Client2会立刻收到通知。这时候Client2会再次查询ParentLock下面的所有节点,确认自己创建的节点Lock2是不是目前最小的节点。如果是最小,则Client2就获得了锁
同理,如果Client2也因为任务完成或者节点崩溃而删除了节点Lock2,那么Client3就会接到通知
最终,Client3成功得到了锁
3)、共享锁
共享锁又称为读锁,在同一时刻可以允许多个线程访问,典型的就是ReentrantReadWriteLock里的读锁,它的读锁是可以被共享的,但是它的写锁确实每次只能被独占
定义锁
和排他锁一样,同样是通过ZooKeeper上的数据节点来表示一个锁,是一个类似于/shared_lock/[Hostname]-请求类型-序号的临时顺序节点,例如/shared_lock/192.168.0.1-R-0000000001,那么,这个节点就代表了一个共享锁,如下图所示:
获取锁
在需要获取共享锁时,所有客户端都会到/shared_lock这个节点下面创建一个临时顺序节点,如果当前是读请求,那么就创建例如/shared_lock/192.168.0.1-R-0000000001的节点;如果是写请求,那么就创建例如/shared_lock/192.168.0.1-W-0000000001的节点
判断读写顺序
每个锁竞争者,只需要关注/shared_lock节点下序号比自己小的那个节点是否存在即可,具体实现如下:
1)客户端调用create()方法创建一个类似于/shared_lock/[Hostname]-请求类型-序号的临时顺序节点
2)客户端调用getChildren()接口来获取所有已经创建的子节点列表
3)如果无法获取共享锁,那么就调用exist()来对比自己小的那个节点注册Watcher
读请求:向比自己序号小的最后一个写请求节点注册Watcher监听
写请求:向比自己序号小的最后一个节点注册Watcher监听
4)等待Watcher通知,继续进入步骤2
释放锁
释放锁的逻辑和排他锁是一致的
整个共享锁的获取和释放流程如下图:
8、分布式队列
分布式队列,简单地将分为两大类,一种是常规的先入先出队列,另一种则是要等到队列元素集聚之后才统一安排执行的Barrier模型
FIFO:先入先出
FIFO:先记入队列的请求操作先完成后,才会开始处理后面的请求
FIFO队列就类似于一个全写的共享锁模型,大体的设计思路:所有客户端都会到/queue_fifo
这个节点下面创建一个临时顺序节点,例如/queue_fifo/192.168.0.1-0000000001
创建完节点之后,根据如下4个步骤来确定执行顺序:
1)通过调用getChildren()接口来获取/queue_fifo
节点下的所有子节点,即获取队列中所有的元素
2)确定自己的节点序号在所有子节点中的顺序
3)如果自己不是序号最小的子节点,那么就需要进入等待,同时向比自己序号小的最后一个节点注册Watcher监听
4)接收到Watcher通知后,重复步骤1
整个FIFO队列的工作流程如下图:
Barrier:分布式屏障
Barrier原意是指障碍物、屏幕,而在分布式系统中,特指系统之间的一个协调条件,规定了一个队列的元素必须都集聚后才能统一进行安排,否则一直等待。这往往出现在那些大规模分布式并行计算的应用场景上:最终的合并计算需要基于很多并行计算的子结果来进行。开始时,/queue_barrier
节点是一个已经存在的默认节点,并且将其节点的数据内容复制一个数字n代表Barrier值,例如n=10表示只有当/queue_barrier
节点下的子节点个数达到10后,才会打开Barrier。之后,所有的客户端都会到/queue_barrier
节点下创建一个临时节点,例如/queue_barrier/192.168.0.1
创建完节点之后,根据如下5个步骤来确定执行顺序:
1)通过调用getData()接口获取/queue_barrier
节点的数据内容:10
2)通过调用getChildren()接口获取/queue_barrier
节点下的所有子节点,即获取队列中的所有元素,同时注册对子节点列表变更的Watcher监听
3)统计子节点的个数
4)如果子节点个数不足10个,那么就需要进入等待
5)接收到Watcher通知后,重复步骤2
整个Barrier队列的工作流程如下图:
更多推荐
所有评论(0)