六、Zookeeper的典型应用场景

1 数据发布/订阅

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

发布/订阅系统一般有两种设计模式,分别是推(Push)模式和拉(Pull)模式。在推模式中,服务端主动将数据更新发送给所有订阅的客户端;而拉模式则是由 客户端主动发起请求获取最新数据,通常客户端都采用定时进行轮询拉取的方式。ZooKeeper采用的是推拉结合的方式:客户端向服务端注册自己需要关注的 节点,一旦该节点的数据发生变更,那么服务端就会向相应的客户端发送Watcher事件通知,客户端接收到这个消息通知之后,需要主动到服务端获取最新的数据。

如果将配置信息存放到ZooKeeper上进行集中管理,那么通常情况下,应用在启动的时候都会主动到ZooKeeper服务端上进行一次配置信息的获取,同时, 在指定节点上注册一个Watcher监听,这样一来,但凡配置信息发生变更,服务端都会实时通知到所有订阅的客户端,从而达到实时获取最新配置信息的目的。

实例:在我们平常的应用系统开发中,经常会碰到这样的需求:系统中需要使用一些通用的配置信息,例如机器列表信息、运行时的开关配置、数据库配置信息等。 这些全局配置信息通畅具备以下3个特性:

数据量通常比较小。
数据内容在运行时会发生动态变化。
集群中各机器共享,配置一致。
对于这类配置信息,一般的做法通常可以选择将其存储在本地配置文件或是内存变量中。本地配置可以采用JMX方式来实时对系统运行时内存变量的更新。

但是一旦机器规模变大,且配置信息变更频繁后,就需要寻找一种更为分布式化的解决方案。

下面以“数据库切换”的应用场景展开,看看如何使用ZooKeeper实现配置管理。

1.1 配置存储

进行配置之前,首先初始化配置存储到ZooKeeper上去,例如/app1/databse_config/(以下简称“配置节点”),写入数据节点中:

1
2
3
4
5
6
7
8
dbcp.driverClassName=com.mysql.jdbc.Driver
dbcp.dbJDBCUrl=jdbc:mysql://localhost:3306/taokeeper
dbcp.characterEncoding=GBK
dbcp.username=admin
dbcp.password=root
dbcp.maxActive=30
dbcp.maxIdle=10
dbcp.maxWait=10000

1.2 配置获取

集群中每台机器在启动初始化阶段,首先会从上面提到的ZooKeeper配置节点上读取数据库信息,同时,客户端还需要在该配置节点上注册一个数据变更的 Water监听,一旦发生节点数据变更,所有订阅的客户端都能够获取到数据变更通知。

1.3 配置变更

在系统运行过程中,可能会出现需要机型数据库切换的情况,借助ZooKeeper的Watcher机制,帮我们将数据变更的通知发送到各个客户端,每个客户端在 接收到这个变更通知后,就可以重新进行最新数据的获取。

2 负载均衡

根据维基百科上的定义,负载均衡(Load Balance)是一种相当常见的计算机网络技术,用来对多个计算机(计算机集群)、网络连接、CPU、磁盘驱动器或是 其它资源进行分配负载,以达到优化资源使用、最大化吞吐率、最小化响应时间和避免过载的目的。通常负载均衡可以分为硬件和软件负载均衡两种,本节 主要探讨的是ZooKeeper在“软”负载均衡中的应用场景。

在分布式系统中,负载均衡更是一种普遍的技术,基本上每一个分布式系统都需要使用负载均衡。在本书第一章讲解分布式系统特征的时候,我们提到,分布式系统具有 对等性,为了保证系统的高可用性,通常采用副本的方式来对数据和服务进行部署。而对于消费者而言,则需要在这些对等的服务提供方中选择一个来执行相关的业务 逻辑,其中比较典型的就是DNS服务。在本节中,我们将详细介绍如何使用ZooKeeper来解决负载均衡问题(请看深入分析Java_Web技术的第一章)。

2.1 一种动态的DNS服务

DNS是域名系统(Domain Name System)的缩写。DNS系统可以看作是一个超大规模的分布式映射表,用于将域名和IP地址进行一一映射,进而方便人们 通过域名来访问互联网站点。

通常情况下,我们可以向域名注册服务商申请域名注册,但是这种方式最大的缺陷在于只能注册有限的域名:

日常开发过程中,经常会碰到这样的情况,在一个Company1公司内部,需要给一个App1应用的服务器集群机器配置一个域名解析。相信有过一线开发 经验的读者一定知道,这个时候通常需要由类似于app1.company1.com的一个域名,其对应的就是一个服务器地址。如果系统数量不多,那么通过 这种传统的DNS配置方式还可以应付,但是,一旦公司规模变大,各类应用层出不穷,那么就很难再通过这种方式来进行统一的管理了。

因此,在实际开发中,往往使用本地HOST绑定来实现域名解析的工作。具体如何进行本地HOST绑定,因为不是本书的重点,并且互联网上有大量额资料, 因此这里不再多说明。使用本地HOST绑定的方法,可以很容易解决域名紧张的问题,基本上每一个系统都可以自行确定系统的域名与目标IO地址。大大提高了 开发调试效率。(就是修改HOST文件,让域名与IP直接映射,减去解析时间)然而,这种看上去完美的方案,也有其致命的缺陷:

当应用的机器规模在一定范围内,并且域名的变更不是特别频繁时,本地HOST绑定是非常高效且简单的方式。然而一旦机器规模变大后,就常常 会碰到这样的情况:我们在应用上线的时候,需要在应用的每台机器上去绑定域名,但是在机器规模相当庞大的情况下,这种做法就相当不方便。 另外,如果想要临时更新域名,还需要到每个机器上去逐个进行变更,更消耗大量时间,因此完全无法保证实时性。

现在,我们来介绍一种基于ZoKeeper实现的动态DNS方案(简称“DDNS”,Dynamic DNS)。

2.2 域名配置

首先需要在ZooKeeper上创建一个节点来进行域名配置。

这样,在/DDNS/app1的节点上,将自己的域名配置上去,并支持多个IP

2.3 域名解析

