ZooKeeper是一个典型的发布/订阅模式的分布式数据管理与协调框架,开发人员可以使用它来进行分布式数据的发布与订阅。另一方面,通过对ZooKeeper中丰富的数据节点类型进行交叉使用,配合Watcher事件通知机制,可以非常方便地构建一系列分布式应用中都会涉及的核心功能,如数据发布/订阅、负载均衡、命名服务、分布式协调/通知、集群管理、Master 选举、分布式锁和分布式队列等。
ZooKeeper是一个高可用的分布式数据管理与协调框架。基于对ZAB算法的实现,该框架能够很好地保证分布式环境中数据的一致性。也正是基于这样的特性,使得ZooKeeper成为了解决分布式一致性问题的利器。
1 数据发布/订阅
数据发布/订阅(Publish/Subscribe)系统,即所谓的配置中心,顾名思义就是发布者将数据发布到ZooKeeper的一个或一系列节点上,供订阅者进行数据订阅,进而达到动态获取数据的目的,实现配置信息的集中式管理和数据的动态更新。
发布/订阅系统一般有两种设计模式,分别是推(Push)模式和拉(Pull)模式。在推模式中,服务端主动将数据更新发送给所有订阅的客户端:而拉模式则是由客户端主动发起请求来获取最新数据,通常客户端都采用定时进行轮询拉取的方式。ZooKeeper采用的是推拉相结合的方式:客户端向服务端注册自己需要关注的节点,一旦该节点的数据发生变更,那么服务端就会向相应的客户端发送Watcher事件通知,客户端接收到这个消息通知之后,需要主动到服务端获取最新的数据。
如果将配置信息存放到ZooKeeper上进行集中管理,那么通常情况下,应用在启动的时候都会主动到ZooKeeper服务端上进行一次配置信息的获取,同时,在指定节点上注册一个Watcher监听,这样一来,但凡配置信息发生变更,服务端都会实时通知到所有订阅的客户端,从而达到实时获取最新配置信息的目的。下面我们通过一个“配置管理”的实际案例来展示ZooKeeper在“数据发布/订阅”场景下的使用方式。
在我们平常的应用系统开发中,经常会碰到这样的需求:系统中需要使用一些通用的配置信息,例如机器列表信息、运行时的开关配置、数据库配置信息等。这些全局配置信息通常具备以下3个特性。
(1)数据量通常比较小。
(2)数据内容在运行时会发生动态变化。
(3)集群中各机器共享,配置一致。
对于这类配置信息,一般的做法通常可以选择将其存储在本地配置文件或是内存变量中。
无论采用哪种方式,其实都可以简单地实现配置管理。如果采用本地配置文件的方式,那么通常系统可以在应用启动的时候读取到本地磁盘的一个文件来进行初始化,并且在运行过程中定时地进行文件的读取,以此来检测文件内容的变更。在系统的实际运行过程中,如果我们需要对这些配置信息进行更新,那么只要在相应的配置文件中进行修改,等到系统再次读取这些配置文件的时候,就可以读取到最新的配置信息,并更新到系统中去,这样就可以实现系统配置信息的更新。另外一种借助内存变量来实现配置管理的方式也非常简单,以Java系统为例,通常可以采用JMX方式来实现对系统运行时内存变量的更新。
从上面的介绍中,我们基本了解了如何使用本地配置文件和内存变量方式来实现配置管理。通常在集群机器规模不大、配置变更不是特别频繁的情况下,无论上面提到的哪种方式,都能够非常方便地解决配置管理的问题。但是,一旦机器规模变大,且配置信息变更越来越频繁后,我们发现依靠现有的这两种方式解决配置管理就变得越来越困难了。我们既希望能够快速地做到全局配置信息的变更,同时希望变更成本足够小,因此我们必须寻求一种更为分布式化的解决方案。
接下去我们就以一个“ 数据库切换”的应用场景展开,看看如何使用ZooKeeper来实现配置管理。
配置存储
在进行配置管理之前,首先我们需要将初始化配置存储到ZooKeeper上去。一般情况下,我们可以在ZooKeeper上选取一个数据节点用于配置的存储,例如/app1/database_config (以下简称“配置节点”),如下图所示。
我们将需要集中管理的配置信息写入到该数据节点中去,例如:
#DBCP
dbcp.driverClassName=com.mysql.jdbc.Driver
dbcp.dbJDBCUrl=jdbc:mysql://1.1.1.1:3306/taokeeper
dbcp.characterEncoding=GBK
dbcp.username=xiaoming
dbcp.password=123456
dbcp.maxActive=30
dbcp.maxIdle=10
dbcp.maxWait=10000
配置获取
集群中每台机器在启动初始化阶段,首先会从上面提到的ZooKeeper配置节点上读取数据库信息,同时,客户端还需要在该配置节点上注册一个数据变更的Watcher监听,一旦发生节点数据变更,所有订阅的客户端都能够获取到数据变更通知。
配置变更
在系统运行过程中,可能会出现需要进行数据库切换的情况,这个时候就需要进行配置变更。借助ZooKeeper,我们只需要对ZooKeeper上配置节点的内容进行更新,ZooKeeper就能够帮我们将数据变更的通知发送到各个客户端,每个客户端在接收到这个变更通知后,就可以重新进行最新数据的获取。
2 负载均衡
根据维基百科上的定义,负载均衡(Load Balance)是一种相当常见的计算机网络技术,用来对多个计算机(计算机集群)、网络连接、CPU、磁盘驱动器或其他资源进行分配负载,以达到优化资源使用、最大化吞吐率、最小化响应时间和避免过载的目的。通常负载均衡可以分为硬件和软件负载均衡两类,本文主要探讨的是ZooKeeper在“软”负载均衡中的应用场景。
在分布式系统中,负载均衡更是一种普遍的技术,基本上每一个分布式系统都需要使用负载均衡。分布式系统具有对等性,为了保证系统的高可用性,通常采用副本的方式来对数据和服务进行部署。而对于消费者而言,则需要在这些对等的服务提供方中选择一个来执行相关的业务逻辑,其中比较典型的就是DNS服务。
一种动态的DNS服务
DNS是域名系统(DomainNameSystem)的缩写,是因特网中使用最广泛的核心技术之一。DNS系统可以看作是一个超大规模的分布式映射表,用于将域名和IP地址进行一一映射,进而方便人们通过域名来访问互联网站点。
通常情况下,我们可以向域名注册服务商申请域名注册,但是这种方式最大的缺陷在于只能注册有限的域名:
日常开发过程中,经常会碰到这样的情况,在一个Company1公司内部,需要给一个App1应用的服务器集群机器配置一个域名解析。相信有过一线开发经验的读者一定知道,这个时候通常会需要有类似于app1.company1.com的一个域名,其对应的就是一个服务器地址。如果系统数量不多,那么通过这种传统的DNS配置方式还可以应付,但是,一旦公司规模变大,各类应用层出不穷,那么就很难再通过这种方式来进行统一的管理了。
因此,在实际开发中,往往使用本地HOST绑定来实现域名解析的工作。使用本地HOST绑定的方法,可以很容易解决域名紧张的问题,基本上每一个系统都可以自行确定系统的域名与目标IP地址。同时,这种方法对于开发人员最大的好处就是可以随时修改域名与IP的映射,大大提高了开发调试效率。然而,这种看上去完美的方案,也有其致命的缺陷:
当应用的机器规模在一定范围内,并且域名的变更不是特别频繁时,本地HOST绑定是非常高效且简单的方式。然而一旦机器规模变大后,就常常会碰到这样的情况:我们在应用上线的时候,需要在应用的每台机器上去绑定域名,但是在机器规模相当庞大的情况下,这种做法就相当不方便。另外,如果想要临时更新域名,还需要到每个机器上去逐个进行变更,要消耗大量时间,因此完全无法保证实时性。
现在,我们来介绍一种基于ZooKeeper 实现的动态DNS方案(以下简称该方案为“DDNS”,Dynamic DNS)。
域名配置
和配置管理一样,我们首先需要在ZooKeeper上创建一个节点来进行域名配置,例如/DDNS/appl/server.app1.company1.com (以下简称“域名节点”),如下图所示。
从上图中我们看到,每个应用都可以创建一个属于自己的数据节点作为域名配置的根节点,例如/DDNS/app/,在这个节点上,每个应用都可以将自己的域名配置上去,下列清单是一个配置示例。
#单个IP:PORT
192.168.0.1:8080
#多个IP:PORT
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而带来的繁琐工作。
自动化的DNS服务
根据上面的讲解,相信读者基本上已经能够使用ZooKeeper来实现一个动态的DNS服务了。但是我们仔细看一下上面的实现就会发现,在域名变更环节中,当域名对应的IP地址发生变更的时候,我们还是需要人为地介入去修改域名节点上的IP地址和端口。
首先来介绍整个动态DNS系统的架构体系中几个比较重要的组件及其职责。
(1)Register集群负责域名的动态注册。
(2)Dispatcher集群负责域名解析。
(3)Scanner集群负责检测以及维护服务状态(探测服务的可用性、屏蔽异常服务节点等)。
(4)SDK提供各种语言的系统接人协议,提供服务注册以及查询接口。
(5)Monitor负责收集服务信息以及对DDNS自身状态的监控。
(6)Controller是一个后台管理的Console,负责授权管理、流量控制、静态配置服务和手动屏蔽服务等功能,另外,系统的运维人员也可以在上面管理Register、Dispatcher和Scanner等集群。
整个系统的核心当然是ZooKeeper集群,负责数据的存储以及一系列分布式协调。下面我们再来详细地看下整个系统是如何运行的。在这个架构模型中,我们将那些目标IP地址和端口抽象为服务的提供者,而那些需要使用域名解析的客户端则被抽象成服务的消费者。
域名注册
域名注册主要是针对服务提供者来说的。域名注册过程可以简单地概括为:每个服务提
供者在启动的过程中,都会把自己的域名信息注册到Register集群中去。
(1)服务提供者通过SDK提供的API接口,将域名、IP地址和端口发送给Register集群。例如,A机器用于提供serviceA.xxx.com,于是它就向Register 发送一个“域名→IP:PORT”的映射:“serviceA.xxx.com →192. 168.0.1:8080”。
(2)Register获取到域名、IP地址和端口配置后,根据域名将信息写入相对应的ZooKeeper域名节点中。
域名解析
域名解析是针对服务消费者来说的,正好和域名注册过程相反:服务消费者在使用域名的时候,会向Dispatcher 发出域名解析请求。Dispatcher 收到请求后,会从ZooKeeper上的指定域名节点读取相应的IP:PORT列表,通过一定的策略选取其中一个返回给前端应用。
域名探测
域名探测是指DDNS系统需要对域名下所有注册的IP地址和端口的可用性进行检测,俗称“健康度检测”。健康度检测一般有两种方式,第一种是服务端主动发起健康度心跳检测,这种方式一般需要在服务端和客户端之间建立起一个TCP长链接;第二种则是客户端主动向服务端发起健康度心跳检测。在DDNS架构中的域名探测,使用的是服务提供者主动向Scanner进行状态汇报(即第二种健康度检测方式)的模式,即每个服务提供者都会定时向Scanner汇报自己的状态。
Scanner会负责记录每个服务提供者最近一次的状态汇报时间,一旦超过5秒没有收到状态汇报,那么就认为该IP地址和端口已经不可用,于是开始进行域名清理过程。在域名清理过程中,Scanner 会在ZooKeeper中找到该域名对应的域名节点,然后将该IP地址和端口配置从节点内容中移除。
以上就是整个DDNS系统中几个核心的工作流程,关于DDNS系统自身的监控与运维,和ZooKeeper关系不是特别大,这里就不再展开讲解了。