Zookeeper--简介

在这里插入图片描述

前言–分布式协调技术

    其实分布式协调技术主要是用来解决分布式环境下的多个进程(程序)之间的同步控制,让他们有序的访问某种临界资源,防止造成“脏数据”。对于分布式系统而言,它并不像我们平时的进程(程序)都是在一台计算机上运行的,那样确实好办,写调度算法就可以解决了,问题就在于它是一个分布式的环境下的,这里涉及到“部分丢失”的概念,
    分析分布式系统中如何进行进程的调度,假设一个机器上挂载了资源,有多个物理分布的进程都要竞争这个资源,但我们有不希望他们是同时进行访问的,因此,就需要一个协调器来解决,这个协调器就是我们在java中经常用到的“锁”,因为是在分布式系统环境下,也称为分布式锁。当某个进程获得锁后,就保持了对该资源的独占,其他的进程就无法访问此资源,而处于等待的状态,直到此进程释放锁,让其他进程获取。分布式锁也是分布式协调技术要实现的核心内容。下面进行详细的讲解。

分布式锁的实现

    我们可能的错觉就是,分布式环境下进行分布式锁,无非就是将原来在同一台机器上对进程的调度的原语,通过网络环境实现在分布式环境中。问题就在于分布式环境中,所有在同一台机器上的假设都不存在,因为:网络是不可靠的。

    比如:在同一台机器上,你对一个服务的调用如果成功,那就是成功,如果调用失败,比如抛出异常那就是调用失败。但是在分布式环境中,由于网络的不可靠,你对一个服务的调用失败了并不表示一定是失败的,可能是执行成功了,但是响应返回的时候失败了。还有,A和B都去调用C服务,在时间上 A还先调用一些,B后调用,那么最后的结果是不是一定A的请求就先于B到达呢? 这些在同一台机器上的种种假设,我们都要重新思考,我们还要思考这些问题给我们的设计和编码带来了哪些影响。还有,在分布式环境中为了提升可靠性,我们往往会部署多套服务,但是如何在多套服务中达到一致性,这在同一台机器上多个进程之间的同步相对来说比较容易办到,但在分布式环境中确实一个大难题。

    所以分布式协调远比在同一台机器上对多个进程的调度要难得多,而且如果为每一个分布式应用都开发一个独立的协调程序。一方面,协调程序的反复编写浪费,且难以形成通用、伸缩性好的协调器。另一方面,协调程序开销比较大,会影响系统原有的性能。所以,急需一种高可靠、高可用的通用协调机制来用以协调分布式应用。

    当下,在分布式协调技术方面做得比较好的就是Google的Chubby和Apache的Zookeeper了,他们作为分布式锁的实现者。

    因为Chubby是作为Google的自身项目,用于商业环境,而Zookeeper则是由雅虎模仿Google的Chubby开发出来的开源的项目。而且在分布式领域内,Zookeeper久经考验,它的可靠性,高可用性都是经过理论和时间的验证的,所以我们在构建一些分布式系统的时候,可以以这些系统为起点构建我们的系统,这样会节省不少成本,而且bug也会更少。

Zookeeper概述

    ZooKeeper是一种为分布式应用所设计的高可用、高性能且一致的开源协调服务,它提供了一项基本服务:分布式锁服务。由于ZooKeeper的开源特性,后来我们的开发者在分布式锁的基础上,摸索了出了其他的使用方法:配置维护、组服务、分布式消息队列、分布式通知/协调等。
    Zookeeper主要是针对大型分布式系统进行高可靠的协调。由这个定义我们知道zookeeper是个协调系统,作用的对象是分布式系统。说到协调,
    以网上常用的一个例子:在实际应用中,我们可以联想到的现实生活中很多十字路口的交通协管,他们手握着小红旗,指挥车辆和行人是不是可以通行。如果我们把车辆和行人比喻成运行在计算机中的单元(线程),那么这个协管是干什么的?很多人都会想到,这不就是锁么?对,在一个并发的环境里,我们为了避免多个运行单元对共享数据同时进行修改,造成数据损坏的情况出现,我们就必须依赖像锁这样的协调机制,让有的线程可以先操作这些资源,然后其他线程等待。

ZooKeeper特性

读、写(更新)模式

    在ZooKeeper集群中,读可以从任意一个ZooKeeper Server读,这一点是保证ZooKeeper比较好的读性能的关键;写的请求会先Forwarder到Leader,然后由Leader来通过ZooKeeper中的原子广播协议(ZAB,Zookeeper Atomic Broadcast),将请求广播给所有的Follower,Leader收到一半以上的写成功的Ack后,就认为该写成功了,就会将该写进行持久化,并告诉客户端写成功了。