在传统的DNS解析中,我们都不需要关系域名的解析过程,所有这些工作都交给了操作系统的域名和IP地址映射机制(本地HOST绑定)或是专门的域名解析 服务器(由域名注册服务商提供)。因此,在这点上,DDNS方案和传统的域名解析有很大的区别————在DDNS中,域名的解析过程都是由每一个应用自己负责的。 通常应用都会首先从域名节点中获取一份IP地址和端口的配置,进行自行解析。同时,每个应用还会在域名节点上注册一个数据变更Watcher监听,以便及时 收到域名变更的通知。

2.4 域名变更

在运行过程中,难免会碰上域名对应的IP地址或是端口变更,这个时候就需要进行域名变更操作。在DDNS中,我们只需要对指定的域名节点进行更新操作, ZooKeeper就会向订阅的客户端发送这个事件通知,应用在接收到这个事件通知后,就会再次进行域名配置的获取。

上面我们介绍了如何使用ZooKeeper来实现一种动态的DNS系统。通过ZooKeeper来实现动态DNS服务,一方面,可以避免域名数量无限增长带来的集中式维护 的成本;另一方面,在域名变更的情况下,也能够避免因逐台机器更新本地HOST而带来的繁琐工作。

2.5 自动化的DNS服务

根据上面的讲解,相信读者基本上已经能够使用ZooKeeper来实现一个动态的DNS服务了。但是我们仔细看一下上面的实现就会发现,在域名变更环节中,当 域名对应的I地址发生变更的时候,我们还是需要人为地介入去修改域名节点上的IP地址和端口。接下来我们看看下面这种使用ZooKeeper实现的更为自动化 的DNS服务。自动化的DNS服务系统主要是为了实现服务的自动化定位。

首先来介绍整个动态DNS系统的架构体系中比较重要的组件及其职责。

  • Register Cluster:负责域名的动态注册。
  • Dispatcher Cluster:负责域名解析。
  • Scanner Cluster:负责检测以及维护服务状态(探测服务的可用性、屏蔽异常服务节点等)。
  • SDK:提供各种语言的系统接入协议,提供服务注册以及查询接口。
  • Monitor:负责收集服务信息以及对DDNS自身状态的监控。
  • Controller:后台管理的Console,负责授权管理、流量控制、静态配置服务和手动屏蔽服务等功能,运维人员在上面管理Register、Dispatcher和Scanner等Cluster。

整个系统的核心当然是ZooKeeper集群,负责数据的存储以及一系列分布式协调。下面我们再来详细地看下整个系统是如何运行的。在这个架构模型中,我们 将那些目标IP地址和端口抽象为服务的提供者,而那些需要使用域名解析的客户端则被抽象成服务的消费者。

2.5.1 域名注册

域名注册主要是针对服务提供者来说的。域名注册过程可以简单地概括为:每个服务提供者在启动的过程中,都会把自己的域名信息注册到Register Cluster中去。

服务提供者通过SDK提供的API接口,将域名、IP地址和端口发送给Register Cluster。例如,A机器用于提供serverA.xxx.com,于是它就向Register 发送一个“域名->IP:PORT”的映射:“serverA.xxx.com->192.168.0.1:8080”。
Register获取到域名、IP地址和端口配置后,根据域名将信息写入相对应的ZooKeeper域名节点中。

2.5.2 域名解析

域名解析是针对服务消费者来说的,正好和域名注册过程相反:服务消费者在使用域名的时候,会向Dispatcher发出域名解析请求。Dispatcher收到请求后, 会从ZooKeeper上的指定域名节点读取相应的IP:PORT列表,通过一定的策略选取其中一个返回给前端应用。

2.5.3 域名探测

域名探测是指DDNS系统需要对域名下所有注册的IP地址和端口的可用性进行检测,俗称“健康度检测”。健康度检测一般有两种方式,第一种是服务端主动发起健康度心跳 检测,这种方式一般需要在服务端和客户端之间建立起一个TCP长链接;第二种则是客户端主动向服务端发起健康度心跳检测。在DDNS架构中的域名探测,使用 的是服务提供者都会定时向Scanner进行状态汇报(即第二种健康度检测方式)的模式,即每个服务提供者后都会定时向Scanner汇报自己的状态。

Scanner会负责记录每个服务提供者最近一次的状态汇报时间,一旦超过5秒没有收到状态汇报,那么就认为该IP地址和端口已经不可用,于是开始进行域名 清理过程。在域名清理过程中,Scanner会在ZooKeeper中找到该域名对应的域名节点,然后将该IP地址和端口配置从节点内容中移除。

3 命名服务

命名服务(Name Service)也是分布式系统中比较常见的一类场景。在分布式系统中,被命名的实体通常可以是集群中的机器、提供的服务地址或远程对象等———— 这些我们都可以统称它们为名字(Name),其中较为常见的就是一些分布式服务框架(如RPC、RMI)中的服务地址列表,通过使用命名服务, 客户端应用能够根据指定名字来获取资源的实体、服务地址和提供者的信息等。

Java语言中的JNDI便是一种典型的命名服务。JNDI是Java命名与目录接口(Java Naming and Directory Interface)的缩写,是J2EE体系中重要的规范之一, 标准的J2EE容器都提供了对JNDI规范的实现。因此,在实际开发中,开发人员常常使用应用服务器自带的JNDI实现来数据源的配置与管理————使用JNDI方式后, 开发人员可以完成不需要关心与数据库相关的任何信息,包括数据库类型、JDBC驱动类型以及数据库账号等。

ZooKeeper提供的命名服务功能与JNDI技术有相似的地方,都能够帮助应用系统通过一个资源引用的方式来实现对资源的定位与使用。另外,广义上命名服务 的资源定位都不是真正意义的实体资源————在分布式环境中,上层应用仅仅需要一个全局唯一的名字,类似于数据库中的唯一主键。下面我们来看看如何使用 ZooKeeper来实现一套分布式全局唯一ID的分配机制。

