Zookeeper分布式一致性原理(十):Zookeeper在大型分布式系统的应用

1. Hadoop

在Hadoop中,ZooKeeper主要用于实现HA(High Availability),这部分逻辑主要集中在Hadoop Common的HA模块中,HDFS的NameNode与YARN的ResourceManger都是基于此HA模块中,HDFS的NameNode与YARN的ResourceManger都是基于此HA模块来实现自己的HA功能的。同时,在YARN中又特别提供了ZooKeeper来存储应用的运行状态。

HDFS的NameNode和YARN的ResourceManager都是基于此HA模块来实现自己的HA功能的。同时,在YARN中又特别提供了Zookeeper来存储应用的运行状态。

1.1 YARN介绍

YARN是Hadoop为了提高计算节点Master的扩展性,同时为了支持多计算模型和提供资源的细粒度调度而引入的全新一代分布式调度框架。其上可以支持MapReduce计算引擎,也支持其他的一些计算引擎,如Tez、Spark、Storm、Imlala和Open MPI等。其架构体系如下图所示。

在这里插入图片描述

1.2 ResourceManager单点问题

ResourceManager是YARN中非常复杂的一个组件,负责集群中所有资源的统一管理和分配,同时接收来自各个节点(NodeManager)的资源汇报信息,并把这些信息按照一定的策略分配给各个应用程序(Application Manager),其内部维护了各个应用程序的ApplicationMaster信息、NodeManager信息以及资源使用信息等。因此,ResourceManager的工作状态直接决定了整个YARN框架是否可以正常运转。

为了解决ResourceManager的这个单点问题,YARN设计了一套Active/Standby模式的ResourceManager HA架构,如下图所示。

在这里插入图片描述

从上图中可以看出,在运行期间,会有多个ResourceManager并存,并且其中只有一个ResourceManager处于Active状态,另外的一些(允许一个或者多个)则是处于Standby状态,当Active节点无法正常工作(如机器挂掉或重启等)时,其余处于Standby状态的节点则会通过竞争选举产生新的Active节点。

主备切换

下面我们就来看看YARN是如何实现多个ResourceManager之间的主备切换的ResourceManager使用基于ZooKeeper实现的ActiveStandbyElector组件来确定ResourceManager的状态:Active或Standby。具体做法如下。

  1. 创建锁节点。

在ZooKeeper上会有一个类似于/yarn-leader-election/pseudo-yarn-rm-cluster的锁节点,所有的ResourceManager在启动的时候,都会去竞争写一个Lock子节点:/yarn-leader-election/pseudo-yarn-rm-cluster/ActiveStandbyElectorLock,同时需要注意的是,该子节点的类型是临时节点。ZooKeeper能够为我们保证最终只有一个ResourceManager能够创建成功。创建成功的那个ResourceManager就切换为Active状态,没有成功的那些ResouorceManager则切换为Standby状态。

  1. 注册Watcher监听。

所有Standby状态的ResourceManager都会向/yarn-leader-election/pseudo-yarn-rm-cluster/ActiveStandbyElectorLock节点注册一个节点变更的Watcher监听,利用临时节点的特性,能够快速感知到Active状态的ResourceManager的运行情况。

  1. 主备切换。

当Active状态的ResourceManager出现诸如重启或挂掉的异常情况时,其在ZooKeeper上创建的Lock节点也会随之被删除。此时其余各个Standby状态的ResourceManager都会接收到来自ZooKeeper服务端的Watcher事件通知,然后会重复进行步骤1的操作。

以上就是利用ZooKeeper来实现ResourceManager的主备切换的过程。ActiveStandbyElector组件位于Hadoop-Common工程的org.apache.hadoop.ha包中,其封装了ResourceManager和ZooKeeper之间的通信与交互过程,下图展示了ActiveStandbyElector的概要类图。

在这里插入图片描述

HDFS中的NameNode和ResourceManager模块都是使用该组件来实现各自的HA的。

1.3 Fencing(隔离)

在上述主备切换过程中,我们假设RM集群由ResourceManager1和ResourceManager2两台机器组成,且ResourceManager1为Active状态,ResourceManager2为Standby状态。某一时刻,ResourceManager1发生了“假死”现象,此时ZooKeeper认为ResourceManger1挂了,根据上述主备切换逻辑,ResourceManager2就会成为Active状态。这就是我们常说的分布式“脑裂”(Brain-Split)现象,即存在了多个处于Active状态的ResourceManger各司其职。那么该如何解决这样的问题呢?

