Zookeeper 四、Zookeeper应用场景

嚴華  金牌会员 | 2024-8-14 22:03:04 | 显示全部楼层 | 阅读模式
打印 上一主题 下一主题

主题 936|帖子 936|积分 2808

Zookeeper是一个典型的发布/订阅模式的分布式数据管理与和谐框架,我们可以使用它来进行分布式数据的发布与订阅。另一方面,通过对Zookeeper中丰富的数据节点范例进行交叉使用,配合Watcher事件通知机制,可以非常方便地构建一系列分布式应用中都会涉及的核心功能,如数据部分/订阅、命名服务、集群管理、Master选举、分布式锁和分布式队列等。
1.数据发布/订阅

数据发布/订阅(Publish/Subscribe)体系,即所谓的配置中心,顾名思义就是发布者将数据发布到Zookeeper的一个或一系列节点上,供订阅者进行数据订阅,进而到达动态获取数据的目的,实现配置信息的会合式管理和数据的动态更新。
发布/订阅体系一般有两种筹划模式,分别是推(Push)模式和拉(Pull)模式。在推模式中,服务端主动将数据更新发送给所有订阅的客户端;而拉模式则是由客户端主动发起哀求来获取更新数据,通常客户端都接纳定时进行轮询拉取的方式。
Zookeeper接纳的是推拉相结合的方式:客户端向服务端注册本身必要关注的节点,一旦该节点的数据发生变更,那么服务端就会向相应的客户端发送Watcher事件通知,客户端接收到这个消息通知之后,必要主动到服务端获取最新的数据。
假如将配置信息存放到Zookeeper上进行会合管理,那么通常环境下,应用在启动的时间都会主动到Zookeeper服务端上进行一次配置信息的获取,同时,在指定节点上注册一个Watcher监听,这样依赖,但凡配置信息发生变更,服务端都会实时通知到所有订阅的客户端,从而到达实时获取最新配置信息的目的。
下面通过一个“配置管理”实际案例来展示Zookeeper在“数据发布/订阅“场景下的使用方式。
在平常的应用体系开辟中,常常会遇到这样的需求:体系中必要使用一些通用的配置信息,例如呆板列表信息、运行时的开关配置、数据库配置信息等。这些全局配置信息通常具备以下3个特性:


  • 数据量通常比力小
  • 数据内容在运行时会发生动态变化
  • 集群中各呆板共享,配置划一
对于这类配置信息,一般的做法通常可以选择将其存储在本地配置文件或内存变量中。无论接纳哪种方式,其实都可以简朴地实现配置管理,在集群呆板规模不大、配置变更不是特别频仍的环境下,无论刚刚提到的哪种方式,都能够非常方便地办理配置管理的问题。但是,一旦呆板规模变大,且配置信息变更越来越频仍后,我们发现依靠现有的这两种方式办理配置管理就变得越来越困难了。我们既盼望能够快速地做到全局配置信息的变更,同时盼望变更成本足够小,因此我们必须寻求一种更为分布式话的办理方案。
接下来以一个“数据库切换”的应用场景展开,看看如何使用Zookeeper来实现配置管理:


  • 配置存储

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


我们将必要管理的信息写入到该数据节点中去。


  • 配置获取

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

  • 配置变更

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

2.命名服务

命名服务(Name Service)也是分布式体系中比力常见的一类场景,是分布式体系最基本的公共服务之一。在分布式体系中,被命名的实体通常可以是集群中的呆板、提供的服务地址或长途对象等——这些我们都可以统称他们为名字(Name),其中较为常见的就是一些分布式服务框架(如RPC、RMI)中的服务地址列表,通过使用命名服务,客户端应用能够根据指定名字来获取资源的实体、服务地址和提供者的信息等。
Zookeeper提供的命名服务功能能够资助应用体系通过一个资源引用的方式来实现对资源的定位与使用。别的,广义上命名服务的资源定位都不是真正意义的实体资源——在分布式环境中,上层应用仅仅必要一个全局唯一的名字,类似于数据库中的唯一主键。
通过调用Zookeeper节点创建的API接口可以创建一个次序节点,而且在API返回值中会返回这个节点的完整名字。利用这个特性,我们就可以借助Zookeeper来天生全局唯一的ID。

阐明:
对于一个使命列表的主键,使用Zookeeper天生唯一ID的基本步骤:

  • 所有客户端都会根据本身的使命范例,在知道范例的使命下面通过调用create()接口来创建一个次序节点,例如创建"job-"节点
  • 节点创建完毕后,create()接口会返回一个完整的节点名,例如“job-00000003”
  • 客户端拿到这个返回值后,拼接上type范例,例如“type2-job-00000003",这就可以作为一个全局唯一的ID了
