ZooKeeper学习笔记及应用场景梳理

Zookeeper官网文档地址:https://zookeeper.apache.org/doc/r3.5.4-beta/zookeeperOver.html

概述

Zookeeper从设计模式角度来理解:是一个基于观察者模式设计的分布式服务管理框架, 它负责存储和管理大家都关心的数据, 然后接受观察者的注册, 一旦这些数据的状态发生变化, Zookeeper就将负责通知已经在Zookeeper上注册的那些观察者做出相应的反应 , 从而实现集群中类似Master/Slave管理模式。

Zookeeper 是一个分布式的服务框架,主要用来解决分布式集群中应用系统的协调和一致性问题,它能提供基于类似于文件系统的目录节点树方式的数据存储,但是 Zookeeper 并不是用来专门存储数据的,它的作用主要是用来维护和监控你存储的数据的状态变化。如:统一命名服务、状态同步服务、集群管理、分布式应用配置管理等。

ZooKeeper作为一个分布式的协调服务框架,主要用来解决分布式集群中,应用系统需要面对的各种通用的一致性问题。ZooKeeper本身可以部署为一个集群,集群的各个节点之间可以通过选举来产生一个Leader,选举遵循半数以上的原则,所以一般集群需要部署奇数个节点。ZooKeeper最核心的功能是,它提供了一个分布式的存储系统,数据的组织方式类似于UNIX文件系统的树形结构。由于这是一个可以保证一致性的存储系统,所以你可以放心地在你的应用集群中读写ZooKeeper的数据,而不用担心数据一致性的问题。分布式系统中一些需要整个集群所有节点都访问的元数据,比如集群节点信息、公共配置信息等,特别适合保存在ZooKeeper中

它能够为分布式应用提供高性能和可靠地协调服务,使用ZooKeeper可以大大简化分布式协调服务的实现,为开发分布式应用极大地降低了成本。协同服务很难正确运行,经常出现竞争危害和死锁。ZooKeeper 的目的就是降低协同服务实现与维护的成本。

架构及原理

集群架构

在这里插入图片描述
Zookeeper集群是由一组Server节点组成,这一组Server节点中存在一个角色为Leader的节点,其他节点都为Follower。客户端可以和集群中的任一Server建立连接,当读请求时,所有Server都可以直接返回结果;当请求为数据变更请求时,Follower会将请求转发给Leader节点,Leader节点接收到数据变更请求后,首先会将变更写入本地磁盘,当持久化完毕后才会将变更写入内存,并将变更后的数据同步到各个Follower。

Zookeeper集群中一共有以下4种角色:
(1) Leader,负责进行投票的发起和决议,更新状态和数据
(2) Follower,用于接收客户端请求并向客户端返回结果,在选择Leader时会进行投票
(3) Observer,一种功能和Follower相同,但是它不参与投票过程。它主要是为了扩展系统,提高读取速度
(4) Client,客户端用来发起请求,严格地说不属于Zookeeper集群

ZooKeeper设计目标

易用性,ZooKeeper允许分布式的进程彼此通过一个共享的结构化的命名空间作协调,这个命名空间组织结构和标准的文件系统相似。命名空间由数据节点(znodes)组成,类似于文件与目录,与专为存储设计的典型文件系统不同,ZooKeeper的数据都保存在内存中,以获得高吞吐和低时延的特性。

ZooKeeper实现了高性能、高可用和严格有序。高性能意味着可以被应用在大型分布式系统,高可用则避免了单点故障的风险,严格有序保证了复杂的同步原语可以在客户端实现。

可复制,就像它所协同的分布式进程一样,ZooKeeper 的组件也是可复制的。构成 ZooKeeper 服务的服务器必须互相感知,它们各自维护了一张相同的内存状态镜像,以及持久存储的事务日志与快照,只要大部分的服务器可用,ZooKeeper 的服务就可以正常运行。客户端会维护与某一台 ZooKeeper 服务器的 TCP 长连接,并通过该连接进行请求发送,响应接收,监听事件获取与心跳检测。如果当前 TCP 长连接断开,客户端会向另一台服务器发起连接请求。