所谓ID,就是一个能够唯一标识某个对象的标识符。在我们熟悉的关系型数据库中,各个表都需要一个主键来唯一标识每条数据库记录,这个主键就是这样的唯一ID。 在过去的单库单表型系统中,通常可以使用数据库字段自带的auto_increment属性来自动为每条数据库记录生成一个唯一的ID,数据库会保证生成的这个ID 在全局唯一。但是随着数据库数据规模的不断增大,分库分表随之出现,而auto_increment属性仅能针对单一表中的记录自动生成ID,因此在这种情况下, 就无法再依靠数据库的auto_increment属性来唯一标识一条记录了。于是,我们必须寻求一种能够在分布式环境下生成全局唯一ID的方法。

一说起全局唯一ID,相信读者都会联想到UUID。没错,UUID是通用唯一识别码(Universally Unique Identifier)的简称,是一种在分布式系统中广泛 使用的用于唯一标识元素的标准,最典型的实现是GUID(Globally Unique Identifier,全局唯一标识符),主流ORM框架Hibernate有对UUID的直接支持。

确实,UUID是一个非常不错的全局唯一ID生成方式,能够非常简便地保证分布式环境中的唯一性。一个标准的UUID是一个包含32位字符和4个短线的字符串, 例如“asd321a-sd-sdwds321d5w4a2-w5e4w51d”。UUID的优势自然不必多说,我们重点来看看它的缺陷。

长度过长:与数据库中的INT类型相比,存储一个UUID需要花费更多得空空间。
含义不明:影响问题排查和开发调试的效率。
接下来,我们结合一个分布式任务调度系统来看看如何使用ZooKeeper来实现这类全局唯一ID的生成。

通过ZooKeeper节点创建的API接口可以创建一个顺序节点,并且在API返回值中会返回这个节点的完整名字。利用这个特性,我们就可以借助ZooKeeper来生成全局唯一的ID了。

所有客户端都会根据自己的任务类型,在指定类型的任务下面通过调用create()接口创建一个顺序节点,例如创建“job-”节点。
节点创建完毕后,create()接口会返回一个完整的节点名,例如“job-0000000003”。
客户端拿到这个返回值后,拼接上type类型,例如“type2-job-0000000003”,这就可以作为一个全局唯一的ID了。
在ZooKeeper中,每一个数据节点都能够维护一份子节点的顺序顺列,当客户单对其创建一个顺序子节点的时候ZooKeeper会自动以后缀的形式在其子节点上 添加一个序号,在这个场景中就是利用了ZooKeeper的这个特性。以下为博主测试:

另外如果子节点过多,导致连接读取超时,可以适当提高配置中的initLimit以及syncLimit的数值(10倍也是可以的)。

4 分布式协调/通知

分布式协调/通知服务是分布系统不可缺少的环节,是将不同的分布式组件有机结合起来的关键所在。对于一个在多台机器上部署运行的应用而言,通常 需要一个协调者(Coordinator)来控制整个系统的运行流程,例如分布式事务的处理、机器间的互相协调等。同时,引入这样一个协调者,便于将分布式协调的职责从 应用中分离出来,从而可以大大减少系统之间的耦合性,而且能够显著提高系统的可扩展性。

ZooKeeper中特有的Watcher注册与异步通知机制,能够很好地实现分布式环境下不同机器,甚至是不同系统之间的协调与通知,从而实现对数据变更的实时处理。 基于ZooKeeper实现分布式协调与通知功能,通常的做法是不同的客户端都对ZooKeeper上同一个数据节点进行Watcher注册,监听数据节点的变化(包括 数据节点本身及其子节点),如果数据节点发生变化,那么所有订阅的客户端都能够接收到相应的Watcher通知,并做出相应的处理。

4.1 MySQL数据复制总线:MySQL_Replicator

MySQL数据复制总线(以下简称“复制总线”)是一个实时数据复制框架,用于在不同的MySQL数据库实例之间进行异步数据复制和数据变化通知。整个系统是一个由 MySQL数据库集群、消息队列系统、任务管理监控平台以及ZooKeeper集群等组件共同构成的一个包含数据生产者、复制管道和数据消息者等部分的数据总线系统。

在该系统中,ZooKeeper主要负责进行一系列的分布式协调工作,在具体的实现上,根据功能将数据复制组件划分为三个核心子模块:Core、Server和Monitor, 每个模块分别为一个单独的进程,通过ZooKeeper进行数据交换。

Core实现了数据复制的核心逻辑,其将数据复制封装成管道,并抽象出生产者和消费者两个概念,其中生产者通常是MySQL数据库的Binlog日志。
Server负责启动和停止复制任务。
Monitor负责监控任务的运行状态,如果在数据复制期间发生异常或出现故障会进行告警。
三个子模块之间的关系如下图:

每个模块作为独立的进程运行在服务端,运行时的数据和配置信息均保存在ZooKeeper上,Web控制台通过ZooKeeper上的数据获取到后台进程的数据,同时发布控制信息。

4.2 任务注册

Core进程启动的时候,首先会向/mysql_replicator/tasks节点(以下简称“任务列表节点”)注册任务。例如,对于一个“复制热门商品”的任务,Task 所在机器在启动的时候,会首先在任务列表节点上创建一个子节点,例如/mysql_replicator/tasks/copy_hot_time(以下简称“任务节点”),如下图:

如果在注册过程中发现该子节点已经存在,说明已经有其他Task机器注册了该任务,因此自己不需要再创建该节点了。

4.3 任务热备份

为了应对复制任务故障或者复制任务所在主机故障,复制组件采用“热备份”的容灾方式,即将同一个复制任务部署在不同的主机上,我们称这样的机器为“任务机器”, 主、备任务机器通过ZooKeeper互相检测运行健康状况。

为了实现上述热备方案,无论在第一步中是否创建了任务节点,每台任务机器都需要在/mysql_replicator/tasks/copy_hot_item/instances节点上 将自己的主机名注册上去。注意,这里注册的节点类型很特殊,是一个临时的顺序节点。在注册完这个子节点后,通常一个完整的节点名如下: /mysql_replicator/tasks/copy_hot_item/instances/[Hostname]-1,其中最后的序列号就是临时顺序节点的精华所在。

在完成该子节点的创建后,每台任务机器都可以获取到自己创建的节点的完成节点名以及所有子节点的列表,然后通过对比判断自己是否是所有子节点中序号最小的。 如果自己是序号最小的子节点,那么就将自己的运行状态设置为RUNNING,其余的任务机器则将自己设置为STANDBY————我们将这样的热备份策略称为“小序号优先”策略。