在这里插入图片描述

WAL和Snapshot

    和大多数分布式系统一样,ZooKeeper也有WAL(Write-Ahead-Log),对于每一个更新操作,ZooKeeper都会先写WAL, 然后再对内存中的数据做更新,然后向Client通知更新结果。另外,ZooKeeper还会定期将内存中的目录树进行Snapshot,落地到磁盘上,这个跟HDFS中的FSImage是比较类似的。这么做的主要目的,一当然是数据的持久化,二是加快重启之后的恢复速度,如果全部通过Replay WAL的形式恢复的话,会比较慢。
FIFO
    对于每一个ZooKeeper客户端而言,所有的操作都是遵循FIFO顺序的,这一特性是由下面两个基本特性来保证的:一是ZooKeeper Client与Server之间的网络通信是基于TCP,TCP保证了Client/Server之间传输包的顺序;二是ZooKeeper Server执行客户端请求也是严格按照FIFO顺序的。

注意:
ZooKeeper性能上的特点决定了它能够用在大型的、分布式的系统当中。从可靠性方面来说,它并不会因为一个节点的错误而崩溃(因为它在一定程度上就是为了解决单点故障的问题,不能只是将问题进行转移)。除此之外,它严格的序列访问控制意味着复杂的控制原语可以应用在客户端上。ZooKeeper在一致性、可用性、容错性的保证,也是ZooKeeper的成功之处,它获得的一切成功都与它采用的协议——Zab协议是密不可分的,这些内容将会在后面介绍。

运行模式

Zookeeper服务有两种不同的运行模式:

  • 独立模式(standalone mode):即只有一个Zookeeper服务器,这种一般适合于测试环境,但不能保证高可用和恢复性
  • 复制模式(replication mode):是在生产环境下使用,运行与一个计算机集群上,这个计算机集群称为一个“集合体"(ensemble)

在这里插入图片描述
    ZooKeeper通过复制来实现高可用性,只要集合体中半数以上的机器处于可用状态,它就能够提供服务。例如,在一个有5个节点的集合体中,每个Follower节点的数据都是Leader节点数据的副本,也就是说我们的每个节点的数据视图都是一样的,这样就可以有五个节点提供ZooKeeper服务。并且集合体中任意2台机器出现故障,都可以保证服务继续,因为剩下的3台机器超过了半数。
    从概念上来讲,Zookeeper所做的就是确保对Znode的每个修改都会被复制到集合体中超过半数的机器上,如果少于半数的机器出现故障,则最少有一台机器会保存最新的状态,那么这台机器就是Leader,其余复本最终也会更新到这个状态,而如果Leader挂了,由于其他机器保存了Leader的复本,那就从中选出一台机器作为新的Leader继续提供服务。

Zookeeper数据模型

系统结构

Zookeeper会维护一个具有层次关系的数据结构,它又非常类似于linux下的文件系统。如下图:
在这里插入图片描述
他们都采用的是含有节点的目录树结构,在Zookeeper中的每个节点称为:Znode。这里有了自身的定义,那么它便和文件系统的目录树有一些不一样的地方:

  1. 引用方式
    Znode通过路径引用,且路径必须是绝对路径,因此每个路径必须由“/”开头;并且,他们是唯一的,每个路径只有一个表示,Zookeeper中的路径是由Unicode字符串组成的,其中“/zookeeper”用来保存管理信息,作为系统的配置。
  2. Znode结构
    Zookeeper命名空间中的Znode,同时具有文件和目录两种特点,即:既能像文件一样维护数据,元信息,ACL,时间戳等数据结构,又能像目录一样作为路径表示的一部分。上图中的每个节点有3个部分组成:
    • stat:状态信息,描述该Znode的版本,权限等信息
    • data:数据信息,即该Znode保存的数据
    • children:该Znode下的子节点信息

注意:
虽然Zookeeper可以关联一些数据,但并没有被设计作为常规的数据库或大数据存储,而是作为管理调度数据的服务,比如分布式应用中的配置文件信息,状态信息,汇集位置等。这些数据的共同特性就是他们都是很小的数据,通常都是以KB为单位。
Zookeeper的服务器和客户端都会严格检查并限制每个Znode的数据大小最大为1M,但常规的文件都应该远小于此值。

状态信息

主要记录的是节点状态改变的时间戳,所以很多都是记录时间的形式在作为状态信息的。

Zxid

