ZooKeeper的典型应用场景

在第5章中,我们已经向读者讲解了如何通过ZooKeeper的客户端来使用ZooKeeper。

从本章开始,我们将从实际的分布式应用场景出发,来讲解如何使用ZooKeeper去解决一些常见的分布式问题,以帮助读者更好地使用ZooKeeper。

ZooKeeper是一个典型的发布/订阅模式的分布式数据管理与协调框架,开发人员可以使用它来进行分布式数据的发布与订阅。

另一方面,通过对ZooKeeper中丰富的数据节点类型进行交叉使用,配合Watcher事件通知机制,可以非常方便地构建一系列分布式应用中都会涉及的核心功能,如数据发布/订阅、负载均衡、命名服务、分布式协调/通知、集群管理、Master选举、分布式锁和分布式队列等。在6.1节中,我们将逐一针对这些典型的分布式应用场景来做详细讲解。

当然,仅仅从理论上学习ZooKeeper的应用场景还远远不够。在6.2节中,我们还将结合Hadoop、HBase和Kafka等广泛使用的开源系统,来讲解ZooKeeper在大型分布式系统中的实际应用。

在6.3节中,会进一步通过对Metamorphosis、Dubbo和Canal等知名案例的讲解,

来向读者展现阿里巴巴集团的这些典型技术产品是如何借助ZooKeeper解决实际生产中的分布式问题的。

典型应用场景及实现

ZooKeeper是一个高可用的分布式数据管理与协调框架。

基于对ZAB算法的实现,该框架能够很好地保证分布式环境中数据的一致性。

也正是基于这样的特性,使得ZooKeeper成为了解决分布式一致性问题的利器。

随着近年来互联网系统规模的不断扩大,大数据时代飞速到来,越来越多的分布式系统将 ZooKeeper 作为核心组件使用,如 Hadoop、HBase 和 Kafka 等,因此,正确理解ZooKeeper的应用场景,对于ZooKeeper的使用者来说,显得尤为重要。

本节将重点围绕数据发布/订阅、负载均衡、命名服务、分布式协调/通知、集群管理、Master 选举、分布式锁和分布式队列等方面来讲解ZooKeeper的典型应用场景及实现。

数据发布/订阅

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

发布/订阅系统一般有两种设计模式,分别是推(Push)模式和拉(Pull)模式。

在推模式中,服务端主动将数据更新发送给所有订阅的客户端;而拉模式则是由客户端主动发起请求来获取最新数据,通常客户端都采用定时进行轮询拉取的方式。

关于这两种模式更详细的讲解以及各自的优缺点,这里就不再赘述,读者可以自行到互联网上搜索相关的资料作进一步的了解。ZooKeeper 采用的是推拉相结合的方式:客户端向服务端注册自己需要关注的节点,一旦该节点的数据发生变更,那么服务端就会向相应的客户端发送Watcher事件通知,客户端接收到这个消息通知之后,需要主动到服务端获取最新的数据。

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

下面我们通过一个“配置管理”的实际案例来展示ZooKeeper在“数据发布/订阅”场景下的使用方式。

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

  • 数据量通常比较小。

  • 数据内容在运行时会发生动态变化。

  • 集群中各机器共享,配置一致。

对于这类配置信息,一般的做法通常可以选择将其存储在本地配置文件或是内存变量中。无论采用哪种方式,其实都可以简单地实现配置管理。

如果采用本地配置文件的方式,那么通常系统可以在应用启动的时候读取到本地磁盘的一个文件来进行初始化,并且在运行过程中定时地进行文件的读取,以此来检测文件内容的变更。

在系统的实际运行过程中,如果我们需要对这些配置信息进行更新,那么只要在相应的配置文件中进行修改,等到系统再次读取这些配置文件的时候,就可以读取到最新的配置信息,并更新到系统中去,这样就可以实现系统配置信息的更新。另外一种借助内存变量来实现配置管理的方式也非常简单,以Java系统为例,通常可以采用JMX方式来实现对系统运行时内存变量的更新。