有序性,ZooKeeper 为每一次更新标记了一个序号,以反映所有事务的顺序。后续操作可以使用该序号实现更高级的抽象,例如同步原语。

速度快,ZooKeeper 在“读频繁”工作场景中性能优异,在数千台集群中运行,读写比为 10:1 时,性能最好。

核心数据结构

Zookeeper的核心数据结构是如下图的树形结构:
在这里插入图片描述
ZooKeeper提供的命名空间非常像一个标准的文件系统。一个名字是一系列的被/分开的路径元素,在ZooKeeper中每一个节点被标示为一个路径。

但是和标准的文件系统不同,每一个ZooKeeper中的节点可以有数据和它关联,子节点也一样。就像一个文件系统允许一个文件是一个目录。(ZooKeeper被设计用来存储协调数据:状态信息,配置,地址信息,等等。所以存储在每一个节点的数据通常很小,在字节和千字节的范围。)我们使用znode来使我们清楚我们正在讨论ZooKeeper的数据节点。

  • 节点ZNode存储同步、协调相关的数据,数据量比较小,比如状态信息、配置内容、位置信息等。
  • ZNode中存有状态信息,包括版本号、ACL变更、时间戳等, 每次变更版本号都会递增。这样一方面可以基于版本号检索状态;另一方面可以实现分布式的乐观锁。
  • ZNode都有ACL,可以限制ZNode的访问权限
  • ZNode上数据的读写都是原子的
  • 客户端可以在ZNode上设置Watcher监听,一但该ZNode有数据变更,就会通知客户端,触发回调方法【这个地方需要注意,Watcher都是一次性,触发一次后就失效,持续监听需要重新注册】
  • 客户端和Zookeeper连接建立后就是一次session,Zookeeper支持临时节点,它和一次session关联,一但session关闭,节点就被删除。【可以用临时节点来实现连通性的检测】

ZNode可以分为持久节点和临时节点两类。持久节点是指一旦该ZNode被创建了,除非主动进行删除操作,这个节点就会一直存在;而临时节点的生命周期会和客户端会话绑定在一起,一旦客户端会话失效其所创建的所有临时节点都会被删除

ZK还支持客户端创建节点时指定一个特殊的SEQUENTIAL属性,这个节点被创建的时候ZK会自动在其节点名后面追加上一个整形数字,该数字是一个由服务端维护的自增数字,以此实现创建名称自增的顺序节点。

担保

ZooKeeper是非常快和简单的。尽管它的目标是构建例如同步这样更复杂服务的基础,它提供了一组保证它们是:

  • 顺序的一致性:来自客户端的更新将会按照它们发送的顺序应用;
  • 原子性:更新要么成功要么失败,没有部分结果;
  • 单一系统映象:客户端将会看到服务端相同的视图,不管它连的是那一个服务端;
  • 可靠性:一旦更新成功,它将一直持续到被下一个客户端更新覆盖;
  • 时效性:系统的客户端视图保证是最新的在一定的时间内;

简单API

ZooKeeper 的设计目标之一是提供非常简单的可编程接口,因此,它只提供如下操作:

  • 创建(creat),在目录树中某个位置创建一个节点;
  • 删除(delete),删除一个节点;
  • 存在性检查(exists),测试节点是否存在于某个位置;
  • 获取数据(get data),从节点中读取数据;
  • 设置数据(set data),向节点中写入数据;
  • 获取子节点(get children),检索节点的子节点列表;
  • 同步(syc),等待数据被传播;

工作机制

Zookeeper的核心是Zab(Zookeeper Atomic Broadcast)协议。Zab协议有两种模式,它们分别是恢复模式和广播模式。

恢复模式

当Zookeeper集群启动或Leader崩溃时,就进入到该模式。该模式需要选举出新的Leader,选举算法基于paxos或fastpaxos。

  1. 每个Server启动以后都会询问其它的Server投票给谁;
  2. 对于其他Server的询问,Server每次根据自己的状态回复自己推荐,Leader的id和该Server最后处理事务的zxid(zookeeper中的每次变更事务都会被赋予一个顺序递增的zxid,zxid越大说明变更越近),Server刚启动时都会选择自己;
  3. 收到Server的回复后,就计算出zxid最大的那个Server,将该Server的信息设置成下次要投票的Server(如果zxid同样大,就选择Server id大的那个);
  4. 计算获得票数最多的Server,如果该Server的得票数超过半数,则该Server当选Leader,否则继续投票直到Leader选举出来。