使节点状态改变的每个操作都会让节点收到一个Zxid格式的时间戳,并且这个时间戳全局有序,也就是每个对节点的改变都将产生唯一的Zxid(这也与锁的机制相关)。如果Zxid1的值小于Zxid2的值,那么Zxid1所对应的事件发生在Zxid所对应的时间之前。而Zookeeper在每个节点会维护三个Zxid值:

  • cZxid:节点的创建时间所对应的Zxid格式时间戳
  • mZxid:节点的修改时间所对应的Zxid格式时间戳
  • pZxid:节点最新修改的时间所对应的Zxid格式的时间戳
版本号(version)

对于节点来说,每个操作都会使这个节点的版本号增加,每个节点维护三个版本号:

  • version:节点数据版本号,也称为 dataversion
  • cverison:子节点版本号
  • aversion:节点拥有的ACL版本号
数据访问

Zookeeper中的每个接单存储的数据要被原子性操作,也就是读操作将获取与节点相关的所有数据,写操作也将替换掉节点的所有数据,并且,每个节点都拥有自己的ACL(访问控制列表),这个列表中规定了用户的权限。

  1. 权限管理
    每个节点都可以管理自身的ACL列表,控制自身的访问权限,需要注意的是,exists和getAcl操作是不受权限的限制的,每个客户端都是可以调用这两个方法的。
  • ACL包括:
    • 权限:perms
    • 验证模式:schema
    • 具体内容:Ids
  • 验证模式包括:
    • Digest:客户端需要通过用户名和密码验证,如user:pwd形式
    • Host:有客户端主机名验证
    • Ip:有ip地址验证
    • World:固定用户为所有人,anyone
  • 管理的权限包括:
    - admin(setacl()),delete,create,write,read

那么设置的一般就是验证的方式schema,然后就是可以操作的权限,验证的方式一般选用digest方式是最常用和方便的,对于操作权限的设置,则是可以通过int型数字和二进制表示,这个有点像文件系统的的rwx,只是这里是adcwr,

  • 使用digest方式设置密码:
    这也是最常用的方式,通过用户名和密码的方式来管理权限,设置权限的格式为:
setAcl path digest:user:pwd:crwda
setAcl 路径节点 验证模式:用户名:密码:权限

下面是一个例子:
1.使用加密机制生成base64位密码,因为不能用明文密码,可以直接百度base64在线编码解码,或者使用如下方式:

java -cp ./zookeeper-3.4.6.jar:./lib/log4j-1.2.16.jar:./lib/slf4j-log4j12-1.6.1.jar:./lib/slf4j-api-1.6.1.jar:zookeeper-3.4.9.jar org.apache.zookeeper.server.auth.DigestAuthenticationProvider test:test
// 生成的"test"的密码为:test:test->test:V28q/NynI4JI3Rk54h0r8O5kMug=

在这里插入图片描述在这里插入图片描述
在这里插入图片描述

当通过addauth输入对应的验证之后,对于一次client对znode进行操作验证ACL的方式为:

  • 对每个ACL,首先操作类型与权限匹配(意思是开始是不会直接匹配用户名和密码的,而是当执行ACL中限制的操作时才会进行下一步匹配)
  • 只有匹配权限成功才进行session的auth信息与ACL的用户名和密码匹配
  • 如果两次都匹配成功,则允许操作,否则返回error权限不够

超级管理员superdigest
    这是对于忘记密码的拯救措施,修改zkServer.sh,加入super权限设置

zkServer -Dzookeeper.DigestAuthenticationProvider.superDigest=super:gG7s8t3oDEtIqF6DM9LlI/R+9Ss=

然后重启服务,然后再客户端使用命令addauth digest super:super

如果znode ACL List中任何一个ACL都没有setAcl权限,那么就算superDigest也修改不了它的权限;再假如这个znode还不开放delete权限,那么它的所有子节点都将不会被删除。唯一的办法是通过手动删除snapshot和log的方法,将ZK回滚到一个以前的状态,然后重启,当然这会影响到该znode以外其它节点的正常应用。

缺陷:
ACL毕竟仅仅是访问控制,并非完善的权限管理,通过这种方式做多集群隔离,还有很多局限性:

  1. ACL并无递归机制,任何一个znode创建后,都需要单独设置ACL,无法继承父节点的ACL设置。
  2. 除了ip这种scheme,digest和auth的使用对用户都不是透明的,这也给使用带来了很大的成本,很多依赖zookeeper的开源框架也没有加入对ACL的支持,例如hbase,storm。
节点类型

Zookeeper中又把节点分为两种类型:临时节点永久节点。节点的类型在创建节点的时候就必须被确定,并且不能改变。

  • 临时节点(EPHEMERAL ):其生命周期依赖于创建他们的会话(Session),一旦会话结束,临时节点将会被自动删除,当然也可以手动删除,虽然每个临时节点的Znode都会被绑定到一个客户端会话,但他们对所有的客户端还是可见的。另外,临时节点不允许创建子节点。
  • 永久节点(PERSISTENT):该节点的生命周期不依赖于会话,并且只有在客户端显式的执行删除操作时,它们才会被删除。

