数据发布与订阅(配置中心)
数据发布/订阅(Publish/Subcribe)系统,即所谓的配置中心,顾名思义就是发布者将数据发布到ZooKeeper节点上,供订阅者进行订阅,进而达到动态获取数据的目的。实现配置信息的集中式管理和动态更新。
发布/订阅系统一般有两种设计模式,分别是推(Push)模式和拉(Pull)模式。在推模式中,服务端主动将数据更新发送给所有订阅的客户端,而拉模式则是由客户端主动发起请求来获取最新数据,通常客户端都采用定时进行轮询拉取的方式。ZooKeeper采用的是推拉相结合的方式:客户端向服务端注册自己需要关注的节点,一旦该节点的数据发生变更,那么服务端就会向相应的客户端发送Watcher事件通知,客户端接收到这个消息通知后,需要主动到服务端获取最新的数据。
在我们平常的应用系统开发中,经常会碰到这样的需求:系统中需要使用一些通用的配置信息,例如机器列表信息、运行时的开关配置、数据库配置信息等。这些全局配置信息通常具备一下3个特性。
- 数据量通常比较小
- 数据内容运行时会发生变动
- 集群中各机器共享,配置一致
对于这类配置信息,一般的做法通常可以选择将其存储在本地配置文件或是内存变量中。如果采用本地配置文件的方式,需要在运行过程中定时地进行文件的读取,以此来检查文件内容的变更。但是一旦机器规模变大,且配置信息变更越来越频繁后就会变得越来越困难。我们既希望能够快速的做到全局配置信息的变更,同事希望变更成本足够小,因此我们必须寻求一种更为分布化的解决方案。
接下来我们就以 数据库切换 的应用场景展开,看看如何使用ZooKeeper来实现配置管理。
配置存储
在进行配置管理前,首先我们需要将初始化配置信息存储到ZooKeeper上去,一般情况下,我们可以在ZooKeeper上选取一个数据节点用于配置信息的存储,例如:/app1/database_config,如下所示
我们将需要管理的配置信息写入到该数据节点中,如下
#数据库配置信息
dbcp.driverClassName=com.mysql.jdbc.Driver
dbcp.dbJDBCUrl=jdbc:mysql://127.0.0.1:3306/rpp-test
dbcp.username=rpp
dbcp.password=1234
dbcp.maxActive=30
dbcp.maxIdle=10
- 配置获取
集群中每台服务器在启动初始化阶段,首先会从上面提到的ZooKeeper配置节点上读取数据库配置信息,同时,客户端还需要在该配置节点上注册一个数据变更的Watcher监听,一旦发生节点数据变更,所有订阅的客户端都能够获取到数据变更通知。
- 配置变更
在系统运行过程中,可能会出现需要进行数据库切换的情况,这个时候就需要进行配置变更。借助ZooKeeper,我们只需要对ZooKeeper在配置节点上的内容进行更新,ZooKeeper就能够帮助我们将数据变更的通知发送到各个客户端,每个客户端在接收到这个变更通知后,就可以重新进行最新数据的获取。
Dubbo 基于 ZooKeeper 实现的注册中心
首先说明的是 在Dubbo 中 ZooKeeper 是做为注册中心使用的,以com.foo.BarService这个服务为例,生产者发布这个服务的所有地址,消费者去注册中心获取生产者地址来选择调用,可以看做是com.foo.BarService这个服务的发布/订阅,但需要注意的是这里通知并没有使用Watcher监听而是用到了临时节点的特性。如果生产者宕机临时节点也会自动删除,消费者在生产者地址调用不通的情况下会选择新的生产者地址来调用,
/dubbo
:这是dubbo在ZooKeeper上创建的根节点;
/dubbo/com.foo.BarService
:这是服务节点,代表了Dubbo的一个服务
/dubbo/com.foo.BarService/providers
:这是服务提供者的根节点,其子节点代表了每一个服务真正的提供者;
/dubbo/com.foo.BarService/consumers
:这是服务消费者的根节点,其子节点代表每一个服务真正的消费者;
服务提供者
服务提供者会在初始化启动时:
首先在 ZooKeeper 的 /dubbo/com.foo.BarService/providers
节点下创建一个子节点。写入自己的URL地址,这就代表了com.foo.BarService这个服务的一个提供者。
服务消费者
服务消费者会在启动时:
读取并订阅 ZooKeeper 上 /dubbo/com.foo.BarService/providers
节点下的所有子节点。
解析出所有提供者的URL地址来作为该服务地址列表,然后发起正常调用。同时,服务消费都还会在 ZooKeeper 的 /dubbo/com.foo.BarService/consumers
节点下创建一个临时节点,并写入自己的URL地址,这就代表了com.foo.BarService这个服务的一个消费者。
dubbo的负载均衡策略由消费端负责实现的,主要有以下几种:
- RandomLoadBalance:随机负载均衡。随机的选择一个。是Dubbo的默认负载均衡策略。
- RoundRobinLoadBalance:轮询负载均衡。轮询选择一个。
- LeastActiveLoadBalance:最少活跃调用数,相同活跃数的随机。活跃数指调用前后计数差。使慢的 Provider
收到更少请求,因为越慢的 Provider 的调用前后计数差会越大。 - ConsistentHashLoadBalance:一致性哈希负载均衡。相同参数的请求总是落在同一台机器上。
监控中心
监控中心是 Dubbo 中服务治理体系的重要一部分,属于我们下面讲到的集群管理的应用场景。它需要知道一个服务的所有提供者和消费者(订阅者),及其变化情况。因此,监控中心在启动时:通过ZooKeeper 的/dubbo/com.foo.BarService
节点来获取所有提供者和消费者的URL地址。并注册Watcher来监听其子节点的变化。
所有提供者在ZooKeeper上创建的节点都是临时节点,利用的是临时节点的生命周期和客户端会话相关特性,来感知服务提供者的变化。一旦服务提供者所有的机器出现故障导致该提供者无法对外提供服务时,该临时节点就会自动从ZooKeeper上删除,这样服务的消费者和监控中心都能感知到服务提供者的变化。
在ZooKeeper节点结构设计上,以服务名和类型(角色类型)作为节点路径,符合Dubbo订阅和通知的需求,这样保证了以服务为粒度的变更通知,通知范围易于控制,即使在服务和提供者和消费者变更频繁的情况下,也不会对ZooKeeper 造成太大的性能影响。
负载均衡
这里说的负载均衡是指软负载均衡。在分布式环境中,为了保证高可用性,通常同一个应用或同一个服务的提供方都会部署多份,达到对等服务。而消费者就须要在这些对等的服务器中选择一个来执行相关的业务逻辑,其中比较典型的是消息中间件中的生产者,消费者负载均衡。LinkedIn开源的Kafka和阿里开源的MetaQ都是通过ZooKeeper来做到生产者、消费者的负载均衡。这里以Kafka为例如讲下:
传统的发布订阅模型如下图所示:
在Kafka中有几个关键角色和概念
- Producer
消息生产者,是消息的产生源头,负责生成消息并发送给Kafka。
- Consumer
消息消费者,是消息的使用方,负责消费Kafka服务器上的消息。
- Topic
主题,由用户自定义,并配置在Kafka服务器,用于建立生产者和消费者之间的订阅关系,生产者将消息发送到指定的Topic,然后消费者再从该Topic下去取消息。
- Partition
消息分区,一个Topic下面会有多个Partition,每个Partition都是一个有序队列,Partition中的每条消息都会被分配一个有序的id。
- Broker
这个其实就是Kafka服务器了,无论是单台Kafka还是集群,被统一叫做Broker,有的资料上把它翻译为代理或经纪人。
- Group
消费者分组,将同一类的消费者归类到一个组里。在Kafka中,多个消费者共同消费一个Topic下的消息,每个消费者消费其中的部分消息,这些消费者就组成了一个分组,拥有同一个组名。
流程如下图所示
Broker注册
Broker是分布式部署并且相互之间相互独立,但是需要有一个注册系统能够将整个集群中的Broker管理起来,此时就使用到了Zookeeper。在Zookeeper上会有一个专门用来进行Broker服务器列表记录的节点:/brokers/ids
每个Broker在启动时,都会到Zookeeper上进行注册,即到/brokers/ids下创建属于自己的节点,如/brokers/ids/[0...N]
。
Topic注册
在Kafka中,同一个Topic的消息会被分成多个分区并将其分布在多个Broker上,这些分区信息及与Broker的对应关系也都是由Zookeeper在维护,由专门的节点来记录,如:/borkers/topics
Kafka中每个Topic都会以/brokers/topics/[topic]
的形式被记录,如/brokers/topics/login
和/brokers/topics/search
等。Broker服务器启动后,会到对应Topic节点(/brokers/topics
)上注册自己的Broker ID并写入针对该Topic的分区总数,如/brokers/topics/login/3->2
,这个节点表示Broker ID为3的一个Broker服务器,对于"login"这个Topic的消息,提供了2个分区进行消息存储,同样,这个分区节点也是临时节点。
生产者负载均衡
当Broker启动时,会注册该Broker的信息,以及可订阅的topic信息。生产者通过注册在Broker以及Topic上的Watcher动态的感知Broker以及Topic的分区情况,从而将Topic的分区动态的分配到broker上。
消费者与分区的对应关系
对于每个消费者分组,Kafka都会为其分配一个全局唯一的Group ID,分组内的所有消费者会共享该ID, Kafka还会为每个消费者分配一个consumer ID,通常采用hostname:uuid的形式。
在kafka的设计中规定,对于topic的每个分区,最多只能被一个消费者进行消费,也就是消费者与分区的关系是一对多的关系。消费者与分区的关系也被存储在ZooKeeper中节点的路劲为 /consumers/[group_id]/owners/[topic]/[broker_id-partition_id]
,该节点的内容就是消费者的Consumer ID。
消费者负载均衡
消费者服务启动时,会创建一个属于消费者节点的临时节点,节点的路径为/consumers/[group_id]/ids/[consumer_id]
,该节点的内容是该消费者订阅的Topic信息。每个消费者会对/consumers/[group_id]/ids
节点注册Watcher监听器,一旦消费者的数量增加或减少就会触发消费者的负载均衡。消费者还会对/brokers/ids/[brokerid]
节点进行监听,如果发现服务器的Broker服务器列表发生变化,也会进行消费者的负载均衡。
集群管理
随着分布式系统规模的日益扩大,集群中的机器规模也随之变大,因此,如何更好的进行集群管理也显得越来越重要了。
所谓集群管理,包括集群监控与集群控制两大块,前者侧重对集群运行时状态的收集,后者则是对集群进行操作与控制。在日常开发和运维过程中,我们经常会有类似于如下的需求。
- 希望知道当前集群中究竟有多少机器在工作。
- 对集群中每台机器的运行时状态进行数据收集。
- 对集群中机器进行上下线操作。
在传统的基于Agent的分布式集群管理体系中,都是通过在集群中的每台机器上部署一个Agent,由这个Agent负责主动向指定的一个监控中心系统(监控中心系统负责将所有数据进行集中处理,形成一系列报表,并负责实时报警,以下简称“监控中心”)汇报自己所在机器的状态。在集群规模适中的场景下,这确实是一种在生产实践中广泛使用的解决方案,能够快速有效地实现分布式环境集群监控,但是一旦系统的业务场景增多,集群规模变大之后,该解决方案的弊端也就显现出来了。
- 大规模升级困难
- 统一的Agent无法满足多样的需求
- 编程语言多样性
利用ZooKeeper临时节点和Watcher监听的这两大特性,就可以实现另一种集群机器存活性监控的系统。例如,监控系统在/clusterServers
节点上注册一个Watcher监听,那么但凡进行动态添加机器的操作,就会在/clusterServers
节点下创建一个临时节点:/clusterServer/[Hostname]
。这样一来,监控系统就能够实时检测到机器的变动情况,至于后续处理就是监控系统的业务了。下面我们就通过在线云主机管理这两个典型例子来看看如何使用ZooKeeper实现集群管理。
在线云主机管理
在线云主机管理通常出现在那些虚拟主机提供商的应用场景中。在这类集群管理中,有很重要的一块就是集群机器的监控。这个场景通常对于集群中的机器状态,尤其是机器在线率的统计有较高的要求,同时需要能够快速的对集群中机器的变更做出响应。
在传统的实现方案中,监控系统通过某种手段(比如检测主机的指定端口)来对每台机器进行定时检测,或者每台机器自己定时向监控系统汇报我还活着。但是这种方式需要每一个业务系统的开发人员自己来处理网络通信、协议设计、调度和容灾等诸多琐碎的问题。下面来看看使用ZooKeeper实现的另一种集群机器存活性监控系统。针对这个系统,我们的需求点通常如下。
- 如何快速的统计出当前生产环境一共有多少台机器?
- 如何快速的获取到机器上/下线的情况?
- 如何实时监控集群中每台主机的运行时状态?
机器上/下线
为了实现自动化的线上运维,我们必须对机器的上/下线情况有一个全聚德监控。通常在新增机器的时候,需要首先将指定的Agent部署到这些机器上去。Agent部署启动之后,会首先向ZooKeeper的指定节点进行注册,具体的做法就是在机器列表节点下面创建一个临时子节点,例如/XAE/machine/[Hostname](下文我们以“主机节点”代表这个节点),如下图所示。
当Agent在ZooKeeper上创建完这个临时子节点后,对/XAE/machines节点关注的监控中心就会接收到子节点变更事件,即上线通知,于是就可以对这个新加入的机器开启相应的后台管理逻辑。另一方面,监控中心同样可以获取到机器下线的通知,这样便实现了对机器上/下线的检测,同时能够很容易的获取到在线的机器列表,对于大规模的扩容和容量评估都有很大的帮助。
机器监控
对于一个在线云主机系统,不仅要对机器的在线装填进行检测,还需要对机器的运行时状态进行监控。在运行的过程中,Agent会定时将主机的运行状态信息写入ZooKeeper上的主机节点,监控中心通过订阅这些节点的数据变更通知来间接的获取主机的运行时信息。
随着分布式系统规模变得越来越庞大,对集群机器的监控和管理显得越来越重要。上面提到的这种借助ZooKeeper来实现的方式,不仅能够实时的检测到集群中机器的上/下线情况,而且能够实时的获取到主机的运行时信息,从而能够构建出一个大规模集群的主机图谱。
Master选举
在分布式环境中,经常会碰到这样的应用场景:集群中的所有系统单元需要对前端业务提供数据,比如一个商品ID,或者是一个网站轮播广告的广告ID(通常出现在一些广告投放系统中)等,而这些商品ID或是广告ID往往需要从一系列的海量数据处理中计算得到——这通常是一个非常耗费I/O和CPU资源的过程。鉴于该计算过程的复杂性,如果让集群中的所有机器都执行这个计算逻辑的话,那么将耗费非常多的资源。一种比较好的方法就是只让集群中的部分,甚至只让其中的一台机器去处理数据计算,一旦计算出数据结果,就可以共享给整个集群中的其他所有客户端机器,这样可以大大减少重复劳动,提升性能。
这里我们以一个简单的广告投放系统后台场景为例来讲解这个模型。整个系统大体上可以分成客户端集群、分布式缓存系统、海量数据处理总线和ZooKeeper四个部分,如下图所示。
首先我们来看整个系统的运行机制。上图中的Client集群每天定时会通过ZooKeeper来实现Master选举。选举产生Master客户端之后,这个Master就会负责进行一系列的海量数据处理,最终计算得到一个数据结果,并将其放置在一个内存/数据库中。同时,Master还需要通知集群中其他所有的客户端从这个内存/数据库中共享计算结果。
前面介绍过创建节点d的API接口,其中提到一个很重要的特性:ZooKeeper将会保证客户端无法重复创建一个已经存在的数据节点。也就是说,如果同时有多个客户端请求创建同一个节点,那么最终一定只有一个客户端请求能够创建成功。利用这个特性,就能很容易的分布式环境中进行Master选举了。
在这个系统中,首先会在ZooKeeper上创建一个日期节点,例如“2013-09-20”,如下图所示。
客户端集群每天都会定时往ZooKeeper上创建一个临时节点,例如/master_election/2013-09-20/binding。在这个过程中,只有一个客户端能够成功创建这个节点,那么这个客户端所在的机器就成为了Master。同时,其他没有在ZooKeeper上成功创建节点的客户端,都会在节点/master_election/2013-09-20上注册一个子节点变更的Watcher,用于监控当前的Master机器是否存活,一旦发现当前的Master挂了,那么其余的客户端将会重新进行Master选举。
从上面的讲解中,我们可以看到,如果仅仅只是想实现Master选举的话,那么其实只需要有一个能够保证数据唯一性的组件即可,例如关系型数据库的主键模型就是非常不错的选择。但是,如果希望能够快速的进行集群Master动态选举,那么基于ZooKeeper来实现是一个不错的新思路。
分布式锁
分布式锁是控制分布式系统之间同步访问共享资源的一种方式。如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要通过一些互斥手段来防止彼此之间的干扰,以保证一致性,在这种情况下,就需要使用分布式锁了。
在平时的实际项目开发中,我们往往很少会去在意分布式锁,而是依赖于关系型数据库固有的排他性来实现不同进程之间的互斥,但大型分布式系统的性能瓶颈往往集中在数据库操作上。本文我们来看看ZooKeeper如何实现分布式锁,主要讲解排他锁和共享锁两类分布式锁。
排他锁
Exclusive Locks,简称X锁,又称为写锁或独占锁,是一种基本的锁类型。如果事务T1对数据对象O1加上了排他锁,那么在整个加锁期间,只允许事务T1对O1进行读取和更新操作,其它任何事务都不能再对这个数据对象进行任何类型的操作——直到T1释放了排他锁。
从上面讲解的排它锁的基本概念中,我们可以看到,排他锁的核心是如何保证当前有且仅有一个事务获得锁,并且锁被释放后,所有正在等待获取锁的事务都能够被通知到,下面我们就来看看如何借助Zookeeper实现排他锁。
定义锁
通常的Java开发编程中,有两种常见的方式可以用来定义锁,分别是synchronized机制和JDK5提供的ReentrantLock。在ZooKeeper中,是通过ZooKeeper上的数据节点来表示一个锁,如/exclusive_lock/lock
节点就可以被定义为一个锁。如下图所示:
获取锁
在需要获取排他锁时,所有的客户端都会试图通过调用create()接口,在/exclusive_lock节点下创建临时子节点/exclusive_lock/lock
。ZooKeeper会保证在所有的客户端中,最络只有一个客户端能够创建成功,那么就可以认为该客户端获取了锁。同时,所有没有获取到锁的客户端就需要到/exclusive_lock
节点上注册一个子节点变更的Watcher监听 , 以便实时监听到lock节点的变更情况。
释放锁
在“定义锁”的部分,我们已经提到,/exclusive_lock
是一个临时节点,因此在以下两种情况下,可能释放锁:
- 当前获取锁的客户端机器宕机,那么ZooKeeper上的这个临时节点就会被移除;
- 正常执行完业务逻辑后,客户端就会主动将自己创建的临时节点删除。
无论什么情况下移除了lock节点,ZooKeeper都会通知所有在/exclusive_lock节点上注册了子节点变更Watcher监听的客户端。这些客户端在接收到通知后,再次重新发起分布式锁获取,即重复“获取锁”过程,
整个排他锁的获取和释放流程如下图所示:
共享锁
Shared Locks,简单S锁,同样是一种基本的锁类型。如果事务T1对数据对象O1加上了共享锁,那么当前事务只能对O1进行读取操作,其它事务也只能对这个数据对象加共享锁——直到该数据对象上的所有共享锁都被释放,简单的说就是别人写的时候我不能读,如果别人都在读我也可以读,别人在读写的时候我都不能写。共享锁和排他锁最根本的区别在于,加上了排他锁后,数据对象只对一个事务可见,而加上了共享锁后,数据对所有事务都可见。下面看如何借助ZooKeeper来实现共享锁。
定义锁
和排他锁一样,同样是通过ZooKeeper上的数据节点来表示一个锁,是一个类似于/shared_lock/[Hostname]-请求类型-序号
的临时顺序节点,如:/shared_lock/192.168.0.1-R-00000001
,那么 这个节点就代表了一个共享锁。如图
获取锁
在需要获取共享锁时,所有客户端都会到/shared_lock这个节点下面创建一个临时顺序节点,如果当前是读请求,那么就创建例如 /shared_lock/192.168.0.1-R-00000001
的节点。如果是写请求,那么就创建例如 /shared_lock/192.168.0.1-W-00000001
的节点,如下图所示:
判断读写顺序
根据共享锁的定义,不同的事务都可以同时对同一个数据对象进行读操作,而更新操作必须在当前没有任何事务读写操作的情况下进行。基于这个原则来通过ZooKeeper的节点来确定分布式读写顺序,大概可以分为如下4个步骤:
-
创建完节点后,获取
/shared_lock
节点下的所有子节点,并对该节点注册了子节点变更Wathcher监听 -
确定自己的节点序号在所有子节点中的顺序
-
对于读请求:
如果没有比自己序号小的子节点,或是所有比自己序号小的子节点都是读请求,那么表明
自己已经成功获取到了共享锁,同时开始执行读取逻辑。
如果比自己序号小的子节点中有写请求,那么就需要进入等待;对于写请求:
如果自己不是序号最小的子节点,那么就需要进入等待。 -
接收到Watcher的通知后,重复步骤1。
释放锁
释放锁的逻辑和排他锁是一致的,这里不再赘述。整个共享锁的获取可以用下图表示。
羊群效应
上面讲解的这个共享锁实现,大体上能满足一般分布式集群竞争锁的需求,并且性能都还可以先——这里说的一般场景是只集群规模不是特别大,一般10台机器以内。但是机器规模扩大后,会有什么问题呢?我们着重来看上面“判断读写顺序“过程的步骤3,结合下图实例,看看实际运行的情况。
针对如上图所示的情况进行分析
-
192.168.0.1首先进行读操作,完成后将节点/shared_lock/192.168.0.1-R-00000001删除。
-
余下4台机器均收到这个节点移除的通知,然后重新从/shared_lock节点上获取一份新的子节点列表。
-
每台机器判断自己的读写顺序,其中192.168.0.2检测到自己序号最小,于是进行写操作,余下的机器则继续等待。
-
继续…
可以看到,192.168.0.1客户端在移除自己的共享锁后,Zookeeper发送了子节点更变Watcher通知给所有机器,然而除了给192.168.0.2产生影响外,对其他机器没有任何作用。大量的Watcher通知和子节点列表获取两个操作会重复运行,这样会造成系能鞥影响和网络开销,更为严重的是,如果同一时间有多个节点对应的客户端完成事务或事务中断引起节点小时,Zookeeper服务器就会在短时间内向其他所有客户端发送大量的事件通知,这就是所谓的羊群效应。
可以有如下改动来避免羊群效应。
- 客户端调用create接口常见类似于/shared_lock/[Hostname]-请求类型-序号的临时顺序节点。
- 客户端调用getChildren接口获取所有已经创建的子节点列表(不注册任何Watcher)。
- 如果无法获取共享锁,就调用exist接口来对比自己小的节点注册Watcher。对于读请求:向比自己序号小的最后一个写请求节点注册Watcher监听。对于写请求:向比自己序号小的最后一个节点注册Watcher监听。
- 等待Watcher通知,继续进入步骤2。
此方案改动主要在于:每个锁竞争者,只需要关注/shared_lock节点下序号比自己小的那个节点是否存在即可。
分布式队列
业界有不少分布式队列产品,不过绝大多数都是类似于ActiveMQ、Metamorphosis、Kafka和HornetQ等的消息中间件(或称为消息队列)。在这里,我们主要介绍基于ZooKeeper实现的分布式队列。分布式队列,简单的讲分为两大类,一种是常规的先入先出队列,另一种则是要等到队列元素聚集之后才统一安排执行的Barrier模型。
FIFO:先入先出
FIFO(First Input First Output,先入先出)的算法思想,以其简单明了的特点,广泛应用于计算机科学的各个方面。而FIFO队列也是一种非常典型且应用广泛的按序执行的队列模型:先进入队列的请求操作先完成后,才会开始处理后面的请求。
使用ZooKeeper实现FIFO队列,和共享锁的实现非常类似。FIFO队列就类似于一个全写的共享锁模型,大体的设计思路其实非常简单:所有客户端都会到/queue_fifo这个节点下面创建一个临时顺序节点,例如/queue_fifo/192.168.0.1-0000000001,如下图所示。
创建完节点之后,根据如下4个步骤来确定执行顺序。
- 通过调用getChildren()接口来获取/queue_fifo节点下的所有子节点,即获取队列中所有的元素。
- 确定自己的节点序号在所有子节点中的顺序。
- 如果自己不是序号最小的子节点,那么就需要进入等待,同时向比自己序号小的最后一个节点注册Watcher监听。
- 接收到Watcher通知后,重复步骤1.
Barrier:分布式屏障
Barrier愿意是指障碍物、屏障,而在分布式系统中,特指系统之间的一个协调条件,规定了一个队列的元素必须都集聚后才能统一进行安排,否则一直等待。这往往出现在那些大规模分布式并行计算的应用场景上:最终的合并计算需要基于很多并行计算的子结果来进行。这些队列其实是在FIFO队列的基础上进行了增强,大致的设计思路如下:开始时,/queue_barrier节点是一个已经存在的默认节点,并且将其节点的数据内容赋值为一个数字n来代表Barrier值,例如n=10表示只有当/queue_barrier节点下的子节点个数达到10后,才会打开Barrier。之后,所有的客户端都会到/queue_barrier节点下创建一个临时节点,例如/queue/barrier/192.168.0.1,如下图所示。
创建完节点之后,根据如下5个步骤来确定执行顺序。
- 通过调用getData()接口获取/queue_barrier节点的数据内容:10.
- 通过调用getChildren()接口获取/queue_barrier节点下的所有子节点,即获取队列中的所有元素,同时注册对子节点列表变更的Watcher监听。
- 统计子节点的个数。
- 如果子节点个数还不足10个,那么就需要进入等待。
- 接收到Watcher通知后,重复步骤2.
整个Barrier队列的工作流程,可以用下图表示。
命名服务(Naming Service)
在分布式环境中,上层应用仅仅需要一个全局唯一的名字,类似于数据库中的唯一主键。在过去,可以使用关系型数据库字段自带的auto_increment属性自动为每个记录生成一个唯一的ID,数据库会保证生成的这个ID在全局唯一。但是,随着数据规模的不断增大,分库分表随之出现,而auto_increment属性仅能针对单一表中的记录。
UUID
UUID是一个不错的全局唯一ID生成方式,能够非常方便地保证分布式环境中的唯一性。一个标准的UUID包含32位字符和4个短线。
缺点:
- 与数据库中的INT类型相比,UUID生成的字符串过长。
- 含义不明,影响问题排查和开发调试。
ZooKeeper生成唯一ID
- 所有客户端都会根据自己的任务类型,在指定类型的任务下面通过调用create()接口来创建一个顺序节点,例如创建“job-”节点。
- 节点创建完毕后,create()接口会返回一个完整的节点名,例如“job-0000000003”。
- 客户端拿到这个返回值后,拼接上type类型,例如“type2-job-0000000003”,这就可以作为一个全局唯一的ID了。
分布式通知/协调
分布式协调/通知服务是分布式系统中不可缺少的一个环节,是将不同的分布式组件有机结合起来的关键所在。对于一个在多台机器上部署运行的应用而言,通常需要一个协调者(Coordinator)来控制整个系统的运行流程,例如分布式事务的处理、机器间的互相协调等。同时,引入这样一个协调者,便于将分布式协调的职责从应用中分离出来,从而可以大大减少系统之间的耦合性,而且能够显著提高系统的可扩展性。
ZooKeeper中特有的Watcher注册与异步通知机制,能够很好的实现分布式环境下不同机器,甚至是不同系统之间的协调与通知,从而实现对数据变更的实时处理。基于ZooKeeper实现分布式协调与通知功能,从而实现对数据变更的实时处理。基于ZooKeeper实现分布式协调与通知功能,通常的做法是不同的客户端都对ZooKeeper上同一个数据节点进行Watcher注册,监听数据节点的变化(包括数据节点本身及其子节点),如果数据节点发生变化,那么所有订阅的客户端都能够接收到相应的Watcher通知,并做出相应的处理。
系统调度
使用ZooKeeper,能够实现另一种系统调度模式:一个分布式系统由控制台和一些客户端系统两部分组成,控制台的职责就是需要将一些指令信息发送给所有的客户端,以控制他们进行相应的业务逻辑。后天管理人员在控制台上做的一些操作,实际上就是修改了ZooKeeper上某些节点的数据,而ZooKeeper进一步把这些数据变更以事件通知的形式发送给了对应的订阅客户端。比如elastic-job的web控制台。