【JAVA核心知识】37:Zookeeper的使用与典型应用场景 ---- 《从Paxos到Zookeeper》读书笔记

【JAVA核心知识】37:Zookeeper的使用与典型应用场景 ---- 《从Paxos到Zookeeper》读书笔记,第1张

【JAVA核心知识】37:Zookeeper的使用与典型应用场景 ---- 《从Paxos到Zookeeper》读书笔记 ZooKeeper的使用

ZooKeeper的使用主要是通过对ZooKeeper的数据节点进行 *** 作来完成各种功能。ZooKeeper包含四种节点:

持久节点持久顺序节点临时节点临时顺序节点

临时节点在客户端与服务器之间的会话失效后会被自动清除,持久节点不会。创建普通节点,会返回节点路径,如果创建了顺序节点,ZooKeeper会返回一个带后缀的节点路径,这个后缀就是一个递增的值。
对数据节点 *** 作之前需要先创建会话,ZooKeeper的会话创建过程是异步的过程,构造方法在处理完客户端初始化 *** 作后会立即返回,大多数情况下此时并没有创建一个可用的会话,会话真正创建完毕时,服务端会发送一个通知到客户端,因此如果需要创建之后立刻使用,那么开发者要自己控制创建完成后再往下走流程。
ZooKeeper提供了创建,删除,读取数据,更新数据,检测节点是否存在,权限控制几个对节点的 *** 作维度。每种 *** 作都为同步和异步两种方式。 可以通过读取数据和检测节点是否存在两个 *** 作来为节点注册Watcher。
默认情况下Zookeeper不支持递归创建,也就是说无法在父节点不存在的情况下创建一个子节点,但是可以通过createParents参数设置本次创建需要递归创建。如果一个节点已存在,再次创建非顺序节点就会抛出NodeExistsException异常。Zookeeper的节点内容只支持(byte[])类型,要存入的内容都需要进行一下转换。

ZooKeeper的Watcher

Watcher (事件监听器),是ZooKeeper中的一个很重要的特性。ZooKeeper允许用户在指定节点上注册一些Watcher ,并且在一些特定事件触发的时候,ZooKeeper服务端会 将事件通知到感兴趣的客户端上去,该机制是ZooKeeper实现分布式协调服务的重要特性。Watcher具有以下特性:

一次性:Watcher是一次性的,一旦Watcher被触发,ZooKeeper就会将其从相应的存储中移除,因此如果需要持续性关注某个节点的话,那么就需要在Watcher触发的处理逻辑中重复注册。 设计成一次性的方式能有效减轻服务端压力,否则对于那些变动频繁的节点,服务端需要频繁的向客户端发送事件通知,这对于网络和服务端性能影响都很大客户端串行执行:客户端Watcher的回调处理是一个串行同步的过程,整个客户端对所有节点注册的Watcher在触发后会按照触发先后顺序串行执行,可以理解为是单线程执行,ZooKeeper用这种方式保证顺序,因此Watcher中的逻辑要尽量简洁高效,避免一个Watcher影响了整个客户端的Watcher回调。轻量:WatchEvent是Watcher的最小通知单元,他仅包含通知状态,事件类型和节点路径三部分内容,也就是说Watcher只会告诉客户端节点发生了变化,而不会告诉客户端发生了何种变化,客户端需要自行去获取数据,这是使用ZooKeeper的Watcher机制需要注意的。另外,客户端在注册Watcher对象时,并不是真的将Watcher对象传递到服务端,而是仅仅在客户端请求中使用boolean类型进行标记,服务端通过标记识别是否需要观测,如果需要服务端也仅仅是保存当前连接的ServerCnxn对象。以上这种轻量级的设计使得ZooKeeper的Watcher机制在网络开心和服务器内存开销上都是非常廉价的。 数据发布/订阅

通过Zookeeper的Watcher通知可以完成数据的发布和订阅,Zookeeper的Watcher通知,具有以下特性:

    一次性:无论是客户端还是服务端,一旦一个Watcher被触发,Zookeeper都会将其从对应的存储中移除,因此开发人员在Watcher的使用上要记住的一点是需要反复注册,这样的设计有效的减轻了服务端的压力。避免Watcher一直有效而节点更新有非常频繁的情况下,服务端需要不停的对客户端发送通知,对网络和服务端性能的影响都很大。客户端串行执行:客户端Watcher回调的过程是一个串行同步的过程,因此来保证顺序,但是这也使得开发人员需要注意,对Watcher的处理逻辑要尽量精简,避免因为Watcher的处理逻辑影响了整个客户端的Watcher回调。轻量:WatchedEvent是Zookeeper中Watcher通知机制的最小通知单元,这个数据结构只包含三部分内容:通知状态,事件类型和节点路径。也就是数Watcher通知非常简单,只会告诉客户端发生了事件,而不会说明具体的内容。客户端收到这个消息通知后需要主动到服务端获取最新的数据。相当于一个推(push)拉(pull)结合的方式。另外客户端向服务端注册Watcher的时候,并不会把客户端真实的Watcher对象传递到服务端,仅仅只是在客户端请求中使用boolean类型属性进行了标记,同时服务端也仅仅只是保存了当前连接的ServerCnxn对象。这种轻量的Watcher机制设计,在网络开销和服务端内存开销上都是非常廉价的。