在Zookeeper中,每一个数据节点都能够维护一份子节点的次序序列,当客户端对其创建一个次序子节点的时间Zookeeper会自动以后缀的情势在其子节点上添加一个序号,在这个场景中就是利用了Zookeeper的这个特性。
3.集群管理

1)集群的出现与存在的问题

随着分布式体系规模的日益扩大,集群中的呆板规模也随之变大,如何更好地进行集群管理也显得越来越重要了。所谓集群管理,包罗集群监控与集群控制两大块,前者偏重对集群运行时状态的收集,后者则是对集群进行操作与控制。
在一样平常开辟和运维过程中,我们常常会有类似于如下的需求:


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


  • 大规模升级困难

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

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

    • 对于呆板的CPU使用率、负载(Load)、内存使用率、网络吞吐以及磁盘容量等呆板基本的物理状态,使用统一的Agent来进行监控或许都可以满足。但是,假如必要深入应用内部,对一些业务状态进行监控,例如,在一个分布式消息中间件中,盼望监控到每个消费者对消息的消费状态;大概在一个分布式使命调度体系中,必要对每个呆板上使命的实验环境进行监控。很显然,对于这些业务耦合紧密的监控需求,不得当由一个统一的Agent来提供。

  • 编程语言多样性

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

2)Zookeeper集群管理

(1)Zookeeper的特性


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

分布式日志收集体系的核心工作就是收集分布在不同呆板上的体系日志,在这里我们重点来看分布式日志体系(以下简称“日志体系”)的收集器模块。
在一个典型的日志体系的架构筹划中,整个日志体系会把所有必要收集的日志呆板(我们以“日志源呆板”代表此类呆板)分为多个组别,每个组别对应一个收集器,这个收集器其实就是一个后台呆板(我们以“收集器呆板”代表此类呆板),用于收集日志。
对于大规模的分布式日志收集体系场景,通常必要办理两个问题:


  • 变化的日志源呆板

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

  • 变化的收集器呆板

    • 日志收集体系自身也会有呆板的变更或扩容,于是会出现新的收集器呆板加入或是老的收集器呆板退出的环境

无论是日志源呆板还是收集器呆板的变更,终极都可以归结为如何快速、合理、动态地为每个收集器分配对应的日志源呆板。这也成为了整个日志体系准确稳固运转的前提,也是日志收集过程中最大的技术挑战之一,在这种环境下,我们就可以引入Zookeeper了。
(3)使用Zookeeper的步骤

①注册收集器呆板

使用Zookeeper来进行日志体系收集器的注册,典型的做法是在Zookeeper上创建一个节点作为收集器的根节点,例如 /logs/collector(下文我们以“收集器节点”代表该数据节点),每个收集器呆板在启动的时间,都会在收集器几点下创建本身的节点,例如 /logs/collector/[Hostname]

②使命分发

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

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

假如收集器呆板挂掉大概是扩容了,就必要动态地进行收集使命的分配。在运行过程中,日志体系始终关注着 /logs/collector 这个节点下所有子节点的变更,一旦检测到有收集器呆板停止汇报或是有新的收集器呆板加入,就要开始进行使命的重新分配。无论是针对收集器呆板停止汇报还是新呆板加入的环境,日志体系都必要将之前分配给该收集器的所有使命进行转移。为了办理这个问题,通常有两种做法:


  • 全局动态分配

    • 这是一种简朴粗暴的做法,在出现收集器呆板挂掉或是新呆板加入的时间,日志体系必要根据新的收集器呆板列表,立即对所有的日志源呆板重新进行一次分组,然后将其分配给剩下的收集器呆板
    • 缺点:一个或部分收集器呆板的变更,就会导致全局动态使命的分配,影响面比力大,因此风险也比力大。

  • 局部动态分配

    • 假如一个收集器呆板挂了,那么日志体系就会把之前分配给这个呆板的使命重新分配到那些负载较低的呆板上去。同样,假如有新的收集器呆板加入,会从那些负载高的呆板上转移部分使命给这个新加入的呆板

(4)注意事项

①节点范例

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

若接纳Watcher机制,那么通知的消息量的网络开销非常大,必要接纳日志体系主动轮询收集器节点的计谋,这样可以节省网络流量,但是存在肯定的延时。
4.Master选举

1)Master选举概述

Master选举是一个在分布式体系中非常常见的应用场景。分布式最核心的特性就是能够将具有独立盘算能力的体系单位摆设在不同的呆板上,构成一个完整的分布式体系。而与此同时,实际场景中往往也必要在这些分布在不同呆板上的独立体系单位中选出一个所谓的“老大”,在盘算机中,我们称之为Master。
在分布式体系中,Master往往用来和谐集群中其他体系单位,具有对分布式体系状态变更的决定权。例如,在一些读写分离的应用场景中,客户端的写哀求往往是由Master来处理的;而在另一些场景中,Master则常常负责处理一些复杂的逻辑,并将处理结果同步给集群中的其他体系单位。Master选举可以说是Zookeeper最典型的应用场景了。
2)Master选举场景