从上面的介绍中,我们基本了解了如何使用本地配置文件和内存变量方式来实现配置管理。通常在集群机器规模不大、配置变更不是特别频繁的情况下,无论上面提到的哪种方式,都能够非常方便地解决配置管理的问题。但是,一旦机器规模变大,且配置信息变更越来越频繁后,我们发现依靠现有的这两种方式解决配置管理就变得越来越困难了。我们既希望能够快速地做到全局配置信息的变更,同时希望变更成本足够小,因此我们必须寻求一种更为分布式化的解决方案。

接下去我们就以一个“数据库切换”的应用场景展开,看看如何使用ZooKeeper来实现配置管理。

  • 配置存储

在进行配置管理之前,首先我们需要将初始化配置存储到ZooKeeper上去。

一般情况下,我们可以在 ZooKeeper 上选取一个数据节点用于配置的存储,例如/app1/database_config(以下简称“配置节点”),如图6-1所示。

  • 配置获取

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

  • 配置变更

在系统运行过程中,可能会出现需要进行数据库切换的情况,这个时候就需要进行配置变更。

借助ZooKeeper,我们只需要对ZooKeeper上配置节点的内容进行更新,ZooKeeper就能够帮我们将数据变更的通知发送到各个客户端,每个客户端在接收到这个变更通知后,就可以重新进行最新数据的获 取。

负载均衡

根据维基百科上的定义,负载均衡(Load Balance)是一种相当常见的计算机网络技术,用来对多个计算机(计算机集群)、网络连接、CPU、磁盘驱动器或其他资源进行分配负载,以达到优化资源使用、最大化吞吐率、最小化响应时间和避免过载的目的。

通常负载均衡可以分为硬件和软件负载均衡两类,本节主要探讨的是 ZooKeeper 在“软”负载均衡中的应用场景。

在分布式系统中,负载均衡更是一种普遍的技术,基本上每一个分布式系统都需要使用负载均衡。在本书第1章讲解分布式系统特征的时候,我们提到,分布式系统具有对等性,为了保证系统的高可用性,通常采用副本的方式来对数据和服务进行部署。

而对于消费者而言,则需要在这些对等的服务提供方中选择一个来执行相关的业务逻辑,其中比较典型的就是 DNS 服务。

在本节中,我们将详细介绍如何使用 ZooKeeper 来解决负载均衡问题。

一种动态的DNS服务

DNS是域名系统(Domain Name System)的缩写,是因特网中使用最广泛的核心技术之一。

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

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

日常开发过程中,经常会碰到这样的情况,在一个Company1公司内部,需要给一个App1应用的服务器集群机器配置一个域名解析。

相信有过一线开发经验的读者一定知道,这个时候通常会需要有类似于 app1.company1.com 的一个域名,其对应的就是一个服务器地址。

如果系统数量不多,那么通过这种传统的DNS配置方式还可以应付,但是,一旦公司规模变大,各类应用层出不穷,那么就很难再通过这种方式来进行统一的管理了。

因此,在实际开发中,往往使用本地HOST绑定来实现域名解析的工作。具体如何进行本地HOST绑定,因为不是本书的重点,并且互联网上有大量的资料,因此这里不再赘述。

使用本地HOST绑定的方法,可以很容易解决域名紧张的问题,基本上每一个系统都可以自行确定系统的域名与目标 IP 地址。同时,这种方法对于开发人员最大的好处就是可以随时修改域名与 IP 的映射,大大提高了开发调试效率。然而,这种看上去完美的方案,也有其致命的缺陷:

当应用的机器规模在一定范围内,并且域名的变更不是特别频繁时,本地HOST绑定是非常高效且简单的方式。

然而一旦机器规模变大后,就常常会碰到这样的情况:我们在应用上线的时候,需要在应用的每台机器上去绑定域名,但是在机器规模相当庞大的情况下,这种做法就相当不方便。

另外,如果想要临时更新域名,还需要到每个机器上去逐个进行变更,要消耗大量时间,因此完全无法保证实时性。

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