YARN中引入了Fencing机制,借助ZooKeeper数据节点的ACL权限控制机制来实现不同RM之间的隔离。具体做法其实非常简单,在上文的“主备切换”部分中我们讲到,多个RM之间通过竞争创建锁节点来实现主备状态的确定。这个地方需要改进的一点是,创建的根节点必须携带ZooKeeper的ACL信息,目的是为了独占该根节点,以防止其他RM对该节点进行更新。

RM1恢复之后,会试图去更新ZooKeeper的相关数据,但是此时发现其没有权限更新ZooKeeper的相关节点数据,也就是说,RM1发现ZooKeeper上的相关节点不是自己创建的,于是就自动切换为Standby状态,这样就避免了“脑裂”现象的出现。

1.4 ResourceManger状态存储

在ResourceManger中,RMStateStore能够存储一些RM的内部状态信息,包括Application以及他们的Attempts信息、Delegation Token及Version Information等。

  • 基于内存实现,一般是用于日常开发测试。
  • 基于文件系统的实现,如HDFS。
  • 基于ZooKeeper的实现。

由于这些状态信息的数据量都不是特别大,因此Hadoop官方建议基于ZooKeeper来实现状态信息的存储。在ZooKeeper上,ResourceManger的状态信息都被存储在/rmstore这个根节点下面,其数据节点的组织结构如下图所示。

在这里插入图片描述

通过上图我们可以大致了解RMStateStore状态信息在ZooKeeper上的存储结构,其中RMAppRoot节点下存储的是与各个Application相关的信息,RMDTSecretManagerRoot存储的是与安全相关的Token等信息。每个Active状态的ResourceManger在初始化阶段都会从ZooKeeper上读取到这些状态信息,并根据这些状态信息继续进行相应的处理。

2. HBase

HBase在实现上严格遵守了Google BigTable论文的设计思想。BigTable使用Chubby类负责分布式状态的协调,Chubby,这是Google实现的一种基于Paxos算法的分布式锁服务,而HBase则采用了开源的ZooKeeper服务来完成对整个系统的分布式协调工作。下图中展示了整个HBase架构及其与ZooKeeper之间的结构关系。

在这里插入图片描述

从上图中可以看到,在HBase的整个架构体系中,ZooKeeper是串联起HBase集群与Client的关键所在。

从上图中可以看到,在HBase的整个架构体系中,ZooKeeper是串联起HBase集群与Client的关键所在。

2.1 系统容错

当HBase启动的时候,每个RegionServer服务器都会到ZooKeeper的/hbase/rs节点下创建一个信息节点(下文中,我们称该节点为“rs状态节点”),例如/hbase/rs/[Hostname],同时,HMaster会对这个节点注册监听。当某个RegionServer挂掉的时候,ZooKeeper会因为在一段时间内无法接收其心跳信息(即Session失效),而删除掉该RegionServer服务器对应的rs状态节点。与此同时,HMaster则会接收到ZooKeeper的NodeDelete通知,从而感知到某个节点断开,并立即开始冗错工作——在HBase的实现中,HMaster会将该RegionServer所处理的数据分片(Region)重新路由到其他节点上,并记录到Meta信息中供客户端查询。

讲到这里,可能有的读者会发问:HBase为什么不直接让HMaster来负责进行RegionServer的监控呢?HBase之所以不适用HMaster直接通过心跳机制等来管理RegionServer状态,是因为在这种方式下,随着系统容量的不断增加,HMaster的管理负担会越来越重。另外他自身也有挂掉的可能,因此数据还需要有持久化的必要。在这种情况下,ZooKeeper就成为了理想的选择。

2.2 RootRegion管理

对于HBase集群来说,数据存储的位置信息是记录在元数据分片,也就是RootRegion上的。每次客户端发起新的请求,需要知道数据的位置,就会去查询RootRegion,而RootRegion自身的位置则是记录在ZooKeeper上的(默认情况下,是记录在ZooKeeper的/hbase/root-region-server节点中)。当RootRegion发生变化,比如Region的手工移动、Balance或者是RootRegion所在服务器发生了故障等时,就能够通过ZooKeeper来感知到这一变化并做出一系列相应的容灾措施,从而保障客户端总是能拿到正确的RootRegion信息。

