博客 ZooKeeper 应用场景

ZooKeeper 应用场景

   数栈君   发表于 2023-06-05 11:30  456  0

 ZooKeeper是一个典型的发布/订阅模式的分布式数据管理与协调框架,我们可以使用它来进行分布式数据的发布与订阅。另一方面,通过对ZooKeeper中丰富的数据节点类型进行交叉使用,配合Watcher 事件通知机制,可以非常方便地构建一系列分布式应用中都会涉及的核心功能,如数据发布/订阅、命名服务、集群管理、Master选举、分布式锁和分布式队列等。那接下来就针对这些典型的分布式应用场景来做下介绍。

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

发布/订阅系统⼀般有两种设计模式,分别是推(Push)模式和拉(Pull)模式。在推模式中,服务端主动将数据更新发送给所有订阅的客户端;而拉模式则是由客户端主动发起请求来获取最新数据,通常客户端都采用定时进行轮询拉取的方式。

ZooKeeper 采用的是推拉相结合的方式:客户端向服务端注册自己需要关注的节点,一旦该节点的数据发生变更,那么服务端就会向相应的客户端发送Watcher事件通知,客户端接收到这个消息通知之后, 需要主动到服务端获取最新的数据。

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

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

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

数据量通常比较小。
数据内容在运行时会发生动态变化。
集群中各机器共享,配置一致。
对于这类配置信息,一般的做法通常可以选择将其存储在本地配置文件或是内存变量中。无论采用哪种方式,其实都可以简单地实现配置管理,在集群机器规模不大、配置变更不是特别频繁的情况下,无论刚刚提到的哪种方式,都能够非常方便地解决配置管理的问题。但是,一旦机器规模变大,且配置信息变更越来越频繁后,我们发现依靠现有的这两种方式解决配置管理就变得越来越困难了。我们既希望能够快速地做到全局配置信息的变更,同时希望变更成本足够小,因此我们必须寻求一种更为分布式化的解决方案

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

配置存储

在进行配置管理之前,首先我们需要将初始化配置信息存储到Zookeeper上去,一般情况下,我们可以在Zookeeper上选取⼀个数据节点用于配置信息的存储,例如:/app1/database_config



配置管理的zookeeper节点示意图

我们将需要管理的配置信息写入到该数据节点中去,例如:

#数据库配置信息#DBCP
dbcp.driverClassName=com.mysql.jdbc.Driver dbcp.dbJDBCUrl=jdbc:mysql://127.0.0.1:3306/lagou-test
dbcp.username=zm
dbcp.password=1234
dbcp.maxActive=30
dbcp.maxIdle=10
配置获取

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

配置变更

在系统运行过程中,可能会出现需要进行数据库切换的情况,这个时候就需要进行配置变更。借助ZooKeeper,我们只需要对ZooKeeper上配置节点的内容进行更新,ZooKeeper就能够帮我们将数据变更的通知发送到各个客户端,每个客户端在接收到这个变更通知后,就可以重新进行最新数据的获取。

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

ZooKeeper 提供的命名服务功能能够帮助应用系统通过一个资源引用的方式来实现对资源的定位与使用。另外,广义上命名服务的资源定位都不是真正意义的实体资源--在分布式环境中,上层应用仅仅需要一个全局唯一的名字,类似于数据库中的唯一主键。

所以接下来。我们来看看如何使用ZooKeeper来实现一套分布式全局唯一ID的分配机制

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

一说起全局唯一ID,相信大家都会联想到 UUID。没错,UUID 是通用唯⼀识别码(Universally Unique Identifier)的简称,是一种在分布式系统中广泛使用的用于唯一标识元素的标准 确实,UUID 是⼀个非常不错的全局唯一ID生成方式,能够非常简便地保证分布式环境中的唯一性。一个标准的UUID 是一个包含 32 位字符和 4 个短线的字符串,例如“e70f1357-f260-46ff-a32d-53a086c57ade”。UUID的优势自然不必多说,我们重点来看看它的缺陷。

长度过长

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

含义不明

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