域名配置

和配置管理一样,我们首先需要在 ZooKeeper 上创建一个节点来进行域名配置,例如 /DDNS/app1/server.app1.company1.com(以下简称“域名节点”),如图6-2所示。

域名解析

在传统的 DNS 解析中,我们都不需要关心域名的解析过程,所有这些工作都交给了操作系统的域名和 IP 地址映射机制(本地 HOST 绑定)或是专门的域名解析服务器(由域名注册服务商提供)。

因此,在这点上,DDNS 方案和传统的域名解析有很大的区别——在DDNS中,域名的解析过程都是由每一个应用自己负责的。

通常应用都会首先从域名节点中获取一份 IP 地址和端口的配置,进行自行解析。

同时,每个应用还会在域名节点上注册一个数据变更 Watcher 监听,以便及时收到域名变更的通知。

域名变更

在运行过程中,难免会碰上域名对应的 IP 地址或是端口变更,这个时候就需要进行域名变更操作。

在DDNS中,我们只需要对指定的域名节点进行更新操作,ZooKeeper就会向订阅的客户端发送这个事件通知,应用在接收到这个事件通知后,就会再次进行域名配置的获取。

上面我们介绍了如何使用 ZooKeeper 来实现一种动态的 DNS 系统。

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

自动化的DNS服务

根据上面的讲解,相信读者基本上已经能够使用 ZooKeeper 来实现一个动态的 DNS 服务了。

但是我们仔细看一下上面的实现就会发现,在域名变更环节中,当域名对应的IP地址发生变更的时候,我们还是需要人为地介入去修改域名节点上的 IP 地址和端口。

接下来我们看看下面这种使用ZooKeeper实现的更为自动化的DNS服务。

自动化的DNS服务系统主要是为了实现服务的自动化定位,整个系统架构如图6-3所示。

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

  • Register集群负责域名的动态注册。

  • Dispatcher集群负责域名解析。

  • Scanner集群负责检测以及维护服务状态(探测服务的可用性、屏蔽异常服务节点等)。

  • SDK提供各种语言的系统接入协议,提供服务注册以及查询接口。

  • Monitor负责收集服务信息以及对DDNS自身状态的监控。

  • 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关系不是特别大,这里就不再展开讲解了。

命名服务

命名服务(Name Service)也是分布式系统中比较常见的一类场景,在《Java网络高级编程》一书中提到,命名服务是分布式系统最基本的公共服务之一。