2.3 Region状态管理

Region是HBase中数据的物理切片,每个Region中记录了全局数据的一小部分,并且不同的Region之间的数据是相互不重复的。但对于一个分布式系统来说,Region是会经常发生变更的,这些变更的原因来自于系统故障、负载均衡、配置修改、Region分裂与合并等。一旦Region发生移动,他必然会经历Offline和重新Online的过程。

在Offline期间数据是不能被访问的,并且Region的这个状态变化必须让全局知晓,否则可能会出现某些事务性的异常。而对于HBase集群来说,Region的数量可能会多达10万级别,甚至更多,因此这样规模的Region状态管理也只有依靠ZooKeeper这样的系统才能做到。

2.4 分布式SplitLog任务管理

当某台RegionServer服务器挂掉时,由于总有一部分新写入的数据还没有持久化到HFile中,因此在迁移该RegionServer的服务时,一个重要的工作就是从HLog汇总恢复这部分还在内存中的数据,而这部分工作最关键的一步就是SplitLog,即HMaster需要遍历该RegionServer服务器的HLog,并按Region切分成小块移动到新的地址下,并进行数据的Replay。

由于单个RegionServer的日志量相对庞大(可能有数千个Region,上GB的日志),而用户又往往希望系统能够快速完成日志的恢复工作。因此一个可行的方案是将这个处理HLog的任务分配给多台RegionServer服务器来共同处理,而这就又需要一个持久化组件来辅助HMaster完成任务的分配。当前的做法是,HMaster会在ZooKeeper上创建一个splitlog的节点(默认情况下,是/hbase/splitlog节点),将“哪个RegionServer处理哪个Region”这样的信息以列表的形式存放到该节点上,然后由各个RegionServer服务器自行到该节点上去领取任务并在任务执行成功或失败后再更新该节点的信息,以通知HMaster继续进行后面的步骤。ZooKeeper在这里担负起了分布式集群中相互通知和信息持久化的角色。

2.5 Replication管理

Replication是实现HBase中主备集群间的实时同步的重要模块。有了Replication,HBase就能实现实时的主备同步,从而拥有了容灾和分流等关系型数据库才拥有的功能,从而大大加强了HBase的可用性,同时也扩展了其应用场景。和传统关系型数据库的Replication功能所不同的是,HBase作为分布式系统,他的Replication是多对多的,且每个节点随时都有可能挂掉,因此在这样的场景下做Replication要比普通数据库复杂的多。

HBase同样借助ZooKeeper来完成Replication功能。做法是在ZooKeeper上记录一个replication节点(默认情况下,是/hbase/replication节点),然后把不同的RegionServer服务器对应的HLog文件名称记录到相应的节点上,HMaster集群会将新增的数据推送给Slave集群,并同时将推送信息记录到ZooKeeper上(我们将这个信息称为“断点记录”),然后再重复以上过程。当服务器挂掉时,由于ZooKeeper上已经保存了断点信息,因此只要有HMaster能够根据这些断点信息来协调用来推送HLog数据的主节点服务器,就可以继续复制了。

3. Kafka

Kafka主要用于实现低延迟的发送和收集大量的事件和日志数据—— 这些数据通常都是活跃的数据。所谓活跃数据,在互联网大型的Web网站应用中非常常见,通常是指网站的PV数和用户访问记录等。这些数据通常以日志的形式记录下来,然后由一个专门的系统来进行日志的收集与统计。

Kafka是一个吞吐量极高的分布式消息系统,其整体设计是典型的分布与订阅模式系统。在Kafka集群中,没有“中心主节点”的概念,集群中所有的服务器都是对等的,因此,可以在不做任何配置更改的情况下实现服务器的添加与删除,同样,消息的生产者和消费者也能够做到随意重启和机器的上下线。Kafka服务器及消息生产者和消费者之间部署关系如下图所示。

在这里插入图片描述