因此对于全局配置信息,在应用较少时我们可以维护在本地,定时读取来更新。但是如果集群应用较多,或者更新较为频繁,那数据的维护工作就变的十分困难,此时就可以通过Zookeeper来实现配置管理,在Zookeeper上注册一个数据节点,应用启动时先从Zookeeper中获取节点数据,然后针对目标节点向服务端注册Watcher事件,当节点数据变更时,服务端向客户端发出Watcher事件通知,客户端收到这个消息通知后主动到服务端获取最新的数据。完成数据的发布与订阅。

负载均衡

负载均衡用来对多个计算机(计算机集群),网络连接,CPU,磁盘驱动器或其他资源进行分配负载,以达到优化资源使用,最大化吞吐率,最小化响应时间和避免过载的目的。通常分为硬件和软件两类。分布式系统中负载均衡是一种普遍的计算,对于消费者而言,需要从对等的服务提供方中选择一个来执行相关的业务逻辑,其中比较典型的就是DNS服务。
DNS是域名系统的缩写,用来将域名和IP一一映射,可以通过向域名服务商申请域名注册,缺陷是这样只能注册有限的域名。因此在实际开发中,往往使用本地Host绑定来实现域名解析工作(就是通过在host文件中配置域名和ip的映射),此种方法可以很容易解决域名紧张的我那天,每一台应用都可以自行确定系统的域名和目标IP,开发人员还可以随时进行修改,然后这种方案同样适用于应用数较小时,当应用数达到一定规模或者域名变更频繁时,对每台机器的域名映射更新就变的复杂,且因为是分机器 *** 作,还存在着非同步的隐患。
因此我们可以通过Zookeeper来实现动态的DNS方案(简称DDNS),每个应用可以在Zookeeper建立属于自己的数据节点,数据节点中存放自己的域名配置。不同于Host方式配置完就行,DDNS需要自行维护映射关系,在应用启动时自己获取域名映射,并注册一个Watcher监听,以便实时获取到域名的变更。集群公用一个节点则可以实现集群内用域名映射的统一管理,减少维护成本。

除此之外我们还可以进一步优化来实现自动化的DNS服务,利用微服务和注册中心的理念,服务提供方在启动的过程同把自己的域名注册到Register集群中,Register获取到域名映射后将其维护到对应的Zookeeper节点。服务消费放使用域名时,向Dispatcher发出域名解析请求,Dispatcher收到请求后读取映射信息并返回给前端应用。同时DDNS还可以对所有注册的IP地址和断开进行可用性检查即健康度检测。一般有两种方式,第一种是服务端主动发起健康度心跳检测,这种方式一般需要在服务端和客户端建立一个TCP长链接,第二种则是客户端主动向服务端发起健康度心跳检查,即每隔一段时间进行一次汇报。 DNNS中采用的是第二种,服务提供方主动定时向Scanner进行状态汇报,Scanner记录每个服务提供方最近一次的汇报时间,一旦超过5s没有汇报,则认为该IP已不可用,进行域名清理:Scanner在Zookeeper中找到该域名对应的域名节点,然后将该IP地址和端口配置从节点内容中移除。

命名服务

JNDI便是一种典型的命名服务,为每一个资源定义一个唯一的名字来对资源进行管理。常常用来实现对数据源的配置和管理,开发人员不需要关系数据库相关的信息(数据库类型,JDBC驱动,数据账户)。Zookeeper提供的命名符合与JNDI有相似的地方。但是从广义的命名服务来说,上层应用需要的是一个全局唯一的名字,类似于数据库的唯一主键,因此这里来看Zookeeper的分布式全局唯一ID分配机制。

单库时对于ID可以通过设置自增来实现,然后再分库分表的场景下,单库自增已经不能保证唯一性了。此时可以采用UUID来生成唯一的ID,一个UUID是由32位字符和3个短线构成的字符串。UUID存在其优势,但是也存在缺点:一是长度过长,相较于INT需要更多的空间,且字符串类型在检索性能上也比不上INT。二是含有不明,UUID仅仅是一串无意义的字符串,无法表达含义。