4.4 热备切换

完成运行状态的标识后,任务的客户端机器就能够正常工作了,其中标记为RUNNING的客户端机器进行正常的数据复制,而标记为STANDBY的客户端机器则进入待命状态。 这里所谓待命状态,就是说一旦标记为RUNNING的机器出现故障停止了任务执行,那么就需要在所有标记为STANDBY的客户端机器再次按照“小序号优先”策略来 选出RUNNING机器来执行,具体的做法就是标记为STANDBY的机器都需要在/mysql_replicator/tasks/copy_hot_item/instances节点上注册一个 “子节点列表变更”的Watcher监听,用来订阅所有任务执行机器的变化情况————一旦RUNNING机器宕机与ZooKeeper断开连接后,对应的节点就会消失, 于是其他机器也就接收到了这个变更通知,从而开始新一轮的RUNNING选举。

4.5 记录执行状态

既然使用了热备份,那么RUNNING任务机器就需要将运行时的上下文状态保留给STANDBY任务机器。在这个场景中,最主要的上下文状态就是数据复制过程中的 一些进度信息,例如Binlog日志的消费位点,因此需要将这些信息保存到ZooKeeper上以便共享。在Mysql_Replicator的设计中,选择了 /mysql_replicator/tasks/copy_hot_item/lastCommit作为Binlog日志消费位点的存储节点,RUNNING任务机器会定时向这个节点写入当前的Binlog日志消费位点。

4.6 控制台协调

在上文中我们主要讲解了Core组件是如何进行分布式任务协调的,接下来我们再看看Server是如何来管理Core组件的。在Mysql_Replicator中,Server主要的 工作就是进行任务的控制,通过ZooKeeper来对不同的任务进行控制与协调。Server会将每个复制任务对应生产者的元数据,即库名、表名、用户名与密码等数据库信息以及 消费者的相关信息以配置的形式写入任务节点/mysql_replicator/tasks/copy_hot_item中去的,以便该任务的所有任务机器都能够共享该复制任务的配置。

4.7 冷备切换

到目前为止我们已经基本了解了Mysql_Replicator的工作原理,现在再回过头来看上面提到的热备份。在该热备份方案中,针对一个任务,都会至少分配两台 任务机器来进行热备份,但是在一定规模的大型互联网公司中,往往有许多MySQL实例需要进行数据复制,每个数据库实例都会对应一个复制任务, 如果每个任务都进行双机热备份的话,那么显然需要消耗太多的机器。

因此我们同时设计了一种冷备份,它和热备份方案的不同点在于,对所有任务进行分组,如下:

和热备份中比较大的区别在于,Core进程被配置了所属Group(组)。举个例子来说,假如一个Core进程被标记了group1,那么在Core进程启动后,会到对应 的ZooKeeper group1节点下面获取所有的Task列表,假如找到了任务“copy_hot_item”之后,就会遍历这个Task列表的instances节点,但凡还没有子节点的, 则会创建一个临时的顺序节点:/mysql_replicator/task-groups/group1/copy_hot_item/instances/[Hostname]-1————当然,在这个过程中,其它 Core进程也会在这个instances节点下创建类似的子节点。和热备份中的“小序号优先”策略一样,顺序小的Core进程将自己标记为RUNNING,不同之处在于,其它Core 进程则会自动将自己创建的子节点删除,然后继续遍历下一个Task节点————我们将这样的过程称为“冷备份扫描”。就这样,所有Core进程在一个扫描周期内不断地对相应 的Group下面的Task进行冷备份扫描。整个过程如下图:

4.8 冷热备份对比

从上面的讲解中,我们基本对热备份和冷备份两种运行方式都有了一定的了解,现在再来对比下这两种运行方式。在热备份方案中,针对一个任务使用了两台机器进行 热备份,借助ZooKeeper的Watcher通知机制和临时顺序节点的特性,能够非常实时地进行互相协调,但缺陷就是机器资源消耗比较大。而在冷备份方案中,采用了扫描机制, 虽然降低了任务协调的实时性,但是节省了机器资源。(博主总结冷备份与热备份的区别在于,热备份一个运行多个等待,冷备份在于一个运行,系统轮询判断是否有一个 在运行,只要有一个在运行就遍历下个任务,如果一个都没有在运行这个任务就让自己运行)。 ,

4.9 一种通用的分布式系统机器间通信方式

在绝大部分的分布式系统中,系统机器间的通信无外乎心跳检测、工作进度汇报和系统调度这三种类型。接下来,我们将围绕这三种类型的机器通信讲解 如何基于ZooKeeper去实现一种分布式系统间的通信方式。

4.9.1 心跳监测

机器间的心跳检测机制是指在分布式环境中,不同机器之间需要检测到彼此是否在正常运行,例如A机器需要知道B机器是否正常运行。在传统的开发中,我们 通常是通过主机之间是否可以互相PING通来判断,更复杂一点的话,则会通过在机器之间建立长连接,通过TCP连接固有的心跳检测机制来实现上层机器的心跳检测, 这些确实都是一些非常常见的心跳检测方法。而ZooKeeper基于ZooKeeper的临时节点特性,可以让不同的机器都在ZooKeeper的一个指定节点下创建临时子节点,不同的机器 之间可以根据这个临时节点来判断对应的客户端机器是否存活。通过这种方式,检测系统和被检测系统之间并不需要直接相关联,而是通过ZooKeeper上的 某个节点进行关联,大大减少了系统耦合。

4.9.2 工作进度汇报

在一个常见的任务分发系统中,通常任务被分发到不同的机器上执行后,需要实时地将自己的任务执行进度汇报给分发系统。这个时候就可以通过ZooKeeper来实现。 在ZooKeeper上选择一个节点,每个任务客户端都在这个节点下面创建临时子节点,这样便可以实现两个功能:

通过判断临时节点是否存在来确定任务机器是否存活;
各个任务机器会实时地将自己的任务执行进度写到这个临时节点上去,以便中心系统能够实时地获取到任务的执行进度。

4.9.3 系统调度