3.1 术语介绍

  • 消息生产者,即Producer,是消息产生的源头,负责生成消息并发送到Kafka服务器上。
  • 消息消费者,即Consumer,是消息的使用方,负责消费Kafka服务器上的消息。
    主题,即Topic,由用户定义并配置在Kafka服务端,用于建立生产者和消费者之间的订阅关系:生产者发送消息到指定Topic下,消费者从这个Topic下消费消息。
  • 消息分区,即Partition,一个Topic下面会分为多个分区,例如“kafka-test”这个Topic可以分为10个分区,分别由两台服务器提供,那么通常可以配置为让每台服务器提供5个分区,假设服务器ID分别为0和1,则所有分区为0-0、0-1、0-2、0-3、0-4和1-0、1-1、1-2、1-3、1-4.消息分区机制和分区的数量与消费者的负载均衡机制有很大关系。
  • Broker,即Kafka的服务器,用于存储消息,在消息中间件中通常被称为Broker。
  • 消费者分组,即Group,用于归组同类消费者。在Kafka中,多个消费者可以共同消费一个Topic下的消息,每个消费者消费其中的部分消息,这些消费者就组成了一个分组,拥有同一个分组名称,通常也被称为消费者集群。
  • Offset,消息存储在Kafka的Broker上,消费者拉取消息数据的过程中需要知道消息在文件中的偏移量,这个偏移量就是所谓的Offset。

3.2 Broker注册

Kafka是一个分布式的消息系统,这也体现在其Broker、Producer和Consumer的分布式部署上。虽然Broker是分布式部署并且相互之间是独立运行的,但还是许哟啊有一个注册系统能够将整个集群中的Broker服务器都管理起来。在Kafka的设计中,选择了使用ZooKeeper来进行所有Broker的管理。

在ZooKeeper上会有一个专门用来进行Broker服务器列表记录的节点,下文中我们称之为“Broker节点”,其节点路径为/brokers/ids。

每个Broker服务器在启动时,都会到ZooKeeper上进行注册,即到Broker节点下创建属于自己的节点,其节点路径为/broker/ids/[0…N]。

从上面的节点路径中,我们可以看出,在Kafka中,我们使用一个全局唯一的数字来指代每一个Broker服务器,可以称其为“Broker ID”,不同的Broker必须使用不同的Broker ID进行注册,例如/broker/ids/1 和/boker/ids/2 分别代表了两个Broker服务器。创建完Broker节点后,每个Broker就会将自己的IP地址和端口等信息写入到该节点中去。

请注意,Broker创建的节点是一个临时节点,也就是说,一旦这个Broker服务器宕机或是下线后,那么对应的Broker节点也就被删除了。因此我们可以通过ZooKeeper上Broker节点的变化情况来动态表征Broker服务器的可用性。

3.3 Topic注册

在Kafka中,会将同一个Topic的消息分成多个分区并将其分布到多个Broker上,而这些分区信息以及与Broker的对应关系也都是由ZooKeeper维护的,由专门的节点来记录,其节点路径为/brokers/topics。下文呢种我们将这个节点称为“Topic节点”。Kafka中的每一个Topic,都会以/brokers/topics/[topic]的形式记录子啊这个节点下,例如/brokers/topics/login和/brokers/topics/search等。

Broker服务器在启动后,会到对应的TOpic节点下注册自己的Broker ID,并写入针对该Topic的分区总数。例如,/brokers/topics/login/3→2这个节点表名Broker ID为3的一个Broker服务器,对于“login”这个Topic的消息,提供了2个分区进行消息存储。同样的,这个分区数节点也是一个临时节点。

3.4 生产者负载均衡

在上面的内容中,我们讲解了Kafka是分布式部署Broker服务器的,会对同一个Topic的消息进行分区并将其分布到不同的Broker服务器上。因此,生产者需要将消息合理的发送到这些分布式的Broker上——这就面临一个问题:如何进行生产者的负载均衡。对于生产者的负载均衡,Kafka支持传统的四层负载均衡,同时也支持使用ZooKeeper方式来实现负载均衡,这里我们首先来看使用四层负载均衡的方案。

在Kafka中,客户端使用了基于ZooKeeper的负载均衡策略来解决生产者的负载均衡问题。在前面内容中也已经提到,每当一个Broker启动时,会首先完成Broker注册过程,并注册一些诸如“有哪些可订阅的Topic”的元数据信息。生产者就能够通过这个节点的变化来动态的感知到Broker服务器列表的变更。在实现上,Kafka的生产者会对ZooKeeper上的“Broker的新增与减少”、“TOpic的新增与减少”和“Broker与Topic关联关系的变化”等事件注册Watcher监听,这样就可以实现一种动态的负载均衡机制了。此外,在这种模式下,还能够允许开发人员控制生产者根据一定的规则(例如根据消费者的消费行为)来进行数据分区,而不仅仅是随机算法而已——Kafka将这汇总特定的分区策略称为“语义分区”。显然,ZooKeeper在整个生产者负载均衡的过程中扮演了非常重要的角色,通过ZooKeeper的Watcher通知能够让生产者动态的获取Broker和Topic的变化情况。

