此文知识来自于:《从Paxos到Zookeeper分布式一致性原理与实践》第六章
数据发布/订阅(Pulish/Subscribe)系统,即所谓的配置中心,顾名思义就是发布者将数据发布到ZooKeeper的一个或一系列节点上,供订阅者进行数据订阅,
进而达到动态获取数据的目的,实现配置信息的集中式管理和数据的动态更新。
发布/订阅系统一般有两种设计模式,分别是推(Push)模式和拉(Pull)模式。在推模式中,服务端主动将数据更新发送给所有订阅的客户端;而拉模式则是由
客户端主动发起请求获取最新数据,通常客户端都采用定时进行轮询拉取的方式。ZooKeeper采用的是推拉结合的方式:客户端向服务端注册自己需要关注的
节点,一旦该节点的数据发生变更,那么服务端就会向相应的客户端发送Watcher事件通知,客户端接收到这个消息通知之后,需要主动到服务端获取最新的数据。
如果将配置信息存放到ZooKeeper上进行集中管理,那么通常情况下,应用在启动的时候都会主动到ZooKeeper服务端上进行一次配置信息的获取,同时,
在指定节点上注册一个Watcher监听,这样一来,但凡配置信息发生变更,服务端都会实时通知到所有订阅的客户端,从而达到实时获取最新配置信息的目的。
实例:在我们平常的应用系统开发中,经常会碰到这样的需求:系统中需要使用一些通用的配置信息,例如机器列表信息、运行时的开关配置、数据库配置信息等。
这些全局配置信息通畅具备以下3个特性:
对于这类配置信息,一般的做法通常可以选择将其存储在本地配置文件或是内存变量中。本地配置可以采用JMX方式来实时对系统运行时内存变量的更新。
但是一旦机器规模变大,且配置信息变更频繁后,就需要寻找一种更为分布式化的解决方案。
下面以“数据库切换”的应用场景展开,看看如何使用ZooKeeper实现配置管理。
进行配置之前,首先初始化配置存储到ZooKeeper上去,例如/app1/databse_config/
(以下简称“配置节点”),写入数据节点中:
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
集群中每台机器在启动初始化阶段,首先会从上面提到的ZooKeeper配置节点上读取数据库信息,同时,客户端还需要在该配置节点上注册一个数据变更的
Water监听,一旦发生节点数据变更,所有订阅的客户端都能够获取到数据变更通知。
在系统运行过程中,可能会出现需要机型数据库切换的情况,借助ZooKeeper的Watcher机制,帮我们将数据变更的通知发送到各个客户端,每个客户端在
接收到这个变更通知后,就可以重新进行最新数据的获取。
根据维基百科上的定义,负载均衡(Load Balance)是一种相当常见的计算机网络技术,用来对多个计算机(计算机集群)、网络连接、CPU、磁盘驱动器或是
其它资源进行分配负载,以达到优化资源使用、最大化吞吐率、最小化响应时间和避免过载的目的。通常负载均衡可以分为硬件和软件负载均衡两种,本节
主要探讨的是ZooKeeper在“软”负载均衡中的应用场景。
在分布式系统中,负载均衡更是一种普遍的技术,基本上每一个分布式系统都需要使用负载均衡。在本书第一章讲解分布式系统特征的时候,我们提到,分布式系统具有
对等性,为了保证系统的高可用性,通常采用副本的方式来对数据和服务进行部署。而对于消费者而言,则需要在这些对等的服务提供方中选择一个来执行相关的业务
逻辑,其中比较典型的就是DNS服务。在本节中,我们将详细介绍如何使用ZooKeeper来解决负载均衡问题(请看深入分析Java_Web技术的第一章)。
DNS是域名系统(Domain Name System)的缩写。DNS系统可以看作是一个超大规模的分布式映射表,用于将域名和IP地址进行一一映射,进而方便人们
通过域名来访问互联网站点。
通常情况下,我们可以向域名注册服务商申请域名注册,但是这种方式最大的缺陷在于只能注册有限的域名:
日常开发过程中,经常会碰到这样的情况,在一个Company1公司内部,需要给一个App1应用的服务器集群机器配置一个域名解析。相信有过一线开发
经验的读者一定知道,这个时候通常需要由类似于app1.company1.com的一个域名,其对应的就是一个服务器地址。如果系统数量不多,那么通过
这种传统的DNS配置方式还可以应付,但是,一旦公司规模变大,各类应用层出不穷,那么就很难再通过这种方式来进行统一的管理了。
因此,在实际开发中,往往使用本地HOST绑定来实现域名解析的工作。具体如何进行本地HOST绑定,因为不是本书的重点,并且互联网上有大量额资料,
因此这里不再多说明。使用本地HOST绑定的方法,可以很容易解决域名紧张的问题,基本上每一个系统都可以自行确定系统的域名与目标IO地址。大大提高了
开发调试效率。(就是修改HOST文件,让域名与IP直接映射,减去解析时间)然而,这种看上去完美的方案,也有其致命的缺陷:
当应用的机器规模在一定范围内,并且域名的变更不是特别频繁时,本地HOST绑定是非常高效且简单的方式。然而一旦机器规模变大后,就常常
会碰到这样的情况:我们在应用上线的时候,需要在应用的每台机器上去绑定域名,但是在机器规模相当庞大的情况下,这种做法就相当不方便。
另外,如果想要临时更新域名,还需要到每个机器上去逐个进行变更,更消耗大量时间,因此完全无法保证实时性。
现在,我们来介绍一种基于ZoKeeper实现的动态DNS方案(简称“DDNS”,Dynamic DNS)。
首先需要在ZooKeeper上创建一个节点来进行域名配置。
这样,在/DDNS/app1
的节点上,将自己的域名配置上去,并支持多个IP
192.168.0.1:8080, 192.168.0.2:8080
在传统的DNS解析中,我们都不需要关系域名的解析过程,所有这些工作都交给了操作系统的域名和IP地址映射机制(本地HOST绑定)或是专门的域名解析
服务器(由域名注册服务商提供)。因此,在这点上,DDNS方案和传统的域名解析有很大的区别————在DDNS中,域名的解析过程都是由每一个应用自己负责的。
通常应用都会首先从域名节点中获取一份IP地址和端口的配置,进行自行解析。同时,每个应用还会在域名节点上注册一个数据变更Watcher监听,以便及时
收到域名变更的通知。
在运行过程中,难免会碰上域名对应的IP地址或是端口变更,这个时候就需要进行域名变更操作。在DDNS中,我们只需要对指定的域名节点进行更新操作,
ZooKeeper就会向订阅的客户端发送这个事件通知,应用在接收到这个事件通知后,就会再次进行域名配置的获取。
上面我们介绍了如何使用ZooKeeper来实现一种动态的DNS系统。通过ZooKeeper来实现动态DNS服务,一方面,可以避免域名数量无限增长带来的集中式维护
的成本;另一方面,在域名变更的情况下,也能够避免因逐台机器更新本地HOST而带来的繁琐工作。
根据上面的讲解,相信读者基本上已经能够使用ZooKeeper来实现一个动态的DNS服务了。但是我们仔细看一下上面的实现就会发现,在域名变更环节中,当
域名对应的I地址发生变更的时候,我们还是需要人为地介入去修改域名节点上的IP地址和端口。接下来我们看看下面这种使用ZooKeeper实现的更为自动化
的DNS服务。自动化的DNS服务系统主要是为了实现服务的自动化定位。
首先来介绍整个动态DNS系统的架构体系中比较重要的组件及其职责。
整个系统的核心当然是ZooKeeper集群,负责数据的存储以及一系列分布式协调。下面我们再来详细地看下整个系统是如何运行的。在这个架构模型中,我们
将那些目标IP地址和端口抽象为服务的提供者,而那些需要使用域名解析的客户端则被抽象成服务的消费者。
.1 域名注册
域名注册主要是针对服务提供者来说的。域名注册过程可以简单地概括为:每个服务提供者在启动的过程中,都会把自己的域名信息注册到Register Cluster中去。
.2 域名解析
域名解析是针对服务消费者来说的,正好和域名注册过程相反:服务消费者在使用域名的时候,会向Dispatcher发出域名解析请求。Dispatcher收到请求后,
会从ZooKeeper上的指定域名节点读取相应的IP:PORT列表,通过一定的策略选取其中一个返回给前端应用。
.3 域名探测
域名探测是指DDNS系统需要对域名下所有注册的IP地址和端口的可用性进行检测,俗称“健康度检测”。健康度检测一般有两种方式,第一种是服务端主动发起健康度心跳
检测,这种方式一般需要在服务端和客户端之间建立起一个TCP长链接;第二种则是客户端主动向服务端发起健康度心跳检测。在DDNS架构中的域名探测,使用
的是服务提供者都会定时向Scanner进行状态汇报(即第二种健康度检测方式)的模式,即每个服务提供者后都会定时向Scanner汇报自己的状态。
Scanner会负责记录每个服务提供者最近一次的状态汇报时间,一旦超过5秒没有收到状态汇报,那么就认为该IP地址和端口已经不可用,于是开始进行域名
清理过程。在域名清理过程中,Scanner会在ZooKeeper中找到该域名对应的域名节点,然后将该IP地址和端口配置从节点内容中移除。
命名服务(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的优势自然不必多说,我们重点来看看它的缺陷。
接下来,我们结合一个分布式任务调度系统来看看如何使用ZooKeeper来实现这类全局唯一ID的生成。
通过ZooKeeper节点创建的API接口可以创建一个顺序节点,并且在API返回值中会返回这个节点的完整名字。利用这个特性,我们就可以借助ZooKeeper来生成
全局唯一的ID了。
在ZooKeeper中,每一个数据节点都能够维护一份子节点的顺序顺列,当客户单对其创建一个顺序子节点的时候ZooKeeper会自动以后缀的形式在其子节点上
添加一个序号,在这个场景中就是利用了ZooKeeper的这个特性。以下为博主测试:
另外如果子节点过多,导致连接读取超时,可以适当提高配置中的initLimit以及syncLimit的数值(10倍也是可以的)。
分布式协调/通知服务是分布系统不可缺少的环节,是将不同的分布式组件有机结合起来的关键所在。对于一个在多台机器上部署运行的应用而言,通常
需要一个协调者(Coordinator)来控制整个系统的运行流程,例如分布式事务的处理、机器间的互相协调等。同时,引入这样一个协调者,便于将分布式协调的职责从
应用中分离出来,从而可以大大减少系统之间的耦合性,而且能够显著提高系统的可扩展性。
ZooKeeper中特有的Watcher注册与异步通知机制,能够很好地实现分布式环境下不同机器,甚至是不同系统之间的协调与通知,从而实现对数据变更的实时处理。
基于ZooKeeper实现分布式协调与通知功能,通常的做法是不同的客户端都对ZooKeeper上同一个数据节点进行Watcher注册,监听数据节点的变化(包括
数据节点本身及其子节点),如果数据节点发生变化,那么所有订阅的客户端都能够接收到相应的Watcher通知,并做出相应的处理。
MySQL数据复制总线(以下简称“复制总线”)是一个实时数据复制框架,用于在不同的MySQL数据库实例之间进行异步数据复制和数据变化通知。整个系统是一个由
MySQL数据库集群、消息队列系统、任务管理监控平台以及ZooKeeper集群等组件共同构成的一个包含数据生产者、复制管道和数据消息者等部分的数据总线系统。
在该系统中,ZooKeeper主要负责进行一系列的分布式协调工作,在具体的实现上,根据功能将数据复制组件划分为三个核心子模块:Core、Server和Monitor,
每个模块分别为一个单独的进程,通过ZooKeeper进行数据交换。
三个子模块之间的关系如下图:
每个模块作为独立的进程运行在服务端,运行时的数据和配置信息均保存在ZooKeeper上,Web控制台通过ZooKeeper上的数据获取到后台进程的数据,同时发布控制信息。
Core进程启动的时候,首先会向/mysql_replicator/tasks
节点(以下简称“任务列表节点”)注册任务。例如,对于一个“复制热门商品”的任务,Task
所在机器在启动的时候,会首先在任务列表节点上创建一个子节点,例如/mysql_replicator/tasks/copy_hot_time
(以下简称“任务节点”),如下图:
如果在注册过程中发现该子节点已经存在,说明已经有其他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日志消费位点。
在上文中我们主要讲解了Core组件是如何进行分布式任务协调的,接下来我们再看看Server是如何来管理Core组件的。在Mysql_Replicator中,Server主要的
工作就是进行任务的控制,通过ZooKeeper来对不同的任务进行控制与协调。Server会将每个复制任务对应生产者的元数据,即库名、表名、用户名与密码等数据库信息以及
消费者的相关信息以配置的形式写入任务节点/mysql_replicator/tasks/copy_hot_item
中去的,以便该任务的所有任务机器都能够共享该复制任务的配置。
到目前为止我们已经基本了解了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进行冷备份扫描。整个过程如下图:
从上面的讲解中,我们基本对热备份和冷备份两种运行方式都有了一定的了解,现在再来对比下这两种运行方式。在热备份方案中,针对一个任务使用了两台机器进行
热备份,借助ZooKeeper的Watcher通知机制和临时顺序节点的特性,能够非常实时地进行互相协调,但缺陷就是机器资源消耗比较大。而在冷备份方案中,采用了扫描机制,
虽然降低了任务协调的实时性,但是节省了机器资源。(博主总结冷备份与热备份的区别在于,热备份一个运行多个等待,冷备份在于一个运行,系统轮询判断是否有一个
在运行,只要有一个在运行就遍历下个任务,如果一个都没有在运行这个任务就让自己运行)。
,
在绝大部分的分布式系统中,系统机器间的通信无外乎心跳检测、工作进度汇报和系统调度这三种类型。接下来,我们将围绕这三种类型的机器通信讲解
如何基于ZooKeeper去实现一种分布式系统间的通信方式。
.1 心跳监测
机器间的心跳检测机制是指在分布式环境中,不同机器之间需要检测到彼此是否在正常运行,例如A机器需要知道B机器是否正常运行。在传统的开发中,我们
通常是通过主机之间是否可以互相PING通来判断,更复杂一点的话,则会通过在机器之间建立长连接,通过TCP连接固有的心跳检测机制来实现上层机器的心跳检测,
这些确实都是一些非常常见的心跳检测方法。而ZooKeeper基于ZooKeeper的临时节点特性,可以让不同的机器都在ZooKeeper的一个指定节点下创建临时子节点,不同的机器
之间可以根据这个临时节点来判断对应的客户端机器是否存活。通过这种方式,检测系统和被检测系统之间并不需要直接相关联,而是通过ZooKeeper上的
某个节点进行关联,大大减少了系统耦合。
.2 工作进度汇报
在一个常见的任务分发系统中,通常任务被分发到不同的机器上执行后,需要实时地将自己的任务执行进度汇报给分发系统。这个时候就可以通过ZooKeeper来实现。
在ZooKeeper上选择一个节点,每个任务客户端都在这个节点下面创建临时子节点,这样便可以实现两个功能:
.3 系统调度
使用ZooKeeper,能够实现另一种调度模式:一个分布式系统由控制台和一些客户端系统两部分组成,控制台的职责就是需要将一些指令信息发送给所有的
客户端,以控制它们进行相应的业务逻辑。后台管理人员在控制台上做的一些操作,实际上就是修改了ZooKeeper上某些节点的数据,而ZooKeeper进一步
把这些数据变更以事件通知的形式发送给了对应的订阅客户端。
总之,使用ZooKeeper来实现分布式系统机器间的通信,不仅能省去大量底层网络通信和协议设计上重复的工作,更为重要的一点是大大降低了系统之间的耦合,
能够非常方便地实现异构系统之间的灵活通信。
作者:李文文丶
链接:https://www.jianshu.com/p/52ed785f1d07
来源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。