假设一个Zookeeper集群有5台机器,ServerId分别为1、2、3、4、5,启动顺序按照1、2、3、4、5依次启动:

  1. Server 1启动,此时它选择自己为leader,同时向外发出投票报文,但收不到任何回复,选票不过半,启动机器数不超过集群的一半(不能正常工作);
  2. Server2启动,此时由于没有历史数据,Server1和Server2会选择ServerId较大的2位leader,但选票不超过一半,启动的机器数不超过集群的一半(不能正常工作);
  3. Server3启动,此时情况与2类似,Server3会被选为Leader,但不同的是此时得票过半,并且启动的机器数超过集群的一半,所以集群可以正常工作,Server3被选为Leader;
  4. Server4启动,由于此时Server3已经被选为Leader,所以Server4只能作为Follower;
  5. Server5启动,与d)同理,Server5也只能作为Follower

广播模式

Leader选举完毕后,Leader需要与Follower进行数据同步:

  1. leader会开始等待server连接;
  2. follower连接leader,将最大的zxid发送给leader;
  3. leader根据follower的zxid确定同步点;
  4. 完成同步后通知follower 已经成为uptodate状态;
  5. follower收到uptodate消息后,就可以重新接受client的请求进行服务了。

问题:对于某个更新请求,Leader通过ZAB协议向3个Follower(a, b, c)发出更新请求, 如果Follower a与Follower b都正常返回,而Follower c 宕机了, 这个更新请求对client来说依然是更新成功的。后来Follower c 机器恢复正常了,可是Follower c 机器上的数据已经是过期了,那Follower c 是如何让自己机器的数据更新到最新的数据呢?

宕机的Follower c在恢复时会有一个故障恢复阶段(即上面提到的恢复模式),在这个阶段会主动同步Leader数据,达到一致后才会重新加入到ZK集群并参与服务。

ZooKeeper典型应用场景

数据发布/订阅(配置中心)

以Dubbo注册中心为例,Dubbo是阿里巴巴开源的分布式服务框架,致力于提供高性能和透明化的远程服务调用解决方案和基于服务框架展开的完整SOA服务治理方案。
在这里插入图片描述
其中服务自动发现是最核心的模块之一,该模块提供基于注册中心的目录服务,使服务消费方能够动态的查找服务提供方,让服务地址透明化,同时服务提供方可以平滑的对机器进行扩容和缩容,注册中心可以基于其提供的外部接口来实现各种不同类型的注册中心,例如数据库、ZooKeeper和Redis等。接下来看一下基于ZooKeeper实现的Dubbo注册中心。
在这里插入图片描述

  • /dubbo: 这是Dubbo在ZK上创建的根节点
  • /dubbo/com.test.testService: 这是服务节点,代表了Dubbo的一个服务;
  • /dubbo/com.test.testService/Providers: 这是服务提供者的根节点,其子节点代表了每个服务的真正提供者;
  • /dubbo/com.test.testService/Consumers: 这是服务消费者的根节点,其子节点代表了每一个服务的真正消费者

Dubbo基于ZK实现注册中心的工作流程:

  • 服务提供者:在初始化启动的时候首先在/dubbo/com.test.testService/Providers节点下创建一个子节点,同时写入自己的url地址,代表这个服务的一个提供者;
  • 服务消费者:在启动的时候读取并订阅ZooKeeper上/dubbo/com.test.testService/Providers节点下的所有子节点,并解析所有提供者的url地址类作为该服务的地址列表,开始发起正常调用。同时在Consumers节点下创建一个临时节点,写入自己的url地址,代表自己是BarService的一个消费者;
  • 监控中心:监控中心是Dubbo服务治理体系的重要一部分,它需要知道一个服务的所有提供者和订阅者及变化情况。监控中心在启动的时候会通过ZK的/dubbo/com.test.testService节点来获取所有提供者和消费者的url地址,并注册Watcher来监听其子节点变化情况。