3.5 消费者负载均衡

与生产类似,Kafka中的消费者同样许哟啊进行负载均衡来实现多个消费者合理的从对应的Brooker服务器上接收消息。Kafka有消费者分组的概念,每个消费者分组中都包含了若干个消费者,每一条消息都只会发送给分组中的一个消费者,不同的消费者分组消费自己特定Topic下面的消息,互不干扰,也不需要互相进行协调。因此消费者的负载均衡也可以看作是同一个消费者分组内部的消息消费策略。

对于每个消费者分组,Kafka都会为其分配一个全局唯一的Group ID,同一个消费者分组内部的所有消费者都共享该ID。同时,Kafka也会为每个消费者分配一个Consumer ID,通常采用“Hostname:UUID”的形式来表示。在Kafka的设计中,规定了每个消息分区有且只能同时有一个消费者进行消息的消费,因此,需要在ZooKeeper上记录下消息分区与消费者之间的对应关系。每个消费者一旦确定了对一个消息分区的消费权利,那么需要将其Consumer ID写入到对应消息分区的临时节点上,例如/consumers/[group_id]/owners/[topic]/[broker_id-partition_id],其中“[broker_id-partition_id]”就是一个消息分区的标识,节点内容就是消费该分区上消息的消费者的Consumer ID。

消息消费进度Offset记录

在消费者对指定消息分区进行消息消费的过程中,需要定时的将分区消息的消费进度,即Offset记录到ZooKeeper上去,以便在该消费者进行重启或是其他消费者重新接管该消息分区的消息消费后,能够从之前的进度开始继续进行消息的消费。Offset在ZooKeeper上的记录由一个专门的节点负责,其节点路径为/consumers/[group_id]/offsets/[topic]/[broker_id-partition_id],其节点内容就是Offset值。

3.6 消费者注册

消费者服务器在初始化启动时加入消费者分组的步骤如下

① 注册到消费者分组。每个消费者服务器启动时,都会到Zookeeper的指定节点下创建一个属于自己的消费者节点,例如/consumers/[group_id]/ids/[consumer_id],完成节点创建后,消费者就会将自己订阅的Topic信息写入该临时节点。

② 对消费者分组中的消费者的变化注册监听。每个消费者都需要关注所属消费者分组中其他消费者服务器的变化情况,即对/consumers/[group_id]/ids节点注册子节点变化的Watcher监听,一旦发现消费者新增或减少,就触发消费者的负载均衡。

③ 对Broker服务器变化注册监听。消费者需要对/broker/ids/[0-N]中的节点进行监听,如果发现Broker服务器列表发生变化,那么就根据具体情况来决定是否需要进行消费者负载均衡。

④ 进行消费者负载均衡。为了让同一个Topic下不同分区的消息尽量均衡地被多个消费者消费而进行消费者与消息分区分配的过程,通常,对于一个消费者分组,如果组内的消费者服务器发生变更或Broker服务器发生变更,会发出消费者负载均衡。

3.7 负载均衡

Kafka借助Zookeeper上记录的Broker和消费者信息,采用消费者均衡算法进行负载均衡,其具体步骤如下。假设一个消息分组的每个消费者记为C1,C2,Ci,…,Cn。那么对于消费者Ci,其对应的消息分区分配策略如下:

  1. 设置Pr为指定Topic所有的消息分区。

  2. 设置Cg为统一消费者分组中的所有消费者。

  3. 对Pr进行排序,使分布在同一个Broker服务器上的分区尽量靠在一起。

  4. 对Cg进行排序。

  5. 设置i为Ci在Cg中的位置索引,同时设置N = size (Pr) / size (Cg)。

  6. 将编号为i * N ~ (i + 1) * N - 1的消息分区分配给Ci。

  7. 重新更新Zookeeper上消息分区与消费者Ci的关系。

猜你喜欢

转载自blog.csdn.net/qq_21125183/article/details/86557263