Zookeeper中的节点创建API可以创建一个顺序节点,并在API返回值中返回这个节点的完整名字,利用这个特性,就可以借助Zookeeper来生成全局唯一的ID了。通过create()创建一个顺序节点,如创建 test-节点,节点创建完毕后,create接口会返回一个完整的节点名,如test-0000000001,你可以在此基础上拼接类型,组成type-test-0000000001来获得一个全局唯一的ID。

分布式锁

排他锁:使用create()接口在指定路径创建一个临时节点,Zookeeper会保证只有一个客户端创建成功,创建成功的客户端就成功拿到了锁,流程结束后删除改节点即完成了锁释放。而创建失败的,如果需要继续等待锁的释放,则可以对这个节点注册一个Watcher进行监控,当节点发生变化时重新尝试获取锁。为什么创建临时节点呢?因为临时节点在客户端与服务器之间的会话失效后会被自动清除,避免宕机导致的无限等待问题。
共享锁:共享锁要求读读可以共享,读写互斥。针对这种情况,客户端需要加锁时可以建立一个类似于
“/shared_lock/[hostname]- 锁类型 - 序号”的临时顺序节点,如过是读请求就类似于/shared_lock/101.101.0.1-R-00001,如果是写请求就是/shared_lock/101.101.0.1-W-00001。此时有以下步骤:

    创建完节点后,对该节点注册Watcher确定自己在子节点中的顺序,如果自己是读请求,且比自己小的请求不包含写请求,那么读锁加成功,如果是写请求,且自己是最小的节点,那么写锁加成功。释放锁之后,通知其他客户端重走步骤2.

但是这个步骤有一个问题: 如果持有锁的客户端释放锁后,会通知所有后续的节点,最糟糕的情况,如果后续第一个节点是一个写节点,那么其他的节点通知就完全没有用的。这样的节点越多,情况也就越糟糕。如果集群规模较小,如10台机器以下,问题不是特别大,但是如果是一个庞大的集群,且短时间内有多个节点进行锁释放,就会瞬间产生大量的事件通知-这就是所谓的羊群效应。此时可以做以下改进:

    create节点完成节点创建,但是不对主节点注册WatchergetChildren()接口获取已创建的子节点如果无法获取锁,那么对比自己小的子节点注册watcher。这里读 *** 作和写 *** 作不同,读 *** 作watcher比自己小的写节点,写 *** 作watcher比自己小的任意节点。等待消息通知,进入步骤2

对于以上两种方式,并不是说第二个就一定优,要根据业务场景来看,在小集群中,第一种反而更简单实用。

Master选举

可以通过同时向数据库插入一条主键相同的数据,插入成功的即为Master,但是此种如果应用挂掉,数据库无法通知集群重新选举,因此可以使用Master的临时节点,整个 *** 作和排他锁的步骤一样,拿到锁的即为Master。

分布式队列

FIFO先进先出: 类似于共享锁的逻辑,建立一个临时的顺序节点进行消费即可。
Barrier屏障:在主节点上维护一个屏障数量属性,并对主节点注册通知,发生变化时获取子节点数目,到达屏障数量的话消费,否则的话重新注册watcher

分布式协调通知

任务注册:通过类似于排他锁的 *** 作为数据库建立一个备份任务。
任务热备份:每台任务机器都注册一个临时的顺序节点,并watcher主节点,当节点发生变化时,去看自己是不是最小的节点,如果是就接替备份任务进入Running状态,如果不是,将自己设置成STANDBY状态继续等待。
热备切换:当主备份机器宕机时,临时顺序节点的后续节点就能及时接替上去继续备份,同时也要求运行中的备份接单将备份进度定时写入一个数据节点。
冷备:不对每个机器都维护备份,而是将机器分组,然后一台机器一直轮询列表,如果这个任务已经有Running 的线程,就遍历下一个,如果没有Runing的线程,就建立一个临时节点,然后看自己是不是最小的(避免竞争),如果是,那么自己就进入备份逻辑,如果不是说明其他机器已占用,就删除自己建立的节点,然后继续轮询。冷备损失了实时性,但是节省了机器资源。
心跳监测:除了ping 和建立长链接的方式,通过zk的临时节点断开自动清除的特性也能做到心跳检测
工作进度汇报:通过watcher来进行进度的实时汇报

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/zaji/5701594.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-17
下一篇 2022-12-17

发表评论

登录后才能评论

评论列表(0条)

保存