所以接下来,我们结合一个分布式任务调度系统来看看如何使用ZooKeepe来实现这类全局唯一ID的生成。之前我们已经提到,通过调用ZooKeeper节点创建的API接口可以创建一个顺序节点,并且在API返回值中会返回这个节点的完整名字。利用这个特性,我们就可以借助ZooKeeper来生成全局唯一的ID了,如下图:



全局唯一ID生成的ZooKeeper节点示意图

说明,对于一个任务列表的主键,使用ZooKeeper生成唯一ID的基本步骤:

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

3、集群管理
随着分布式系统规模的日益扩大,集群中的机器规模也随之变大,那如何更好地进行集群管理也显得越来越重要了。所谓集群管理,包括集群监控与集群控制两大块,前者侧重对集群运行时状态的收集,后者则是对集群进行操作与控制。

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

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

大规模升级困难

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

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

对于机器的CPU使用率、负载(Load)、内存使用率、网络吞吐以及磁盘容量等机器基本的物理状态, 使用统一的Agent来进行监控或许都可以满足。但是,如果需要深入应用内部,对一些业务状态进行监控,例如,在一个分布式消息中间件中,希望监控到每个消费者对消息的消费状态;或者在一个分布式 任务调度系统中,需要对每个机器上任务的执行情况进行监控。很显然,对于这些业务耦合紧密的监控需求,不适合由一个统一的Agent来提供。

编程语⾔多样性

随着越来越多编程语言的出现,各种异构系统层出不穷。如果使用传统的Agent方式,那么需要提供各种语言的 Agent 客户端。另一方面,“监控中心”在对异构系统的数据进行整合上面临巨大挑战。

Zookeeper的两大特性:

客户端如果对Zookeeper的数据节点注册Watcher监听,那么当该数据节点的内容或是其子节点列表发生变更时,Zookeeper服务器就会向订阅的客户端发送变更通知。
对在Zookeeper上创建的临时节点,一旦客户端与服务器之间的会话失效,那么临时节点也会被自动删除
利用其两大特性,可以实现集群机器存活监控系统,若监控系统在/clusterServers节点上注册一个Watcher监听,那么但凡进行动态添加机器的操作,就会在/clusterServers节点下创建一个临时节点:/clusterServers/[Hostname],这样,监控系统就能够实时监测机器的变动情况。

下面通过分布式日志收集系统这个典型应用来学习Zookeeper如何实现集群管理。

分布式日志收集系统

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

在一个典型的日志系统的架构设计中,整个日志系统会把所有需要收集的日志机器(我们以“日志源机器”代表此类机器)分为多个组别,每个组别对应一个收集器,这个收集器其实就是一个后台机器(我们以“收集器机器”代表此类机器),用于收集日志。

对于大规模的分布式日志收集系统场景,通常需要解决两个问题:

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

使用Zookeeper的场景步骤如下

(1)注册收集器机器

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



(2)任务分发

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

(3)状态汇报

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

(4)动态分配

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

全局动态分配:

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

局部动态分配::

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

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

上述步骤已经完整的说明了整个日志收集系统的工作流程,其中有两点注意事项:

(1)节点类型

在/logs/collector节点下创建临时节点可以很好的判断机器是否存活,但是,若机器挂了,其节点会被删除,记录在节点上的日志源机器列表也被清除,所以需要选择持久节点来标识每一台机器,同时在节点下分别创建/logs/collector/[Hostname]/status节点来表征每一个收集器机器的状态,这样,既能实现对所有机器的监控,同时机器挂掉后,依然能够将分配任务还原。

(2)日志系统节点监听

若采用Watcher机制,那么通知的消息量的网络开销非常大,需要采用日志系统主动轮询收集器节点的策略,这样可以节省网络流量,但是存在一定的延时。

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



在这个系统中,首先会在 ZooKeeper 上创建一个日期节点,例如“2020-11-11

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

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


————————————————
版权声明:本文为CSDN博主「悠然予夏」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_52851967/article/details/126278349

0条评论
社区公告
  • 大数据领域最专业的产品&技术交流社区,专注于探讨与分享大数据领域有趣又火热的信息,专业又专注的数据人园地

最新活动更多
微信扫码获取数字化转型资料
钉钉扫码加入技术交流群