在分布式系统中,被命名的实体通常可以是集群中的机器、提供的服务地址或远程对象等——这些我们都可以统称它们为名字(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 个短线的字符串,例如“e70f1357-f260-46ff-a32d-53a086c57ade”。UUID的优势自然不必多说,我们重点来看看它的缺陷。

  • 长度过长

UUID 最大的问题就在于生成的字符串过长。显然,和数据库中的 INT 类型相比,存储一个UUID需要花费更多的空间。

  • 含义不明

上面我们已经看到一个典型的 UUID 是类似于“e70f1357-f260-46ff-a32d-53a086c57ade”的一个字符串。根据这个字符串,开发人员从字面上基本看不出任何其表达的含义,这将会大大影响问题排查和开发调试的效率。

接下来,我们结合一个分布式任务调度系统来看看如何使用ZooKeeper来实现这类全局唯一ID的生成。

在5.3.2节中,我们已经提到,通过调用ZooKeeper节点创建的API接口可以创建一个顺序节点,并且在API返回值中会返回这个节点的完整名字。

利用这个特性,我们就可以借助ZooKeeper来生成全局唯一的ID了,如图6-4所示。

图6-4.全局唯一ID生成的ZooKeeper节点示意图

结合图6-4,我们来讲解对于一个任务列表的主键,使用ZooKeeper生成唯一ID的基本步骤。

1.所有客户端都会根据自己的任务类型,在指定类型的任务下面通过调用create()接口来创建一个顺序节点,例如创建“job-”节点。

2.节点创建完毕后,create()接口会返回一个完整的节点名,例如“job-0000000003”。

3.客户端拿到这个返回值后,拼接上 type 类型,例如“type2-job-0000000003”,这就可以作为一个全局唯一的ID了。

在ZooKeeper中,每一个数据节点都能够维护一份子节点的顺序顺列,当客户端对其创建一个顺序子节点的时候 ZooKeeper 会自动以后缀的形式在其子节点上添加一个序号,在这个场景中就是利用了ZooKeeper的这个特性。关于ZooKeeper的顺序节点,将在7.1.2节中做详细讲解。

分布式协调/通知

分布式协调/通知服务是分布式系统中不可缺少的一个环节,是将不同的分布式组件有机结合起来的关键所在。

对于一个在多台机器上部署运行的应用而言,通常需要一个协调者(Coordinator)来控制整个系统的运行流程,例如分布式事务的处理、机器间的互相协调等。

同时,引入这样一个协调者,便于将分布式协调的职责从应用中分离出来,从而可以大大减少系统之间的耦合性,而且能够显著提高系统的可扩展性。

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

MySQL数据复制总线:Mysql_Replicator

MySQL数据复制总线(以下简称“复制总线”)是一个实时数据复制框架,用于在不同的 MySQL 数据库实例之间进行异步数据复制和数据变化通知。

整个系统是一个由MySQL 数据库集群、消息队列系统、任务管理监控平台以及 ZooKeeper 集群等组件共同构成的一个包含数据生产者、复制管道和数据消费者等部分的数据总线系统,图 6-5所示是该系统的整体结构图。

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

  • Core实现了数据复制的核心逻辑,其将数据复制封装成管道,并抽象出生产者和消费者两个概念,其中生产者通常是MySQL数据库的Binlog[2]日志。

  • Server负责启动和停止复制任务。

  • Monitor负责监控任务的运行状态,如果在数据复制期间发生异常或出现故障会进行告警。

三个子模块之间的关系如图6-6所示。

图6-6.MySQL数据复制子组件关系图

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

任务注册

Core进程在启动的时候,首先会向/mysql_replicator/tasks节点(以下简称“任务列表节点”)注册任务。

例如,对于一个“复制热门商品”的任务,Task 所在机器在启动的时候,会首先在任务列表节点上创建一个子节点,例如/mysql_replicator/tasks/copy_hot_item(以下简称“任务节点”),如图 6-7 所示。如果在注册过程中发现该子节点已经存在,说明已经有其他Ta s k机器注册了该任务,因此自己不需要再创建该节点了。

任务热备份

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

为了实现上述热备方案,无论在第一步中是否创建了任务节点,每台任务机器都需要在 /mysql_replicator/tasks/copy_hot_item/instances 节点上将自己的主机名注册上去。

注意,这里注册的节点类型很特殊,是一个临时的顺序节点。

在注册完这个子节点后,通常一个完整的节点名如下:/mysql_replicator/tasks/copy_hot_item/intsances/[Hostname]-1,其中最后的序列号就是临时顺序节点的精华所在。关于ZooKeeper的临时顺序节点生成原理,将在7.1.2节中做详细讲解。

在完成该子节点的创建后,每台任务机器都可以获取到自己创建的节点的完成节点名以及所有子节点的列表,然后通过对比判断自己是否是所有子节点中序号最小的。

如果自己是序号最小的子节点,那么就将自己的运行状态设置为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实例需要进行数据复制,每个数据库实例都会对应一个复制任务,如果每个任务都进行双机热备份的话,那么显然需要消耗太多的机器。

因此我们同时设计了一种冷备份的方案,它和热备份方案最大的不同点在于,对所有任务进行分组,如图6-8所示。

和热备份中比较大的区别在于,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 进行冷备份扫描。整个过程可以通过如图6-9所示的流程图来表示。

冷热备份对比

从上面的讲解中,我们基本对热备份和冷备份两种运行方式都有了一定的了解,现在再来对比下这两种运行方式。

在热备份方案中,针对一个任务使用了两台机器进行热备份,借助ZooKeeper的Watcher通知机制和临时顺序节点的特性,能够非常实时地进行互相协调,但缺陷就是机器资源消耗比较大。

而在冷备份方案中,采用了扫描机制,虽然降低了任务协调的实时性,但是节省了机器资源。

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

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

心跳检测

机器间的心跳检测机制是指在分布式环境中,不同机器之间需要检测到彼此是否在正常运行,例如 A 机器需要知道 B 机器是否正常运行。

在传统的开发中,我们通常是通过主机之间是否可以相互PING通来判断,更复杂一点的话,则会通过在机器之间建立长连接,通过 TCP 连接固有的心跳检测机制来实现上层机器的心跳检测,这些确实都是一些非常常见的心跳检测方法。

下面来看看如何使用ZooKeeper来实现分布式机器间的心跳检测。

基于ZooKeeper的临时节点特性,可以让不同的机器都在ZooKeeper的一个指定节点下创建临时子节点,不同的机器之间可以根据这个临时节点来判断对应的客户端机器是否存活。

通过这种方式,检测系统和被检测系统之间并不需要直接相关联,而是通过ZooKeeper上的某个节点进行关联,大大减少了系统耦合。

工作进度汇报

在一个常见的任务分发系统中,通常任务被分发到不同的机器上执行后,需要实时地将自己的任务执行进度汇报给分发系统。这个时候就可以通过 ZooKeeper 来实现。

在ZooKeeper 上选择一个节点,每个任务客户端都在这个节点下面创建临时子节点,这样便可以实现两个功能:

  • 通过判断临时节点是否存在来确定任务机器是否存活;

  • 各个任务机器会实时地将自己的任务执行进度写到这个临时节点上去,以便中心系统能够实时地获取到任务的执行进度。

系统调度

使用 ZooKeeper,能够实现另一种系统调度模式:一个分布式系统由控制台和一些客户端系统两部分组成,控制台的职责就是需要将一些指令信息发送给所有的客户端,以控制它们进行相应的业务逻辑。

后台管理人员在控制台上做的一些操作,实际上就是修改了ZooKeeper上某些节点的数据,而ZooKeeper进一步把这些数据变更以事件通知的形式发送给了对应的订阅客户端。

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

集群管理

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

所谓集群管理,包括集群监控与集群控制两大块,前者侧重对集群运行时状态的收集,后者则是对集群进行操作与控制。

在日常开发和运维过程中,我们经常会有类似于如下的需求。

  • 希望知道当前集群中究竟有多少机器在工作。

  • 对集群中每台机器的运行时状态进行数据收集。

  • 对集群中机器进行上下线操作。

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

  • 大规模升级困难

以客户端形式存在的 Agent,在大规模使用后,一旦遇上需要大规模升级的情况,就非常麻烦,在升级成本和升级进度的控制上面临巨大的挑战。

  • 统一的Agent无法满足多样的需求

对于机器的CPU使用率、负载(Load)、内存使用率、网络吞吐以及磁盘容量等机器基本的物理状态,使用统一的Agent来进行监控或许都可以满足。

但是,如果需要深入应用内部,对一些业务状态进行监控,例如,在一个分布式消息中间件中,希望监控到每个消费者对消息的消费状态;或者在一个分布式任务调度系统中,需要对每个机器上任务的执行情况进行监控。很显然,对于这些业务耦合紧密的监控需求,不适合由一个统一的Agent来提供。

  • 编程语言多样性

随着越来越多编程语言的出现,各种异构系统层出不穷。如果使用传统的Agent方式,那么需要提供各种语言的 Agent 客户端。

另一方面,“监控中心”在对异构系统的数据进行整合上面临巨大挑战。

  • ZooKeeper具有以下两大特性。

· 客户端如果对ZooKeeper的一个数据节点注册Watcher监听,那么当该数据节点的内容或是其子节点列表发生变更时,ZooKeeper服务器就会向订阅的客户端发送变更通知。

· 对在ZooKeeper上创建的临时节点,一旦客户端与服务器之间的会话失效,那么该临时节点也就被自动清除。

利用 ZooKeeper 的这两大特性,就可以实现另一种集群机器存活性监控的系统。

例如,监控系统在/clusterServers节点上注册一个Watcher监听,那么但凡进行动态添加机器的操作,就会在/clusterServers节点下创建一个临时节点:/clusterServers/[Hostname]

这样一来,监控系统就能够实时检测到机器的变动情况,至于后续处理就是监控系统的业务了。下面我们就通过分布式日志收集系统和在线云主机管理这两个典型例子来看看如何使用ZooKeeper实现集群管理。

分布式日志收集系统

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

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

  • 变化的日志源机器

在生产环境中,伴随着机器的变动,每个应用的机器几乎每天都是在变化的(机器硬件问题、扩容、机房迁移或是网络问题等都会导致一个应用的机器变化),也就是说每个组别中的日志源机器通常是在不断变化的。

  • 变化的收集器机器

日志收集系统自身也会有机器的变更或扩容,于是会出现新的收集器机器加入或是老的收集器机器退出的情况。

上面两个问题,无论是日志源机器还是收集器机器的变更,最终都归结为一点:如何快速、合理、动态地为每个收集器分配对应的日志源机器,这也成为了整个日志系统正确稳定运转的前提,也是日志收集过程中最大的技术挑战之一。

在这种情况下,引入ZooKeeper是个不错的选择,下面我们就来看ZooKeeper在这个场景中的使用。

注册收集器机器

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

任务分发

待所有收集器机器都创建好自己对应的节点后,系统根据收集器节点下子节点的个数,将所有日志源机器分成对应的若干组,然后将分组后的机器列表分别写到这些收集器机器创建的子节点(例如/logs/collector/host1)上去。

这样一来,每个收集器机器都能够从自己对应的收集器节点上获取日志源机器列表,进而开始进行日志收集工作。

状态汇报

完成收集器机器的注册以及任务分发后,我们还要考虑到这些机器随时都有挂掉的可能。因此,针对这个问题,我们需要有一个收集器的状态汇报机制:每个收集器机器在创建完自己的专属节点后,还需要在对应的子节点上创建一个状态子节点,例如 /logs/collector/host1/status,每个收集器机器都需要定期向该节点写入自己的状态信息。

我们可以把这种策略看作是一种心跳检测机制,通常收集器机器都会在这个节点中写入日志收集进度信息。

日志系统根据该状态子节点的最后更新时间来判断对应的收集器机器是否存活。

动态分配

如果收集器机器挂掉或是扩容了,就需要动态地进行收集任务的分配。在运行过程中,日志系统始终关注着/logs/collector这个节点下所有子节点的变更,一旦检测到有收集器机器停止汇报或是有新的收集器机器加入,就要开始进行任务的重新分配。

无论是针对收集器机器停止汇报还是新机器加入的情况,日志系统都需要将之前分配给该收集器的所有任务进行转移。

为了解决这个问题,通常有两种做法。

  • 全局动态分配

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

  • 局部动态分配

全局动态分配方式虽然策略简单,但是存在一个问题:一个或部分收集器机器的变更,就会导致全局动态任务的分配,影响面比较大,因此风险也就比较大。所谓局部动态分配,顾名思义就是在小范围内进行任务的动态分配。在这种策略中,每个收集器机器在汇报自己日志收集状态的同时,也会把自己的负载汇报上去。

请注意,这里提到的负载并不仅仅只是简单地指机器CPU负载(Load),而是一个对当前收集器任务执行的综合评估,这个评估算法和ZooKeeper本身并没有太大的关系,这里不再赘述。

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

同样,如果有新的收集器机器加入,会从那些负载高的机器上转移部分任务给这个新加入的机器。

注意事项

在上面的介绍中,我们已经了解了ZooKeeper是如何协调一个分布式日志收集系统工作的,接下来再来看看一些细节问题。

  • 节点类型

我们首先来看/logs/collector这个节点下面子节点的节点类型。在上面已经提到,/logs/collector节点下面的所有子节点都代表了每个收集器机器,那么初步认为这些子节点必须选择临时节点,原因是日志系统可以根据这些临时节点来判断收集器机器的存活性。

但是,同时还需要注意的一点是:在分布式日志收集这个场景中,收集器节点上还会存放所有已经分配给该收集器机器的日志源机器列表,如果只是简单地依靠ZooKeeper自身的临时节点机制,那么当一个收集器机器挂掉或是当这个收集器机器中断“心跳汇报”的时候,待该收集器节点的会话失效后,ZooKeeper就会立即删除该节点,于是,记录在该节点上的所有日志源机器列表也就随之被清除掉了。

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

  • 日志系统节点监听

在实际生产运行过程中,每一个收集器机器更改自己状态节点的频率可能非常高(如每秒1次或更短),而且收集器的数量可能非常大,如果日志系统监听所有这些节点变化,那么通知的消息量可能会非常大。

另一方面,在收集器机器正常工作的情况下,日志系统没有必要去实时地接收每次节点状态变更,因此大部分这些状态变更通知都是无用的。

因此我们考虑放弃监听设置,而是采用日志系统主动轮询收集器节点的策略,这样就节省了不少网卡流量,唯一的缺陷就是有一定的延时(考虑到分布式日志收集系统的定位,这个延时是可以接受的)。

在线云主机管理

在线云主机管理通常出现在那些虚拟主机提供商的应用场景中。

在这类集群管理中,有很重要的一块就是集群机器的监控。

这个场景通常对于集群中的机器状态,尤其是机器在线率的统计有较高的要求,同时需要能够快速地对集群中机器的变更做出响应。

在传统的实现方案中,监控系统通过某种手段(比如检测主机的指定端口)来对每台机器进行定时检测,或者每台机器自己定时向监控系统汇报“我还活着”。但是这种方式需要每一个业务系统的开发人员自己来处理网络通信、协议设计、调度和容灾等诸多琐碎的问题。下面来看看使用ZooKeeper实现的另一种集群机器存活性监控系统。针对这个系统,我们的需求点通常如下。 · 如何快速地统计出当前生产环境一共有多少台机器? · 如何快速地获取到机器上/下线的情况? · 如何实时监控集群中每台主机的运行时状态?

机器上/下线

为了实现自动化的线上运维,我们必须对机器的上/下线情况有一个全局的监控。通常在新增机器的时候,需要首先将指定的Agent部署到这些机器上去。

Agent部署启动之后,会首先向ZooKeeper的指定节点进行注册,具体的做法就是在机器列表节点下面创建一个临时子节点,例如 /XAE/machine/[Hostname](下文我们以“主机节点”代表这个节点),如图6-11所示。

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

机器监控

对于一个在线云主机系统,不仅要对机器的在线状态进行检测,还需要对机器的运行时状态进行监控。

在运行的过程中,Agent 会定时将主机的运行状态信息写入 ZooKeeper上的主机节点,监控中心通过订阅这些节点的数据变更通知来间接地获取主机的运行时信息。

随着分布式系统规模变得越来越庞大,对集群机器的监控和管理显得越来越重要。

上面提到的这种借助ZooKeeper来实现的方式,不仅能够实时地检测到集群中机器的上/下线情况,而且能够实时地获取到主机的运行时信息,从而能够构建出一个大规模集群的主机图谱。

Master选举

Master选举是一个在分布式系统中非常常见的应用场景。

分布式最核心的特性就是能够将具有独立计算能力的系统单元部署在不同的机器上,构成一个完整的分布式系统。

而与此同时,实际场景中往往也需要在这些分布在不同机器上的独立系统单元中选出一个所谓的“老大”,在计算机科学中,我们称之为Master。

在分布式系统中,Master往往用来协调集群中其他系统单元,具有对分布式系统状态变更的决定权。

例如,在一些读写分离的应用场

TODO……………….

参考资料

分布式一致性原理与实践