使用ZooKeeper,能够实现另一种调度模式:一个分布式系统由控制台和一些客户端系统两部分组成,控制台的职责就是需要将一些指令信息发送给所有的 客户端,以控制它们进行相应的业务逻辑。后台管理人员在控制台上做的一些操作,实际上就是修改了ZooKeeper上某些节点的数据,而ZooKeeper进一步 把这些数据变更以事件通知的形式发送给了对应的订阅客户端。

总之,使用ZooKeeper来实现分布式系统机器间的通信,不仅能省去大量底层网络通信和协议设计上重复的工作,更为重要的一点是大大降低了系统之间的耦合, 能够非常方便地实现异构系统之间的灵活通信。

5 集群管理

随着分布式系统规模的日益扩大,集群中的机器规模也随之变大,因此,如何更好地进行集群管理也显得越来越重要了。

所谓集群管理,包括集群监控与集群控制两大块、前者侧重对集群运行时状态的收集,后者则是对集群进行操作与控制。在日常开发和运维过程中,我们经常会有 类似于如下的需求。

希望知道当前集群中究竟有多少机器在工作。
对集群中每台机器的运行时状态进行数据收集。
对集群中机器进行上下线操作。
在传统的基于Agent的分布式集群管理体系中,都是通过在集群中的每台机器上部署一个Agent,由这个Agent负责主动向指定的一个监控中心系统(监控中心 系统负责将所有数据进行集中处理,形成一系列报表,并负责实时报警,以下简称“监控中心”)汇报自己所在机器的状态。在集群规模适中的场景下,这确实 是一种在生产实践中广泛使用的解决方案,能够快速有效地实现分布式环境集群监控,但是一旦系统的业务场景增多,集群规模变大,该解决方案的弊端也就显现出来了:

大规模升级困难:以客户端形式存在的Agent,在大规模使用后,一旦遇到需要大规模升级的情况,就非常麻烦,在升级成本和升级进度的控制上面临巨大的挑战。
统一的Agent无法满足多样的需求:对于机器的CPU使用率、负载(Load)、内存使用率、网络吞吐以及磁盘容量等机器基本的物理状态,使用统一的Agent 来进行监控或许都可以满足。但是,如果需要深入应用内部,对一些业务状态进行监控,例如,在一个分布式消息中间件中,希望监控到每个消费者对消息的消费状态; 或者在一个分布式任务调度系统中,需要对每个机器上任务的执行情况进行监控。很显然,对于这些业务耦合紧密的监控需求,不适合由一个统一的Agent来提供。
编程语言多样性:随着越来越多编程语言的出现,各种异构系统层出不穷。如果使用传统的Agent方式,那么需要提供各种语言的Agent客户端。另一方面, “监控中心”在对异构系统的数据进行整合上面临巨大挑战。
ZooKeeper具有以下两大特性:

客户端如果对ZooKeeper的一个数据节点注册Watcher监听,那么当该数据节点的内容或是其子节点列表发生变更时,ZooKeeper服务器就会向订阅的 客户端发送变更通知。
对在ZooKeeper上创建的临时节点,一旦客户端与服务器之间的会话失效,那么该临时节点也就被自动清除。
利用ZooKeeper的这两大特性,就可以实现另一种集群机器存活性监控的系统。例如,监控系统在/clusterServers节点上注册一个Watcher监听, 那么但凡进行动态添加机器的操作,就会在/clusterServers节点下创建一个临时节点/clusterServers/[Hostname]。这样一来监控系统就能够实时 检测到机器的变动情况,至于后续处理就是监控系统的业务了。下面我们就通过分布式日志收集系统和在线云主机管理这两个典型例子来看看如何使用ZooKeeper 实现集群管理。

5.1 分布式日志收集系统

分布式日志收集系统的核心工作就是收集分布在不同机器上的系统日志,在这里我们重点来看分布式日志系统的收集器模块。

在一个典型的日志系统的架构设计中,整个日志系统会把所有需要收集的日志机器(下文以“日志源机器”代表此类机器)分为多个组别,每个组别对应一个收集器, 这个收集器其实就是一个后台机器(下文以“收集器机器”代表此类机器),用于收集日志。对于大规模的分布式日志收集系统场景,通常需要解决如下两个问题。

变化的日志源机器:在生产环境中,伴随着机器的变动,每个应用的机器几乎每天都是在变化的(机器硬件问题、扩容、机房迁移或是网络问题都会导致一个应用的机器变化), 也就是说每个组别中的日志源机器通常是在不断变化的。
变化的收集器机器:日志收集系统自身也会有机器的变更或扩容,于是会出现新的收集器加入或是老的收集器机器退出的情况。
上面两个问题,无论是日志源机器还是收集器机器的变更,最终都归结为一点:如何快速、合理、动态地为每个收集器分配对应的日志源机器,这也成为了整个 日志系统正确稳定运转的前提,也是日志收集过程中最大的技术挑战。在这种情况下,引入ZooKeeper是个不错的选择,下面我们来看ZooKeeper在这个 场景中的使用。

5.1.1 注册收集器机器

使用ZooKeeper来进行日志系统收集器的注册、典型做法是在ZooKeeper上创建一个节点作为收集器的根节点,例如/logs/collector(下文我们以“收集器 节点”代表该数据节点),每个收集器机器在启动的时候,都会在收集器节点下创建自己的节点,例如logs/collector/[Hostname]。

5.1.2 任务分发

待所有收集器机器都创建好自己对应的节点后,系统根据收集器节点下子节点的个数,将所有日志源机器分成对应的若干组,然后将分组后的机器列表分别写到 这些收集器机器创建的子节点(例如/logs/collector/host1)上去。这样一来,每个收集器机器都能够从自己对应的收集器节点获取日志源机器列表, 进而开始进行日志收集工作。

5.1.3 状态汇报

完成收集器机器的注册以及任务分发后,我们还要考虑到这些机器随时都有挂掉的可能。因此,针对这个问题,我们需要有一个收集器的状态汇报机制: 每个收集器机器在创建完自己的专属节点后,还需要在对应的子节点上创建一个状态子节点,例如/logs/collector/host1/status,每个收集器都需要定期向 该节点写入自己的状态信息。我们可以把这种策略看作是一种检测机制,通常收集器机器都会在这个节点写入日志收集进度信息。日志系统根据该状态子节点的最后更新时间 来判断对应的收集器机器是否存活。