所有服务提供者在ZK上创建的节点都是临时节点,利用的是临时节点的生命周期和客户端会话绑定的特性,一旦提供者机器挂掉无法对外提供服务时该临时节点就会从ZK上摘除,这样服务消费者和监控中心都能感知到服务提供者的变化

发布与订阅模型,即所谓的配置中心,顾名思义就是发布者将数据发布到ZooKeeper节点上,供订阅者动态获取数据,实现配置信息的集中式管理和动态更新。例如全局的配置信息,服务式服务框架的服务地址列表等就非常适合使用:

  1. 应用中用到的一些配置信息放到ZooKeeper上进行集中管理。这类场景通常是这样:应用在启动的时候会主动来获取一次配置,同时,在节点上注册一个Watcher,这样一来,以后每次配置有更新的时候,都会实时通知到订阅的客户端,从来达到获取最新配置信息的目的;
  2. 分布式搜索服务中,索引的元信息和服务器集群机器的节点状态存放在ZK的一些指定节点,供各个客户端订阅使用;
  3. 分布式日志收集系统。这个系统的核心工作是收集分布在不同机器的日志。收集器通常是按照应用来分配收集任务单元,因此需要在ZK上创建一个以应用名作为path的节点P,并将这个应用的所有机器ip,以子节点的形式注册到节点P上,这样一来就能够实现机器变动的时候,能够实时通知到收集器调整任务分配。

负载均衡

这里说的负载均衡是指软负载均衡。在分布式环境中,为了保证高可用性,通常同一个应用或同一个服务的提供方都会部署多份,达到对等服务。而消费者就须要在这些对等的服务器中选择一个来执行相关的业务逻辑,其中比较典型的是消息中间件中的生产者,消费者负载均衡。

消息中间件中发布者和订阅者的负载均衡,linkedin开源的KafkaMQ和阿里开源的metaq都是通过zookeeper来做到生产者、消费者的负载均衡。这里以metaq为例来说明。

生产者负载均衡:metaq发送消息的时候,生产者在发送消息的时候必须选择一台broker上的一个分区来发送消息,因此metaq在运行过程中,会把所有broker和对应的分区信息全部注册到ZK指定节点上,默认的策略是一个依次轮询的过程,生产者在通过ZK获取分区列表之后,会按照brokerId和partition的顺序排列组织成一个有序的分区列表,发送的时候按照从头到尾循环往复的方式选择一个分区来发送消息。

消费者负载均衡: 在消费过程中,一个消费者会消费一个或多个分区中的消息,但是一个分区只会由一个消费者来消费。MetaQ的消费策略是:

  • 每个分区针对同一个group只挂载一个消费者。
  • 如果同一个group的消费者数目大于分区数目,则多出来的消费者将不参与消费。
  • 如果同一个group的消费者数目小于分区数目,则有部分消费者需要额外承担消费任务。
  • 在某个消费者故障或者重启等情况下,其他消费者会感知到这一变化(通过zookeeper watch消费者列表),然后重新进行负载均衡,保证所有的分区都有消费者进行消费。

分布式锁

分布式锁是控制分布式系统之间同步访问共享资源的一种方式。如果不同系统或同一系统不同机器之间共享了同一资源,那访问这些资源时通常需要一些互斥手段来保证一致性,这种情况下就需要用到分布式锁了。

使用关系型数据库是一种简单、广泛的实现方案,但大多数大型分布式系统中数据库已经是性能瓶颈了,如果再给数据库添加额外的锁会更加不堪重负;另外,使用数据库做分布式锁,当抢到锁的机器挂掉的话如何释放锁也是个头疼的问题。

接下来看下使用ZK如何实现排他锁。排他锁的核心是如何保证当前有且只有一个事务获得锁,并且锁被释放后所有等待获取锁的事务能够被通知到。