接下来,结合“一种海量数据处理与共享模型”这个详细例子来看看Zookeeper在集群Master选举中的应用场景。
在分布式环境中,常常会遇到这样的应用场景:集群中的所有体系单位必要对前端业务提供数据,比如一个商品ID,大概一个网站轮播广告的广告ID等,而这些商品ID或是广告ID往往必要从一系列的海量数据处理盘算得到——这通常是一个非常耗费I/O和CPU资源的过程。鉴于该盘算过程的复杂性,假如让集群中的所有呆板都实验这个盘算逻辑的话,那么将耗费非常多的资源。一种比力好的方法就是只让集群中的部分,甚至只让其中的一台呆板去处理数据盘算,一旦盘算出数据结果,就可以共享给整个集群中的其他所有客户端呆板,这样可以大大减少重复劳动,提升性能。这里以一个简朴的广告投放体系后台场景为例来讲解这个模型。

整个体系大体上可以分为客户端集群、分布式缓存体系、海量数据处理总线和Zookeeper四个部分
运行机制:
图中的Client集群每天定时会通过Zookeeper来实现Master选举。选举产生Master客户端之后,这个Master就会复杂进行一系列的海量数据处理,终极盘算得到一个数据结果,并将其放置在一个内存/数据库中。同时,Master还必要通知集群中其他所有的客户端从这个内存/数据库中共享盘算结果。
3)Master选举实现

需求:在集群的所有呆板中选举出一台呆板作为Master
利用Zookeeper的强划一性,能够很好保证在分布式高并发环境下节点的创建肯定能够保证全局唯一性,即Zookeeper将会保证客户端无法重复创建一个已经存在的数据节点。也就是说,假如同时有多个客户端哀求创建同一个节点,那么终极肯定只有一个客户端哀求能够创建成功。利用这个特性,就能很容易地在分布式环境中进行Master选举了。

在这个体系中,起首会在Zookeeper上创建一个日期节点,例如“2024-01-11”
客户端集群每天都会定时往Zookeeper上创建一个临时节点,例如 /master_election/2024-01-11/binding。在这个过程中,只有一个客户端能够成功创建这个节点,那么这个客户端所在的呆板就成为了Master。同时,其他没有在Zookeeper上成功创建节点的客户端,都会在节点/master_election/2024-01-11 上注册一个子节点变更的Watcher,用于监控当前的Master呆板是否存活,一旦发现当前的Master挂了,那么其余的客户端将会重新进行Master选举。
5.分布式锁

分布式锁是空值分布式体系之间同步访问共享资源的一种方式。假如不同的体系或是同一个体系的不同主机之间共享了一个或一组资源,那么访问这些资源的时间,往往必要通过一些互斥手段来防止彼此之间的干扰,以保证划一性,在这种环境下,就必要使用分布式锁了。
在平常的实际项目开辟中,我们往往很少去在意分布式锁,而是依赖于关系型数据库固有的排他性来实现不同进程之间的互斥。这确实是一种非常轻便且被广泛使用的分布式锁实现方式。然而有一个不争的事实是,现在绝大多数大型分布式体系的性能瓶颈都会合在数据库操作上。因此,假如上层业务再给数据库添加一些额外的锁,例如行锁、表锁甚至是繁重的事务处理,那么就会让数据库更加不堪重负
1)排它锁

①定义锁

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

②获取锁

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

在“定义锁”部分提到,/exclusive_lock/lock 是一个临时节点,因此在以下两种环境下,都有可能释放锁。

  • 当前获取锁的客户端呆板发生宕机,那么Zookeeper上的这个临时节点就会被移除
  • 正常实验完业务逻辑后,客户端就会主动将本身创建的临时节点删除。
无论在什么环境下一处了lock节点,Zookeeper都会通知在 /exclusive_lock 节点上注册了子节点变更Watcher监听的客户端。这些客户端在接收到通知后,再次重新发起分布式锁获取,即重复“获取锁”过程。整个排它锁的获取和释放流程如下:

2)共享锁

共享锁(Shared Locks),又称为读锁,同样是一种基本的锁范例
假如事务T1对数据对象O1加上了共享锁,那么当前事务只能对O1进行读操作,其他事务也只能对这个数据对象加共享锁——直到该数据对象上的所有共享锁都被释放
共享锁和排它锁最根本的区别在于,加上排它锁后,数据对象只对一个事务可见,而加上共享锁后,数据对所有事务都可见。
①定义锁