5.1.4 动态分配

如果收集器机器挂掉或是扩容了,就需要动态地进行收集任务的分配。在运行过程中,日志系统始终关注着/logs/collector这个节点下所有子节点的变更, 一旦检测到有收集器机器停止汇报或是有新的收集器机器加入,就要开始进行任务的重新分配。无论是针对收集器机器停止汇报还是新机器加入的情况, 日志系统都需要将之前分配给该收集器的所有任务转移。为了解决这个问题,通常有两种做法。

5.1.4.1 全局动态分配

这是一种简单粗暴的做法,在出现收集器机器挂掉或是新机器加入的时候,日志系统需要根据新的收集器机器列表,立即对所有的日志源机器重新进行一次分组, 然后将其分配给剩下的收集器机器。

5.1.4.2 局部动态分配

全局动态分配方式虽然策略简单,但是存在一个问题:一个或部分收集器机器的变更,就会导致全局动态任务的分配,影响面比较大,因此风险也就比较大。 所谓局部动态分配,顾名思义就是在小范围内进行任务的动态分配。在这种策略中,每个收集器机器在汇报自己日志收集状态的同时,也会把自己的负载汇报上去。 请注意,这里提到的负载并不仅仅只是简单地指机器CPU负载(Load),而是一个对当前收集器任务执行的综合评估。

在这种策略中,如果一个收集器机器挂了,那么日志系统就会把之前分配给这个机器的任务重新分配到那些负载较低的机器上去。同样,如果有新的收集器机器加入, 会从那些负载高的机器上转移部分任务给这个新加入的机器。

5.1.5 注意事项

5.1.5.1 节点类型

首先看/logs/collector这个节点下面子节点的节点类型。这个节点下面的所有子节点都代表了每个收集器机器,那么初步认为这些子节点必须选择临时节点, 原因是日志系统可以根据这些临时节点来判断收集器机器的存活性。但是,同时还需要注意的一点是:在分布式日志收集这个场景中,收集器节点上还会存放所有 已经分配给该收集器机器的日志源机器列表,如果只是简单地依靠ZooKeeper自身的临时节点机制,那么当一个收集器挂掉或是当这个收集器机器中断“心跳汇报” 的时候,待该收集器节点的会话失效后,ZooKeeper就会立即删除该节点,于是,记录在该节点上的所有日志源机器列表也就随之被清除掉了。

从上面的描述中可以知道,临时节点显然无法满足这里的业务需求,所以我们选择了使用持久节点来标识每一个收集器机器,同时在这个持久节点下面分别创建 /logs/collector/[Hostname]/status节点来表征每一个收集器机器的状态。这样一来,既能实现日志系统对所有收集器的监控,同时在收集器机器挂掉 后,依然能够准确地将分配于其中的任务还原。

5.1.5.2 日志系统节点监听

在实际生产运行过程中,每一个收集器机器更改自己状态节点的频率可能非常高(如每秒1次或更短),而且收集器的数量可能非常大,如果日志系统监听所有 这些节点变化,那么通知的消息量可能会非常大。另一方面,在收集器机器正常工作的情况下,日志系统没有必要去实时地接收每次节点状态变更,因此大部分 这些变更通知都是无用的。因此我们考虑放弃监听设置,而是采用日志系统主动轮询收集器节点的策略,这样就节省了不少网卡流量,唯一的缺陷就是有 一定的延时(考虑到分布式日志收集系统的定位,这个延时是可以接受的)。

5.2 在线云主机管理

在线云主机管理通常出现在那些虚拟主机提供商的应用场景中。在这类集群管理中,有很重要的一块就是集群机器的监控。这个场景通常对于集群中的机器状态, 尤其是机器在线率的统计有较高的要求,同时需要能够快速地对集群中机器的变更做出响应。

在传统的实现方案中,监控系统通过某种手段(比如检测主机的指定端口)来对每台机器进行定时检测,或者每台机器自己定时向监控系统汇报“我还活着”。 但是这种方式需要每个业务系统的开发人员自己来处理网络通信、协议设计、调度和容灾等诸多琐碎的问题。下面来看看使用ZooKeeper实现的另一种集群机器 存活性监控系统。针对这个系统,我们的需求点通常如下。

如何快速地统计当前生产环境一共有多少台机器?
如何快速地获取到机器上/下线的情况?
如何实时监控集群中每台主机的运行时状态?

5.2.1 机器上/下线

为了实现自动化的线上运维,我们必须对机器的上/下线情况有一个全局的监控。通常在新增机器的时候,需要首先将指定的Agent部署到这些机器上去。 Agent部署启动之后,会首先向ZooKeeper的指定节点进行注册,具体的做法就是在机器列表节点下面创建一个临时子节点,例如/XAE/machine/[Hostname] (下文以“主机节点”代表这个节点),如下图:

当Agent在ZooKeeper上创建完这个临时子节点后,对/XAE/machines节点关注的监控中心就会接收到“子节点变更”事件,即上线通知,于是就可以对这个 新加入的机器开启相应的后台管理逻辑。另一方面,监控中心同样可以获取到机器下线的通知,这样便实现了对机器上/下线的检测,同时能够很容易地获取 到在线的机器列表,对于大规模的扩容和容量评估都有很大的帮助。

5.2.2 机器监控

对于一个在线云主机系统,不仅要对机器的在线状态进行检测,还需要对机器的运行时状态进行监控。在运行的过程中,Agent会定时将主机的运行状态信息 写入ZooKeeper上的主机节点,监控中心通过订阅这些节点的数据变更通知来间接地获取主机的运行时信息。

随着分布式系统规模变得越来越庞大,对集群机器的监控和管理显得越来越重要。上面提到的这种借助ZooKeeper来实现的方式,不仅能够实时地检测到集群 中机器的上/下线情况,而且能够实时地获取到主机的运行时信息,从而能够构建出一个大规模集群的主机图谱。

6 Master选举