在这里插入图片描述
如图所示,在需要获取排他锁时,所有客户端都会试图在/exclusive_lock下创建临时子节点/exclusive_lock/lock,最终只有一个客户端能创建成功,该客户端就获取到了锁。同时没有获取到锁的客户端需要到/exclusive_lock节点上注册一个子节点变更的Watcher监听,用于实时监听lock节点的变更情况。

/exclusive_lock/lock是一个临时节点,在一下两种情况下都有可能释放锁:

  • 当获取锁的客户端挂掉,ZK上的该节点会被删除;
  • 正常执行完业务逻辑之后客户端会主动将自己创建的临时节点删除;

无论在什么情况下删除了lock临时节点ZK都会通知在/exclusive_lock节点上注册了子节点变更Watcher监听的客户端,重新发起锁的获取。

分布式队列或Barrier

队列方面,简单地讲有两种,一种是常规的先进先出队列,另一种是要等到队列成员聚齐之后的才统一按序执行。对于第一种先进先出队列,和分布式锁服务中的控制时序场景基本原理一致,这里不再赘述。 第二种队列其实是在FIFO队列的基础上作了一个增强,可以理解为是分布式Barrier,举个栗子,在大规模分布式并行计算的场景下,最终的合并计算需要基于很多并行计算的子结果来进行,即系统需要满足特定的条件,一个队列的元素必须都聚齐之后才能进行后续处理,否则一直等待。

看下如何用ZK来支持这种场景。在这里插入图片描述
通常可以给/queue赋值n(假设n=10),表示队列或Task的大小,凡是其中一个子任务完成(就绪),那么就去/queue下建立自己的临时时序节点(CreateMode.EPHEMERAL_SEQUENTIAL),当 /queue 发现自己下面的子节点满足指定个数,就可以进行下一步处理了。
执行步骤:

  • 调用获取节点数据的api获取/queue节点的内容:10;
  • 调用获取子节点总数的api获取/queue下的所有子节点,并且注册对子节点变更的Watcher监听;
  • 统计子节点个数,如果子节点个数小于10则继续等待,否则打开屏障继续处理;
  • 接收到Watcher通知后,重复步骤2;

ZooKeeper在HBase中的应用

HBase全称Hadoop DataBase,是一个基于Hadoop文件系统设计、面向海量数据的高可靠性、高性能、面向列、可伸缩的分布式存储系统。在HBase向在线分布式存储方向发展过程中,开发者发现如果有RegionServer服务器挂掉时系统和客户端都无法及时得知信息,服务难以快速迁移到其它RegionServer服务器上,问题原因是缺少相应的分布式协调组件,于是后来ZooKeeper被加入到HBase的技术体系中。

目前ZooKeeper已经成为HBase的核心组件,应用场景包括系统容错、RootRegion管理、Region状态管理、分布式SplitLog任务管理和Replication管理,除此之外还包括HMaster选举、Table的enable/disable状态记录及几乎所有元数据的存储等。

小结

ZooKeeper是一个分布式的协调服务,它的核心服务是一个高可用、高可靠的一致性存储,在此基础上,提供了包括读写元数据、节点监控、选举、节点间通信和分布式锁等很多功能,这些功能可以极大方便我们快速开发一个分布式的集群系统。但ZooKeeper也并不是完美的,在使用的时候你需要注意几个问题:

  1. 不要往ZooKeeper里面写入大量数据,它不是一个真正意义上的存储系统,只适合存放少量的数据。依据服务器配置的不同,ZooKeeper在写入超过几百MB数据之后,性能和稳定性都会严重下降。
  2. 不要让业务集群的可用性依赖于ZooKeeper的可用性,什么意思呢?你的系统可以使用Zookeeper,但你要留一手,要考虑如果Zookeeper集群宕机了,你的业务集群最好还能提供服务。因为ZooKeeper的选举过程是比较慢的,而它对网络的抖动又比较敏感,一旦触发选举,这段时间内的ZooKeeper是不能提供任何服务的。
发布了92 篇原创文章 · 获赞 447 · 访问量 46万+

猜你喜欢

转载自blog.csdn.net/fuzhongmin05/article/details/104123101
今日推荐