通过Zookeeper上的数据节点(临时次序节点)来表现一个锁

②获取锁

在必要获取共享锁时,所有客户端都会到 /shared_lock 这个节点下面创建一个临时次序节点,假如当前是读哀求,那么就会创建例如 /shared_lock/host1-R-00000001的节点;假如是写哀求,那么就会创建例如 /shared_lock/host2-W-00000002的节点。
通过Zookeeper来确定分布式读写次序,大抵分为四步:

  • 创建完节点后,获取 /shared_lock 节点下所有子节点,并对该节点变更注册监听
  • 确定本身的节点序号在所有子节点中的次序
  • 对于读哀求:若没有比本身序号小的子节点或所有比本身序号小的子节点都是读哀求,那么表明本身已经成功获取到共享锁,同时开始实验读逻辑,如有写哀求,则必要等候。对于写哀求:若本身不是序号最小的节点,那么必要等候
  • 接受到Watcher通知后,重复步骤1
③释放锁

在“定义锁”部分提到,/shared_lock/lock 是一个临时节点,因此在以下两种环境下,都有可能释放锁。

  • 当前获取锁的客户端呆板发生宕机,那么Zookeeper上的这个临时节点就会被移除
  • 正常实验完业务逻辑后,客户端就会主动将本身创建的临时节点删除。
无论在什么环境下一处了lock节点,Zookeeper都会通知在 /exclusive_lock 节点上注册了子节点变更Watcher监听的客户端。这些客户端在接收到通知后,再次重新发起分布式锁获取,即重复“获取锁”过程。
3)羊群效应


针对如上图所示的环境进行分析

  • host1起首进行读操作,完成后将节点 /shered_lock/host1-R-0000001删除
  • 余下4台呆板均收到这个节点移除的通知,然后重新从 /shared_lock节点上获取一份新的子节点列表
  • 每台呆板判定本身的读写次序,其中host2检测到本身序号最小,于是进行写操作,余下的呆板则继续等候
  • 继续…
其中,host1客户端在移除本身的共享锁后,Zookeeper发送了子节点变更Watcher通知给所有呆板,然而除了给host2产生影响外,对其他呆板没有任何作用。大量的Watcher通知和子节点列表获取两个操作会重复运行,这样不但会对zookeeper服务器造成巨大的性能影响和网络开销,更为严峻的是,假如同一时间有多个节点对应的客户端完成事务或是事务停止引起节点消失,Zookeeper服务器就会在短时间内向其余客户端发送大量的事件通知,这就是所谓的羊群效应
改进:
每个锁竞争者,只必要关注 /shared_lock节点下序号比本身小的谁人节点是否存在即可,详细实现如下:

  • 客户端调用create接口常见类似于 /shared_lock[Hostname]-哀求范例-序号的临时次序节点
  • 客户端调用getChildren接口获取所有已经创建的子节点列表(不注册任何Watcher)
  • 假如无法获取共享锁,就调用exist接口来对比本身小弟节点注册Watcher。对于读哀求:向比本身序号小的最后一个写哀求注册Watcher监听。对于写哀求:向比本身序号小的最后一个节点注册Watcher监听
  • 等候Watcher通知,继续进入步骤2
此方案改动重要在于:每个锁竞争者,只必要关注 /shared_lock 节点下序号比本身小的谁人节点是否存在即可

6.分布式队列

分布式队列可以简朴分为两大类:一种是通例的FIFO先入先出队列模型,还有一种是等候队列元素聚集后统一安排处理实验的Barrier模型
1)FIFO先入先出队列

FIFO队列就类似于一个全写的共享锁模型,大体的筹划思路:所有客户端都会到 /queue_fifo 这个节点下面创建一个临时次序节点,例如 /queue_fifo/host1-00000001

创建完节点后,根据如下4个步骤来确定实验次序:

  • 通过调用getChildren接口来获取 /quque_fifo 节点的所有子节点,即获取队列中所有的元素
  • 确定本身的节点序号在所有子节点中的次序
  • 假如本身的序号不是最小,那么必要等候,同时向比本身序号小的最后一个节点注册Watcher监听
  • 接受到Watcher通知后,重复步骤1

2)Barrier:分布式屏障

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

创建完节点后,按照如下步骤实验:

  • 通过调用getData接口获取 /queue_barrier节点的数据内容:10
  • 通过调用getChildren接口获取 /queue_barrier节点下的所有子节点,同时注册对子节点变更的Watcher监听
  • 统计子节点的个数
  • 假如子节点个数还不敷10个,那么必要等候
  • 接受到Watcher通知后,重复步骤2


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

嚴華

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表