Master选举是一个在分布式系统中非常常见的应用场景。分布式最核心的特性就是能够将具有独立计算能力的系统单元部署在不同的机器上,构成一个完整的 分布式系统。而与此同时,实际场景中往往也需要在这些分布在不同机器上的独立系统单元中选出一个所谓的“老大”,在计算机科学中,我们称之为“Master”。

在分布式系统中,Master往往用来协调集群中其他系统单元,具有对分布式系统状态变更的决定权。例如,在一些读写分离的应用场景中,客户端的写请求往往 是由Master来处理的;而在另一些场景中,Master则常常负责处理一些复杂的逻辑,并将处理结果同步给集群中其它系统单元。Master选举可以说是ZooKeeper 最典型的应用场景了,在本节中,我们就结合“一种海量数据处理与共享模型”这个具体例子来看看ZooKeeper在集群Master选举中的应用场景。

在分布式环境中,经常会碰到这样的应用场景:集群中的所有系统单元需要对前端业务提供数据,比如一个商品ID,或者是一个网站轮播广告的广告ID(通常 出现在一些广告投放系统中)等,而这些商品ID或是广告ID往往需要从一系列的海量数据处理中计算得到————这通常是一个非常耗费I/O和CPU资源的过程。 鉴于该计算过程的复杂性,如果让集群中的所有机器都执行这个计算逻辑的话,那么将耗费非常多的资源。一种比较好的方法就是只让集群中的部分,甚至只 让其中的一台机器去处理数据计算,一旦计算出数据结果,就可以共享给整个集群中的其他所有客户端机器,这样可以大大减少重复劳动,提升性能。

这里我们以一个简单的广告投放系统后台场景为例来讲解这个模型。整个系统大体上可以分成客户端集群、分布式缓存系统、海量数据处理总线和ZooKeeper 四个部分,如下图:

Client集群每天定时会通过ZooKeeper来实现Master选举。选举产生Master客户端之后,这个Master就会负责进行一系列的海量数据处理,最终计算得到 一个数据结果,并将其放置在一个内存/数据库中。同时,Master还需要通知集群中其它所有的客户端从这个内存/数据库中共享计算结果。

接下去,我们将重点来看Master选举的过程,首先来明确下Master选举的需求:在集群的所有机器中选举出一台机器作为Master。针对这个需求,通常情况 下,我们可以选择常见的关系型数据库中的主键特性来实现:集群中的所有机器都向数据库中插入一条相同主键ID的记录,数据库会帮助我们自动进行主键冲突 检查,也就是说,所有进行插入操作的客户端机器中,只有一台机器能够成功————那么,我们就认为向数据库中成功插入数据的客户端机器成为Master。

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

ZooKeeper的强一致性,能够很好地保证在分布式高并发情况下节点的创建一定能够保证全局唯一性,即ZooKeeper将会保证客户端无法重复创建一个已经存在 的数据节点。也就是说,如果同时有多个客户端请求创建同一个节点,那么最终一定只有一个客户端请求能够创建成功。利用这个特性,就能很容易地在分布式 环境中进行Master选举了。

在这个系统中,首先会在ZooKeeper上创建一个日期节点,如下图:

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

从上面的讲解中,我们可以看到,如果仅仅只是想实现Master选举的话,那么其实只需要有一个能够保证唯一性的组件即可,例如关系型数据库的主键模型 就是不错的选择。但是,如果希望能够快速地进行集群Master动态选举,那么基于ZooKeeper来实现是一个不错的新思路。

7 分布式锁

分布式锁是控制分布式系统之间同步访问共享资源的一种方式。如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的 时候,往往需要通过一些互斥手段来防止彼此之间的干扰,以保证一致性,在这种情况下,就需要使用分布式锁了。

在平时的实际项目开发中,我们往往很少会去在意分布式锁,而是依赖于关系型数据库固有的排他性来实现不同进程之间的互斥。这确实是一种非常简便且被 广泛使用的分布式锁实现方式。然而有一个不争的事实是,目前绝大多数大型分布式系统的性能瓶颈都集中在数据库操作上。因此,如果上层业务再给数据库 添加一些额外的锁,例如行锁、表锁甚至是繁重的事务处理,那么是不是会让数据库更加不堪重负呢?下面我们来看看使用ZooKeeper如何实现分布式锁, 这里主要讲解排他锁和共享锁两类分布式锁。

7.1 排他锁

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

从上面讲解的排他锁的基本概念中,我们可以看到,排他锁的核心是如何保证当前有且仅有一个事务获得锁,并且锁被释放后,所有正在等待获取锁的事务都 能够被通知到。下面我们就看看如何借助ZooKeeper实现排他锁。

7.1.1 定义锁

有两种常见的方式可以用来定义锁,分别是synchronized机制和JDK5提供的ReentrantLock。然而,在ZooKeeper中,没有类似于这样的API可以直接使用, 而是通过ZooKeeper上的数据节点来表示一个锁,例如/exclusive_lock/lock节点就可以被定义为一个锁,如下图:

7.1.2 获取锁

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

7.1.3 释放锁

由于是临时节点,有下面两种情况,可能释放锁:

  • 当前获取锁的客户端机器发生宕机
  • 正常执行完业务逻辑后,客户端主动将临时节点删除。
    无论在上面情况下移除了lock节点,ZooKeeper都会通知所有在/exclusive_lock节点上注册了子节点变更Watcher监听的客户端。这些客户端在接收到通知后, 再次重新发起分布式锁获取,即重复“获取锁”过程。如下图:

7.2 共享锁

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

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

7.2.1 定义锁

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

7.2.2 获取锁

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

7.2.3 判断读写顺序

根据共享锁的定义,不同的事务都可以同时对同一数据对象进行读取操作,而更新操作必须在当前没有任何事务进行读写操作的情况下进行。基于这个原则, 我们来看看如何通过ZooKeeper的节点来确定分布式读写顺序,大致可以分为如下4个步骤。

创建完节点后,获取/shared_lock节点下的所有子节点,并对该节点注册子节点变更的Watcher监听。
确定自己的节点序号在所有子节点中的顺序。
如果当前节点业务为读请求:如果没有比自己序号小的子节点,或是所有比自己序号小的子节点都是读请求,那么表明自己已经成功获取到了共享锁,同时 开始执行读取逻辑。如果比自己序号小的子节点有写请求,那么就需要进入等待。
如果当前节点业务为写请求:如果自己不是序号最小的子节点, 那么就需要进入等待。
接收到Watcher通知后,重复步骤1。