关于会话(session):
Client与Zookeeper服务端之间的通信,需要创建一个Session,这个Session会有一个超时时间,因为服务端会把Client的session信息持久化,所以在session没超时之前,它们之间的连接在各个服务端之间可以透明的移动。
在客户端和服务端之间通过发送心跳来进行连接,每个一定时间就会发送一个心跳,通过心跳来传递信息,也通过心跳来判断客户端是否断开连接,这个超时时间是配置信息可以设置的。
在这里插入图片描述

顺序节点

    当创建Znode节点时,客户端可以请求Zookeeper的路径结尾添加一个递增的计数,这个计数对于此节点的父节点来说是唯一的,它的格式为“%10d”(10位数字,没有用0补充)

观察者

    客户端可以在节点上设置watch,也称为监视器,当节点状态发生改变时(Znode的增删改)将会触发watch所对应的操作。(关于具体的触发事件,参见后面的 Watch触发器
    当watch被触发时,Zookeeper会向客户端发送且仅发送一条通知,因为watch只能被触发一次,这样可以减少网络流量。

Zookeeper节点的属性

在这里插入图片描述
在这里插入图片描述

Zookeeper服务中的操作

基本操作有9个:
在这里插入图片描述
说明:

  • 更新ZooKeeper操作是有限制的。delete或setData必须明确要更新的Znode的版本号,我们可以调用exists找到。如果版本号不匹配,更新将会失败。
  • 更新ZooKeeper操作是非阻塞式的。因此客户端如果失去了一个更新(由于另一个进程在同时更新这个Znode),他可以在不阻塞其他进程执行的情况下,选择重新尝试或进行其他操作。
  • 尽管ZooKeeper可以被看做是一个文件系统,但是处于便利,摒弃了一些文件系统地操作原语。因为文件非常的小并且使整体读写的,所以不需要打开、关闭或是寻地的操作。

Watch触发器

(相关示例参见Zookeeper–客户端操作

    Zookeeper可以为所有的读操作设置watch,这些读操作包括:exists(),getData()和getChildren()。
    watch事件是一次性的触发器,当watch的对象状态发生改变时,将会触发此对象上的watch所对应的事件,watch事件将被异步发送给客户端,并且Zookeeper为watch机制提供了有序的一致性保证。理论上,客户端接收watch事件的时间快于看到watch对象状态变化的时间。

watch类型
  • data watch:getData()和exists()负责设置data watch
  • child watch:getChildren()负责设置child watch

我们可以通过操作返回的数据来设置不同的watch:

  1. getData()和exists():返回节点的数据信息
  2. getChildren():返回子节点列表信息

于是:

  • 一个成功的setData操作将触发Znode的data watch
  • 一个成功的create节点操作将触发Znode的data watch和child watch
  • 一个成功的delete节点操作将触发Znode的data watch和child watch

    可能这样理解会有些难以理解,这样解释就会明白了,触发器的设置是在上述的两种类型的三种方法中,但是并不是调用这些方法就会触发watch,不是这样的,而是调用这些方法返回的数据信息在前后时间段里不一样,就会触发,
    也就是说,数据信息发生了修改时才会触发,拿getData()为例,当创建一个节点(create),更新数据(setData)以及删除节点(delete)时,数据信息发生变化,而因此,是在调用这些操作的时候才会触发watch操作。

注册与触发

在这里插入图片描述

  • exists操作上的watch:在被监视的Znode创建,删除或更新数据时被触发
  • getData()操作上的watch:在被监视的Znode删除或更新数据时被触发,在被创建时不能被触发,因为只有Znode一定存在时,getData操作才会成功。
  • getChildren()操作上的watch:在被监视的Znode的子节点创建或删除,或是这个Znode节点本身被删除时被触发。NodeDelete表示Znode被删除,NodeDeletedChange表示子节点被删除,可以通过查看watch的事件类型来查看上述两个删除。

注意:
Zookeeper的watch实际上要处理两类事件(都是通过重载process(WatchEvent event)方法实现):
1.连接状态时间(type=None && path = null)
这类时间不需要注册,也不需要我们连续触发,只需要处理就行
2.节点事件
节点的建立,删除,数据的修改,它是one time trigger,我们需要不停的注册触发,还可能发生事件丢失的情况
节点时间的触发,通过方法exists,getData或getChildren()来处理这类方法时,有双重作用:
1.注册触发事件
2.方法本身的功能,即可以用异步的回调方法来实现,通过重载processResult()来处理方法本身。

在这里插入图片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章