7.2.4 释放锁

释放锁的逻辑和排他锁是一致的。

7.2.5 羊群效应

上面讲解的这个共享锁实现,大体上能够满足一般的分布式集群竞争锁的需求,并且性能都还可以————这里说的一般场景是指集群规模不是特别大,一般是在 10台机器以内。但是如果机器规模扩大之后,会有什么问题呢?我们着重来看上面“判断读写顺序”过程的步骤3,如下图,看看实际运行中的情况。

192.168.0.1这台机器首先进行读操作,完成读操作后将节点/192.168.0.1-R-000000001删除。
余下的4台机器均收到了这个节点被移除的通知,然后重新从/shared_lock/节点上获取一份新的子节点列表。
每个机器判断自己的读写顺序。其中192.168.0.2这台机器检测到自己已经是序号最小的机器了,于是开始进行写操作,而余下的其他机器发现没有轮到 自己进行读取或更新操作,于是继续等待。
继续……
上面这个过程就是共享锁在实际运行中最主要的步骤了,我们着重看下上面步骤3中提到的:“而余下的其他机器发现没有轮到自己进行读取或更新操作,于是继续等待。” 很明显,我们看到,192.168.0.1这个客户端在移除自己的共享锁后,ZooKeeper发送了子节点变更Watcher通知给所有机器,然而这个通知除了给192.168.0.2 这台机器产生实际影响外,对于余下的其他所有机器都没有任何作用。

相信读者也已经意思到了,在这整个分布式锁的竞争过程中,大量的“Watcher通知”和“子节点列表获取”两个操作重复运行,并且绝大多数的运行结果都是 判断出自己并非是序号最小的节点,从而继续等待下一次通知————这个看起来显然不怎么科学。客户端无端地接收到过多和自己并不相关的事件通知,如果在集群 规模比较大的情况下,不仅会对ZooKeeper服务器造成巨大的性能影响和网络冲击,更为严重的是,如果同一时间有多个节点对应的客户端完成事务或是事务 中断引起节点消息,ZooKeeper服务器就会在短时间内向其余客户端发送大量的事件通知————这就是所谓的羊群效应。

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

7.2.6 改进后的分布式锁实现

现在我们来看看如何改进上面的分布式锁实现。首先,我们需要肯定的一点是,上面提到的共享锁实现,从整体思路上来说完全正确。这里主要的改动在于: 每个锁竞争者,只需要关注/shared_lock/节点下序号比自己小的那个节点是否存在即可,具体实现如下:

  1. 客户端调用create()方法创建一个类似于/shared_lock/[Hostname]-请求类型-序号的临时顺序节点。
  2. 客户端调用getChildren()接口来获取所有已经创建的子节点列表,注意,这里不注册任何Watcher。
  3. 如果无法获取共享锁,那么就调用exist()来对比自己小的那个节点注册Watcher。注意,这里“比自己小的节点”只是一个笼统的说法,具体对于读请求和写请求不一样。
    读请求:向比自己序号小的最后一个写请求节点注册Watcher监听。
    写请求:向比自己序号小的最后一个节点注册Watcher监听。
  4. 等待Watcher通知,继续进入步骤2。
    流程图如下:

7.2.7 注意

看到这里,相信很多读者都会觉得改进后的分布式锁实现相对来说比较麻烦。确实如此,如同在多线程并发编程实践中,我们会去尽量缩小锁的范围————对于 分布式锁实现的改进其实也是同样的思路。那么对于开发人员来说,是否必须按照改进后的思路来设计实现自己的分布式锁呢?答案是否定的。在具体的实际开发 过程中,我们提倡根据具体的业务场景和集群规模来选择适合自己的分布式锁实现:在集群规模不大、网络资源丰富的情况下,第一种分布式锁实现方式是 简单实用的选择;而如果集群规模达到一定程度,并且希望能够精细化地控制分布式锁机制,那么不妨试试改进版的分布式锁实现。

8 分布式队列

业界有不少分布式队列产品,不过绝大多数都是类似于ActiveMQ、Kafka等的消息中间件。在本节中,我们主要介绍基于ZooKeeper实现的分布式队列。 分布式队列,简单地讲分为两大类,一种是常规的先入先出队列,另一种则是要等到队列元素集聚之后才统一安排执行的Barrier模型。

8.1 FIFO:先进先出

使用ZooKeeper实现FIFO队列,和共享锁的实现非常类似。FIFO队列就类似于一个全写的共享锁模型,大体的设计思想其实非常简单:所有客户端都会到 /queue_fifo这个节点下面创建一个临时顺序节点,例如/queue_fifo/192.168.0.1-0000000001,如下图:

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

通过调用getChildren()接口来获取/queue_fifo节点下的所有子节点,即获取队列中所有的元素。
确定自己的节点序号在所有子节点中的顺序。
如果自己不是序号最小的子节点,那么就需要进入等待,同时向比自己序号小的最后一个节点注册Watcher监听。
接收到Watcher通知到,重复步骤1。
整个FIFO队列的工作流程,如下图:

8.2 Barrier:分布式屏障

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

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

  1. 通过调用getDate()接口获取/queue_barrier节点的数据内容:10。
  2. 通过调用getChildren()接口获取/queue_barrier节点下的所有子节点,即获取队列中所有元素,同时注册对子节点列表变更的Watcher监听。
  3. 统计子节点的个数。
  4. 如果子节点个数还不足10个,那么就需要进入等待。
  5. 接收到Watcher通知后,重复步骤2。

博主理解为,如果在很少的时间内,同时超过了10个以上的业务机创建了临时节点,那么业务处理的速度并不是恒定的,因为有可能这个业务被11个机器处理, 下一个被12个业务机处理?

9 小结

数据发布/订阅(配置中心)、负载均衡(DNS解析)、命名服务(顺序节点特性)、分布式协调/通知(Watcher机制),集群管理(子节点)、Master选举 (同时创建节点)、分布式锁(同时创建节点)、分布式队列(创建顺序节点)