Zookeeper基础知识和Linux环境下搭建

最近要实现一个基于zookeeper的分布式锁,但是一些知识一无所知,找了一些,汇总在这里,下边的大部分来自这个博客搭建地址,亲测好使

一、ZooKeeper的背景

1.1 认识ZooKeeper

ZooKeeper---译名为“动物园管理员”。动物园里当然有好多的动物,游客可以根据动物园提供的向导图到不同的场馆观赏各种类型的动物,而不是像走在原始丛林里,心惊胆颤的被动 物所观赏。为了让各种不同的动物呆在它们应该呆的地方,而不是相互串门,或是相互厮杀,就需要动物园管理员按照动物的各种习性加以分类和管理,这样我们才能更加放心安全的观赏动物。

回到企业级应用系统中,随着信息化水平的不断提高,企业级系统变得越来越庞大臃肿,性能急剧下降,客户抱怨频频。拆分系统是目前我们可选择的解决系统可伸缩性和性能问题的唯一行之有效的方法。但是拆分系统同时也带来了系统的复杂性——各子系统不是孤立存在的,它们彼此之间需要协作和交互,这就是我们常说的分布式系统0。各个子系统就好比动物园里的动物,为了使各个子系统能正常为用户提供统一的服务,必须需要一种机制来进行协调——这就是ZooKeeper(动物园管理员)。

1.2 为什么使用ZooKeeper

我们知道要写一个分布式应用是非常困难的,主要原因就是局部故障。一个消息通过网络在两个节点之间传递时,网络如果发生故障,发送方并不知道接收方是否接收到了这个消息。他可能在网络故障迁就收到了此消息,也坑没有收到,又或者可能接收方的进程死了。发送方了解情况的唯一方法就是再次连接发送方,并向他进行询问。这就是局部故障:根本不知道操作是否失败。因此,大部分分布式应用需要一个主控、协调控制器来管理物理分布的子进程。目前,大部分应用需要开发私有的协调程序,缺乏一个通用的机制。协调程序的反复编写浪费,且难以形成通用、伸缩性好的协调器。协调服务非常容易出错,并很难从故障中恢复。例如:协调服务很容易处于竞态1甚至死锁2。Zookeeper的设计目的,是为了减轻分布式应用程序所承担的协调任务。

Zookeeper并不能阻止局部故障的发生,因为它们的本质是分布式系统。他当然也不会隐藏局部故障。ZooKeeper的目的就是提供一些工具集,用来建立安全处理局部故障的分布式应用。

ZooKeeper是一个分布式小文件系统,并且被设计为高可用性。通过选举算法和集群复制可以避免单点故障3,由于是文件系统,所以即使所有的ZooKeeper节点全部挂掉,数据也不会丢失,重启服务器之后,数据即可恢复。另外ZooKeeper的节点更新是原子的,也就是说更新不是成功就是失败。通过版本号,ZooKeeper实现了更新的乐观锁4,当版本号不相符时,则表示待更新的节点已经被其他客户端提前更新了,而当前的整个更新操作将全部失败。当然所有的一切ZooKeeper已经为开发者提供了保障,我们需要做的只是调用API。与此同时,随着分布式应用的的不断深入,需要对集群管理逐步透明化监控集群和作业状态,可以充分利ZK的独有特性。

1.3 ZooKeeper的应用 

ZooKeeper本质上是一个分布式的小文件存储系统。原本是Apache Hadoop的一个组件,现在被拆分为一个Hadoop的独立子项目,在Hbase(Hadoop的另外一个被拆分出来的子项目,用于分布式环境下的超大数据量的DBMS)中也用到了ZooKeeper集群。 

Hadoop,使用Zookeeper的事件处理确保整个集群只有一个NameNode,存储配置信息等.
  HBase,使用Zookeeper的事件处理确保整个集群只有一个HMaster,察觉HRegionServer联机和宕(dàng)机,存储访问控制列表等。

有人会怀疑ZooKeeper的执行能力,在ZooKeeper诞生的地方——Yahoo!他被用作雅虎消息代理的协调和故障恢复服务。雅虎消息代理是一个高度可扩展的发布-订阅系统,他管理着成千上万台联及程序和信息控制系统。它的吞吐量标准已经达到大约每秒10000基于写操作的工作量。对于读操作的工作量来说,它的吞吐量标准还要高几倍。

二、ZooKeeper的介绍

2.1 ZooKeeper的概述

Zookeeper 是为分布式应用程序提供高性能协调服务的工具集合,也是Google的Chubby一个开源的实现,是Hadoop 的分布式协调服务。它包含一个简单的原语集5,分布式应用程序可以基于它实现配置维护、命名服务、分布式同步、组服务等。Zookeeper可以用来保证数据在ZK集群之间的数据的事务性一致6。其中ZooKeeper提供通用的分布式锁服务7,用以协调分布式应用。

Zookeeper作为Hadoop项目中的一个子项目,是 Hadoop集群管理的一个必不可少的模块,它主要用来解决分布式应用中经常遇到的数据管理问题,如集群管理、统一命名服务、分布式配置管理、分布式消息队列、分布式锁、分布式协调等。在Hadoop中,它管理Hadoop集群中的NameNode,还有在Hbase中Master Election、Server 之间状态同状步等。
  Zoopkeeper 提供了一套很好的分布式集群管理的机制,就是它这种基于层次型的目录树的数据结构,并对树中的节点进行有效管理,从而可以设计出多种多样的分布式的数据管理模型。

2.2 ZooKeeper的设计目标

众所周知,分布式环境下的程序和活动为了达到协调一致目的,通常具有某些共同的特点,例如,简单性、有序性等。ZooKeeper不但在这些目标的实现上有自身特点,并且具有独特优势。下面我们将简述ZooKeeper的设计目标。

(1)简单化

ZooKeeper允许各分布式进程通过一个共享的命名空间相互联系,该命名空间类似于一个标准的层次型的文件系统:由若干注册了的数据节点构成(用Zookeeper的术语叫znode),这些节点类似于文件和目录。典型的文件系统是基于存储设备的,文传统的文件系统主要用于存储功能,然而ZooKepper的数据是保存在内存中的。也就是说,可以获得高吞吐和低延迟。ZooKeeper的实现非常重视高性能、高可靠,以及严格的有序访问。

高性能保证了ZooKeeper可以用于大型的分布式系统,高可靠保证了ZooKeeper不会发生单点故障,严格的顺序访问保证了客户端可以获得复杂的同步操作原语。

(2)健壮性

就像ZooKeeper需要协调的分布式系统一样,它本身就是具有冗余结构,它构建在一系列主机之上,叫做一个”ensemble”。
  构成ZooKeeper服务的各服务器之间必须相互知道,它们维护着一个状态信息的内存映像8,以及在持久化存储中维护着事务日志和快照9。只要大部分服务器正常工作,ZooKeeper服务就能正常工作。
  客户端连接到一台ZooKeeper服务器。客户端维护这个TCP连接,通过这个连接,客户端可以发送请求、得到应答,得到监视事件以及发送心跳。如果这个连接断了,客户端可以连接到另一个ZooKeeper服务器。

(3)有序性

ZooKeeper给每次更新附加一个数字标签,表明ZooKeeper中的事务顺序,后续操作可以利用这个顺序来完成更高层次的抽象功能,例如同步原语7。

(4)速度优势

ZooKeeper特别适合于以读为主要负荷的场合。ZooKeeper可以运行在数千台机器上,如果大部分操作为读,例如读写比例为10:1,ZooKeeper的效率会很高。

2.3 ZooKeeper的集群

ZK集群如下图2.1所示。这是实际应用的一个场景,该ZooKeeper集群当中一共有5台服务器,有两种角色Leader和Follwer,5台服务器连通在一起,客户端有分别连在不同的ZK服务器上。如果当数据通过客户端1,在左边第一台Follower服务器上做了一次数据变更,他会把这个数据的变化同步到其他所有的服务器,同步结束之后,那么其他的客户端都会获得这个数据的变化。

图 2.1

注意:

通常Zookeeper由2n+1台servers组成,每个server都知道彼此的存在。每个server都维护的内存状态镜像以及持久化存储的事务日志和快照。为了保证Leader选举能过得到多数的支持,所以ZooKeeper集群的数量一般为奇数。对于2n+1台server,只要有n+1台(大多数)server可用,整个系统保持可用。

2.3.1 集群中的角色

在ZooKeeper集群当中,集群中的服务器角色有两种Leader和Learner,Learner角色又分为Observer和Follower,具体功能如下:

1.领导者(leader),负责进行投票的发起和决议,更新系统状态

2.学习者(learner),包括跟随者(follower)和观察者(observer),

3.follower用于接受客户端请求并向客户端返回结果,在选主过程中参与投票

4.Observer可以接受客户端请求,将写请求转发给leader,但observer不参加投票过程,只同步leader的状态,observer的目的是为了扩展系统,提高读取速度。

5. 客户端(client),请求发起方

ZooKeeper的组件图中给出了ZooKeeper服务的高层次的组件。除了请求处理器(requestprocessor)外,构成ZooKeeper服务的每个服务器都有一个备份。复制的数据库(replicateddatabase)是一个内存数据库,包含整个数据树。为了可恢复,更新会被log到磁盘,并且在更新这个内存数据库之前,先序列化到磁盘。

每个ZooKeeper都为客户端提供服务。客户端只连接到一个服务器,并提交请求。读请求直接由本地的复制数据库提供数据。对服务状态进行修改的请求、写请求通过一个约定的协议进行通讯。

作为这个协议的一部分,所有的写请求都被传送到一个叫“首领(leader)”的服务器,而其他的服务器,叫做“(随从)followers”,follower从leader接收信息修改的提议,并同意进行。当leader发生故障时,协议的信息层(messaginglayer)关注leader的替换,并同步到所有的follower。

ZooKeeper采用一个自定义的信息原子操作协议,由于信息层的操作是原子性的,ZooKeeper能保证本地的复制数据库不会产生不一致。当leader接收到一个写请求,它计算出写之后系统的状态,把它变成一个事务。

2.3.2 Zookeeper的读写机制和保证及特点

(1)ZooKeeper的读写机制

Zookeeper是一个由多个server组成的集群

一个leader,多个follower

每个server保存一份数据副本

全局数据一致

分布式读写

更新请求转发,由leader实施

(2)ZooKeeper的保证

ZooKeeper运行非常快而且简单。虽然它的目标是构建更加复杂服务(例如同步)的基础,但它提供了一些保证,如下:

1.顺序一致性:来自于客户端的更新,根据发送的先后被顺序实施。

2.唯一的系统映像:尽管客户端连接到不同的服务器,但它们看到的一个唯一(一致性)的系统服务,client无论连接到哪个server,数据视图都是一致的。

3.可靠性:一旦实施了一个更新,就会一直保持那种状态,直到客户端再次更新它,同时数据更新原子性,一次数据更新要么成功,要么失败。

4.及时性:在一个确定的时间内,客户端看到的系统状态是最新的。

(3)ZooKeeper特点

最终一致性:client不论连接到哪个Server,展示给它都是同一个视图,这是zookeeper最重要的性能。

可靠性:具有简单、健壮、良好的性能,如果消息m被一台服务器接受,那么它将被所有的服务器接受。

实时性:Zookeeper保证客户端将在一个时间间隔范围内获得服务器的更新信息,或者服务器失效的信息。 但由于网络延时等原因,Zookeeper不能保证两个客户端能同时得到刚更新的数据,如果需要最新数据,应该在读数据之前调用sync()接口。

等待无关(wait-free):慢的或者失效的client,不得干预快速的client的请求,使得每个client都能有效的等待。

原子性:更新只能成功或者失败,没有中间状态。

顺序性:包括全局有序和偏序两种:

全局有序:是指如果在一台服务器上消息a在消息b前发布,则在所有Server上消息a都将在消息b前被发布;

偏序:是指如果一个消息b在消息a后被同一个发送者发布,a必将排在b前面

三、ZooKeeper服务

3.1 ZooKeeper数据模型

ZooKeeper拥有一个层次的命名空间,这个和分布式的文件系统非常相似。不同的是ZooKeeper命名空间中的Znode,兼具文件和目录两种特点。既像文件一样维护着数据、元信息、ACL、时间戳等数据结构,又像目录一样可以作为路径标识的一部分,并可以具有子znode。用户对znode具有增、删、改、查等操作(权限允许的情况下)。

znode具有原子性操作,每个znode的数据将被原子性地读写,读操作会读取与znode相关的所有数据,写操作会一次性替换所有数据。zookeeper并没有被设计为常规的数据库或者大数据存储,相反的是,它用来管理调度数据,比如分布式应用中的配置文件信息、状态信息、汇集位置等等。这些数据的共同特性就是它们都是很小的数据,通常以KB为大小单位。zooKeeper的服务器和客户端都被设计为严格检查并限制每个znode的数据大小至多1M,当时常规使用中应该远小于此值。

Zonde由路径标注,ZooKeeper中被表示成有反斜杠分割的Unicode字符串,如同Unix中的文件路径。路径必须是绝对的,因此他们必须由反斜杠来字符开头。除此以外,他们必须是唯一的,也就是说每一个路径只有一个表示,因此这些路径不能改变。ZooKeeper的数据结构, 与普通的文件系统极为类似. 见下图:

图中的每个节点称为一个znode. 每个znode由3部分组成:

1.stat:此为状态信息, 描述该znode的版本, 权限等信息.

2.data:与该znode关联的数据.

3.children:该znode下的子节点.

3.1.1 ZooKeeper节点Znode

ZooKeeper目录树中每一个节点对应一个Znode。每个Znode维护着一个属性结构,它包含着版本号(dataVersion),时间戳(ctime,mtime)等状态信息。ZooKeeper正是使用节点的这些特性来实现它的某些特定功能。每当Znode的数据改变时,他相应的版本号将会增加。每当客户端检索数据时,它将同时检索数据的版本号。并且如果一个客户端执行了某个节点的更新或删除操作,他也必须提供要被操作的数据版本号。如果所提供的数据版本号与实际不匹配,那么这个操作将会失败。

Znode是客户端访问ZooKeeper的主要实体,它包含以下几个特征:

(1)Watches

客户端可以在节点上设置watch(我们称之为监视器)。当节点状态发生改变时(数据的增、删、改)将会触发watch所对应的操作。当watch被触发时,ZooKeeper将会向客户端发送且仅发送一条通知,因为watch只能被触发一次。

(2)数据访问

ZooKeeper中的每个节点存储的数据要被原子性的操作。也就是说读操作将获取与节点相关的所有数据,写操作也将替换掉节点的所有数据。另外,每一个节点都拥有自己的ACL(访问控制列表),这个列表规定了用户的权限,即限定了特定用户对目标节点可以执行的操作。

(3)节点类型

ZooKeeper中的节点有两种,分别为临时节点和永久节点。节点的类型在创建时即被确定,并且不能改变。
  ZooKeeper的临时节点:该节点的生命周期依赖于创建它们的会话。一旦会话结束,临时节点将被自动删除,当然可以也可以手动删除。另外,需要注意是,ZooKeeper的临时节点不允许拥有子节点。
  ZooKeeper的永久节点:该节点的生命周期不依赖于会话,并且只有在客户端显示执行删除操作的时候,他们才能被删除。

(4)顺序节点(唯一性的保证)

当创建Znode的时候,用户可以请求在ZooKeeper的路径结尾添加一个递增的计数。这个计数对于此节点的父节点来说是唯一的,它的格式为“%10d”(10位数字,没有数值的数位用0补充,例如“0000000001”)。当计数值大于232-1时,计数器将溢出。

org.apache.zookeeper.CreateMode中定义了四种节点类型,分别对应:

PERSISTENT:永久节点

EPHEMERAL:临时节点

PERSISTENT_SEQUENTIAL:永久节点、序列化

EPHEMERAL_SEQUENTIAL:临时节点、序列化

3.1.2 ZooKeeper中的时间

ZooKeeper有多种记录时间的形式,其中包含以下几个主要属性:

(1)Zxid

致使ZooKeeper节点状态改变的每一个操作都将使节点接收到一个zxid格式的时间戳,并且这个时间戳全局有序。也就是说,也就是说,每个对节点的改变都将产生一个唯一的zxid。如果zxid1的值小于zxid2的值,那么zxid1所对应的事件发生在zxid2所对应的事件之前。实际上,ZooKeeper的每个节点维护者三个zxid值,为别为:cZxid、mZxid、pZxid。

cZxid: 是节点的创建时间所对应的Zxid格式时间戳。

mZxid:是节点的修改时间所对应的Zxid格式时间戳。

实现中zxid是一个64为的数字,它高32位是epoch用来标识leader关系是否改变,每次一个leader被选出来,它都会有一个 新的epoch。低32位是个递增计数。

(2)版本号

对节点的每一个操作都将致使这个节点的版本号增加。每个节点维护着三个版本号,他们分别为:

version 节点数据版本号

cversion 子节点版本号

aversion 节点所拥有的ACL版本号

3.1.3 节点的属性结构

通过前面的介绍,我们可以了解到,一个节点自身拥有表示其状态的许多重要属性,如下图所示。

3.1.4 Zonde总结

(1)znode中的数据可以有多个版本,在查询该znode数据时就需要带上版本信息。如:set path version / delete path version

(2)znode可以是临时znode,由create -e 生成的节点,一旦创建这个znode的client与server断开连接,该znode将被自动删除。

client和server之间通过heartbeat来确认连接正常,这种状态称之为session,断开连接后session失效。

(3)临时znode不能有子znode。

(4)znode可以自动编号,由create -s 生成的节点,例如在 create -s /app/node 已存在时,将会生成 /app/node00***001节点。

(5)znode可以被监控,该目录下某些信息的修改,例如节点数据、子节点变化等,可以主动通知监控注册的client。事实上,通过这个特性,可以完成许多重要应用,例如配置管理、信息同步、分布式锁等等。

3.2 ZooKeeper服务中的操作

在ZooKeeper中有9个基本操作,如下图所示:

更新ZooKeeper操作是有限制的。delete或setData必须明确要更新的Znode的版本号,我们可以调用exists找到。如果版本号不匹配,更新将会失败。

更新ZooKeeper操作是非阻塞式的。因此客户端如果失去了一个更新(由于另一个进程在同时更新这个Znode),他可以在不阻塞其他进程执行的情况下,选择重新尝试或进行其他操作。

尽管ZooKeeper可以被看做是一个文件系统,但是处于便利,摒弃了一些文件系统地操作原语。因为文件非常的小并且使整体读写的,所以不需要打开、关闭或是寻地的操作。

3.2.1 watch触发器

读操作exists、getChildren和getData都被设置了watch,并且这些watch都由写操作来触发:create、delete和setData。ACL操作并不参与到watch中。当watch被触发时,watch事件被生成,他的类型由watch和触发他的操作共同决定。ZooKeeper所管理的watch可以分为两类:

1.数据watch(data watches):getData和exists负责设置数据watch;

2.孩子watch(child watches):getChildren负责设置孩子watch;

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

1.getData和exists:返回关于节点的数据信息

2.getChildren:返回孩子列表

因此,一个成功的setData操作将触发Znode的数据watch。

一个成功的create操作将触发Znode的数据watch以及孩子watch。

一个成功的delete操作将触发Znode的数据watch以及孩子watch。

watch由客户端所连接的ZooKeeper服务器在本地维护,因此watch可以非常容易地设置、管理和分派。当客户端连接到一个新的服务器上时,任何的会话事件都将可能触发watch。另外,当从服务器断开连接的时候,watch将不会被接收。但是,当一个客户端重新建立连接的时候,任何先前注册过的watch都会被重新注册。

exists操作上的watch,在被监视的Znode创建、删除或数据更新时被触发。

getData操作上的watch,在被监视的Znode删除或数据更新时被触发。在被创建时不能被触发,因为只有Znode一定存在,getData操作才会成功。

getChildren操作上的watch,在被监视的Znode的子节点创建或删除,或是这个Znode自身被删除时被触发。可以通过查看watch事件类型来区分是Znode还是他的子节点被删除:NodeDelete表示Znode被删除,NodeDeletedChanged表示子节点被删除。

watch设置操作及相应的触发器如图下图所示:

watch事件包括了事件所涉及的Znode的路径,因此对于NodeCreated和NodeDeleted事件来说,根据路径就可以简单区分出是哪个Znode被创建或是被删除了。为了查询在NodeChildrenChanged事件后哪个子节点被改变了,需要再次调用getChildren来获得新的children列表。同样的,为了查询NodeDeletedChanged事件后产生的新数据,需要调用getData。在两种情况下,Znode可能在获取watch事件或执行读操作这两种状态下切换,在写应用程序时,必须记住这一点。

(1)Zookeeper的watch实际上要处理两类事件:

1. 连接状态事件(type=None, path=null)

这类事件不需要注册,也不需要我们连续触发,我们只要处理就行了。

2. 节点事件

节点的建立,删除,数据的修改。它是one time trigger,我们需要不停的注册触发,还可能发生事件丢失的情况。

上面2类事件都在Watch中处理,也就是重载的process(Event event)

(2)节点事件的触发,通过函数exists,getData或getChildren来处理

这类函数,有双重作用:

1. 注册触发事件

2. 函数本身的功能

函数的本身的功能又可以用异步的回调函数来实现,重载processResult()过程中处理函数本身的的功能。

函数还可以指定自己的watch,所以每个函数都有4个版本。根据自己的需要来选择不同的函数,不同的版本。

3.3 ZooKeeper访问控制列表ACL

ZooKeeper使用ACL来对Znode进行访问控制。ACL的实现和Unix文件访问许可非常相似:它使用许可位来对一个节点的不同操作进行允许或禁止的权限控制。但是,和标准的Unix许可不同的是,Zookeeper对于用户类别的区分,不止局限于所有者(owner)、组 (group)、所有人(world)三个级别。Zookeeper中,数据节点没有“所有者”的概念。访问者利用id标识自己的身份,并获得与之相应的 不同的访问权限。

注意:

传统的文件系统中,ACL分为两个维度,一个是属组,一个是权限,子目录/文件默认继承父目录的ACL。而在Zookeeper中一个ACL和一个ZooKeeper节点相对应。并且,父节点的ACL与子节点的ACL是相互独立的。也就是说,ACL不能被子节点所继承,父节点所拥有的权限与子节点所用的权限都没有任何关系。

Zookeeper支持可配置的认证机制。它利用一个三元组来定义客户端的访问权限:(scheme:expression, perms) 。其中:

1.scheme:定义了expression的含义。

如:(host:host1.corp.com,READ),标识了一个名为host1.corp.com的主机,有该数据节点的读权限。

2.Perms:标识了操作权限。

如:(ip:19.22.0.0/16, READ),表示IP地址以19.22开头的主机,有该数据节点的读权限。

Zookeeper的ACL也可以从三个维度来理解:一是,scheme; 二是,user; 三是,permission,通常表示为scheme:id:permissions,如下图所示。

1.world : id格式:anyone。

如:world:anyone代表任何人,zookeeper中对所有人有权限的结点就是属于world:anyone的。

2.auth : 它不需要id。

注:只要是通过authentication的user都有权限,zookeeper支持通过kerberos来进行认证, 也支持username/password形式的认证。

3.digest: id格式:username:BASE64(SHA1(password))。

它需要先通过username:password形式的authentication。

4.ip: id格式:客户机的IP地址。

设置的时候可以设置一个ip段。如:ip:192.168.1.0/16, 表示匹配前16个bit的IP段

5.super: 超级用户模式。

在这种scheme情况下,对应的id拥有超级权限,可以做任何事情

ZooKeeper权限定义如下图所示:

ZooKeeper内置的ACL模式如下图所示:

当会话建立的时候,客户端将会进行自我验证。另外,ZooKeeper Java API支持三种标准的用户权限,它们分别为:

1.ZOO_PEN_ACL_UNSAFE:对于所有的ACL来说都是完全开放的,任何应用程序可以在节点上执行任何操作,比如创建、列出并删除子节点。

2.ZOO_READ_ACL_UNSAFE:对于任意的应用程序来说,仅仅具有读权限。

3.ZOO_CREATOR_ALL_ACL:授予节点创建者所有权限。需要注意的是,设置此权限之前,创建者必须已经通了服务器的认证。

下面演示一个通过digest(用户名密码的方式)为创建的节点设置ACL的例子,代码如下:

import org.apache.zookeeper.*;
import org.apache.zookeeper.server.auth.DigestAuthenticationProvider;
import org.apache.zookeeper.data.*;
import java.util.*;

public class NewDigest {

    public static void main(String[] args) throws Exception {//new一个acl
        List acls = new ArrayList();   

     //添加第一个id,采用用户名密码形式
        Id id1 = new Id("digest",DigestAuthenticationProvider.generateDigest("admin:admin"));
        ACL acl1 = new ACL(ZooDefs.Perms.ALL, id1);
        acls.add(acl1);
        
     //添加第二个id,所有用户可读权限
        Id id2 = new Id("world", "anyone");
        ACL acl2 = new ACL(ZooDefs.Perms.READ, id2);
        acls.add(acl2);

        // zk用admin认证,创建/test ZNode。
     ZooKeeper zk = new ZooKeeper("host1:2181,host2:2181,host3:2181",2000, null);
     zk.addAuthInfo("digest", "admin:admin".getBytes());
     zk.create("/test", "data".getBytes(), acls, CreateMode.PERSISTENT);
   }
}

3.4 ZooKeeper的执行

ZooKeeper服务可以以两种模式运行。在单机模式下,只有一个ZooKeeper服务器,便于用来测试。但是他没有高可用性和恢复性的保障。在工业界,ZooKeeper以复合模式10运行在一组叫ensemble的集群上。ZooKeeper通过复制来获得高可用性,同时,只要ensemble中大部分机器运作,就可以提供服务。在2n+1个节点的ensemble中,可以承受n台机器故障。

ZooKeeper的思想非常简单:他所需要做的就是保证对Znode树的每一次修改都复制到ensemble中的大部分机器上去。如果机器中的小部分出故障了,那么至少有一台机器将会恢复到最新状态,其他的则保存这副本,直到最终达到最新状态。Zookeeper采用Zab协议,它分为两个阶段,并且可能被无限的重复。

(1)阶段1:领导者选举

在ensemble中的机器要参与一个选择特殊成员的进程,这个成员叫领导者,其他机器脚跟随者。在大部分的跟随者与他们的领导者同步了状态以后,这个阶段才算完成。

(2)阶段2:原子广播

所有的写操作请求被传送给领导者,并通过广播将更新信息告诉跟随者。当大部分跟随者执行了修改之后,领导者就提交更新操作,客户端将得到更新成功的回应。未获得一致性的协议被设计为原子的,因此无论修改失败与否,他都分两阶段提交。

如果领导者出故障了,城下的机器将会再次进行领导者选举,并在新领导被选出前继续执行任务。如果在不久后老的领导者恢复了,那么它将以跟随者的身份继续运行。领导者选举非常快,由发布的结果所知,大约是200毫秒,因此在选举是性能不会明显减慢。
所有在ensemble中的机器在更新它们内存中的Znode树之前会先将更新信息写入磁盘。读操作请求可由任何机器服务,同时,由于他们只涉及内存查找,因此非常快。

3.5 ZooKeeper一致性

在ensemble中的领导者和跟随着非常灵活,跟随者通过更新号来滞后领导者11,结果导致了只要大部分而不是所有的ensemble中的元素确认更新,就能被提交了。对于ZooKeeper来说,一个较好的智能模式是将客户端连接到跟着领导者的ZooKeeper服务器上。客户端可能被连接到领导者上,但他不能控制它,而且在如下情况时,甚至可能不知道。参见下图:

每一个Znode树的更新都会给定一个唯一的全局标识,叫zxid(表示ZooKeeper事务“ID”)。更新是被排序的,因此如果zxid的z1<z2,那么z1就比z2先执行。对于ZooKeeper来说,这是分布式系统中排序的唯一标准。

ZooKeeper是一种高性能、可扩展的服务。ZooKeeper的读写速度非常快,并且读的速度要比写快。另外,在进行读操作的时候,ZooKeeper依然能够为旧的数据提供服务。这些都是由ZooKeeper所提供的一致性保证的,它具有如下特点:

(1)顺序一致性

任何一个客户端的更新都按他们发送的顺序排序,也就意味着如果一个客户端将Znode z的值更新为值a,那么在之后的操作中,他会将z更新为b,在客户端发现z带有值b之后,就不会再看见带有值a的z。

(2)原子性

更新不成功就失败,这意味着如果更新失败了,没有客户端会知道。☆☆

(3)单系统映像☆

无论客户端连接的是哪台服务器,他与系统看见的视图一样。这就意味着,如果一个客户端在相同的会话时连接了一台新的服务器,他将不会再看见比在之前服务器上看见的更老的系统状态,当服务器系统出故障,同时客户端尝试连接ensemble中的其他机器时,故障服务器的后面那台机器将不会接受连接,直到它连接到故障服务器。

(4)容错性☆☆☆

一旦更新成功后,那么在客户端再次更新他之前,他就固定了,将不再被修改,这就会保证产生下面两种结果:

如果客户端成功的获得了正确的返回代码,那么说明更新已经成功。如果不能够获得返回代码(由于通信错误、超时等原因),那么客户端将不知道更新是否生效。

当故障恢复的时候,任何客户端能够看到的执行成功的更新操作将不会回滚。

(5)实时性☆☆

在任何客户端的系统视图上的的时间间隔是有限的,因此他在超过几十秒的时间内部会过期。这就意味着,服务器不会让客户端看一些过时的数据,而是关闭,强制客户端转到一个更新的服务器上。

解释一下:

由于性能原因,读操作由ZooKeeper服务器的内存提供,而且不参与写操作的全局排序。这一特性可能会导致来自使用ZooKeeper外部机制交流的客户端与ZooKeeper状态的不一致。举例来说,客户端A将Znode z的值a更新为a',A让B来读z,B读到z的值是a而不是a’。这与ZooKeeper的保证机制是相容的(不允许的情况较作“同步一致的交叉客户端视 图”)。为了避免这种情况的发生,B在读取z的值之前,应该先调用z上的sync。Sync操作强制B连接上的ZooKeeper服务器与leader保 持一致这样,当B读到z的值时,他将成为A设置的值(或是之后的值)

容易混淆的是:

sync操作只能被异步调用12。这样操作的原因是你不需要等待他的返回,因为ZooKeeper保证了任何接下去的操作将会发生在sync在服务器上执行以后,即使操作是在sync完成前被调用的。

这些已执行的保证后,ZooKeeper更高级功能的设计与实现将会变得非常容易,例如:leader选举、队列,以及可撤销锁等机制的实现。

3.6 ZooKeeper会话

ZooKeeper客户端与ensemble中的服务器列表配置一致,在启动时,他尝试与表中的一个服务器相连接。如果连接失败了,他就尝试表中的其他服务器,以此类推,知道他最终连接到其中一个,或者ZooKeeper的所有服务器都无法获得时,连接失败。

一旦与ZooKeeper服务器连接成功,服务器会创建与客户端的一个新的对话。每个回话都有超时时段,这是应用程序在创建它时设定的。如果服务器没有在超时时段内得到请求,他可能会中断这个会话。一旦会话被中断了,他可能不再被打开,而且任何与会话相连接的临时节点都将丢失。

无论什么时候会话持续空闲长达一定时间,都会由客户端发送ping请求保持活跃(犹如心跳)。时间段要足够小以监测服务器故障(由读操作超时反应),并且能再回话超市时间段内重新连接到另一个服务器。

在ZooKeeper中有几个time参数。tick time是ZooKeeper中的基本时间长度,为ensemble里的服务器所使用,用来定义对于交互运行的调度。其他设置以tick time的名义定义,或者至少由它来约束。

创建更复杂的临时性状态的应用程序应该支持更长的会话超时,因为重新构建的代价会更昂贵。在一些情况下,我们可以让应用程序在一定会话时间内能够重启,并且避免会话过期。(这可能更适合执行维护或是升级)每个会话都由服务器给定一个唯一的身份和密码,而且如果是在建立连接时被传递给ZooKeeper的话,只要没有过期它能够恢复会话。

这些特性可以视为一种可以避免会话过期的优化,但它并不能代替用来处理会话过期。会话过期可能出现在机器突然故障时,或是由于任何原因导致的应用程序安全关闭了,但在会话中断前没有重启。

3.7 ZooKeeper实例状态

Zookeeper对象的转变是通过其生命周期中的不同状态来实现。可以使用getState()方法在任何时候去查询他的状态:

public states getState()

Zookeeper状态事务,如图3.5所示

图 3.5 Zookeeper状态事务

  getState()方法的返回类型是states,states是枚举类型代表Zookeeper对象可能所处的不同状态,一个Zookeeper实例可能一次只处于一个状态。一个新建的Zookeeper实例正在于Zookeeper服务器建立连接时,是处于CONNECTING状态的。一旦连接建立好以后,他就变成了Connected状态。

使用Zookeeper的客户端可以通过注册Watcher的方法来获取状态转变的消息。一旦进入了CONNNECTED状态,Watcher将获得一个KeepState值为SyncConnected的WatchedEvent。

注意Zookeeper的watcher有两个职责:

<1>了解Zookeeper的状态改变。传递给ZooKeeper对象构造函数的(默认)watcher,被用来监测状态的改变。

<2>了解Zonde的改变。监测Zonde的改变既可以使用专门的实例设置到读操作上,也可以使用读操作的默认watcher。

Zookeeper实例可能失去或重新连接Zookeeper服务,在CONNECTED和CONNECTING状态中切换。如果连接断开,watcher得到一个Disconnected事件。学要注意的是,这些状态的迁移是由Zookeeper实例自己发起的,如果连接断开他将自动尝试自动连接。

如果任何一个close()方法被调用,或是会话由Expired类型的KeepState提示过期时,ZooKeeper可能会转变成第三种状态CLOSED。一旦处于CLOSED状态,Zookeeper对象将不再是活动的了(可以使用states的isActive()方法进行测试),而且不能被重用。客户端必须建立一个新的Zookeeper实例才能重新连接到Zookeeper服务。


Zookeeper的环境配置

一、Zookeeper的搭建方式

Zookeeper安装方式有三种,单机模式和集群模式以及伪集群模式。

1.单机模式:Zookeeper只运行在一台服务器上,适合测试环境;

2.伪集群模式:就是在一台物理机上运行多个Zookeeper 实例。

3.集群模式:Zookeeper运行于一个集群上,适合生产环境,这个计算机集群被称为一个“集合体”(ensemble)。

Zookeeper通过复制来实现高可用性,只要集合体中半数以上的机器处于可用状态,它就能够保证服务继续。为什么一定要超过半数呢?这跟Zookeeper的复制策略有关:zookeeper确保对znode 树的每一个修改都会被复制到集合体中超过半数的机器上。

1.1 Zookeeper的单机模式搭建

(1)下载ZooKeeper:http://pan.baidu.com/s/1pJlwbR9

(2)解压:tar -zxvf zookeeper-3.4.5.tar.gz 重命名:mv zookeeper-3.4.5 zk

(3)配置文件:在conf目录下删除zoo_sample.cfg文件,创建一个配置文件zoo.cfg。

tickTime=2000
    dataDir=/usr/local/zk/data
    dataLogDir=/usr/local/zk/dataLog        
    clientPort=2181

(4)配置环境变量:为了今后操作方便,我们需要对Zookeeper的环境变量进行配置,方法如下在/etc/profile文件中加入如下内容:

export ZOOKEEPER_HOME=/usr/local/zk
    export PATH=.:HADOOP H OME/bin: ZOOKEEPER_HOME/bin:JAVA H OME/bin: PATH

(5)启动ZooKeeper的Server:zkServer.sh start;关闭ZooKeeper的Server:zkServer.sh stop

1.2 Zookeeper的伪集群模式搭建

Zookeeper不但可以在单机上运行单机模式Zookeeper,而且可以在单机模拟集群模式 Zookeeper的运行,也就是将不同节点运行在同一台机器。我们知道伪分布模式下Hadoop的操作和分布式模式下有着很大的不同,但是在集群为分布 式模式下对Zookeeper的操作却和集群模式下没有本质的区别。显然,集群伪分布式模式为我们体验Zookeeper和做一些尝试性的实验提供了很大 的便利。比如,我们在实验的时候,可以先使用少量数据在集群伪分布模式下进行测试。当测试可行的时候,再将数据移植到集群模式进行真实的数据实验。这样不 但保证了它的可行性,同时大大提高了实验的效率。这种搭建方式,比较简便,成本比较低,适合测试和学习,如果你的手头机器不足,就可以在一台机器上部署了 3个server。

1.2.1. 注意事项

在一台机器上部署了3个server,需要注意的是在集群为分布式模式下我们使用的每个配置文档模拟一台机器,也就是说单台机器及上运行多个Zookeeper实例。但是,必须保证每个配置文档的各个端口号不能冲突,除了clientPort不同之外,dataDir也不同。另外,还要在dataDir所对应的目录中创建myid文件来指定对应的Zookeeper服务器实例。

(1)clientPort端口:如果在1台机器上部署多个server,那么每台机器都要不同的 clientPort,比如 server1是2181,server2是2182,server3是2183,

(2)dataDir和dataLogDir:dataDir和dataLogDir也需要区分下,将数据文件和日志文件分开存放,同时每个server的这两变量所对应的路径都是不同的。

(3)server.X和myid: server.X 这个数字就是对应,data/myid中的数字。在3个server的myid文件中分别写入了0,1,2,那么每个server中的zoo.cfg都配 server.0 server.2,server.3就行了。因为在同一台机器上,后面连着的2个端口,3个server都不要一样,否则端口冲突。

下面是我所配置的集群伪分布模式,分别通过zoo1.cfg、zoo2.cfg、zoo3.cfg来模拟由三台机器的Zookeeper集群。详见下图1.1-1.3

代码清单 1.1 zoo1.cfg

# The number of milliseconds of each tick
tickTime=2000

# The number of ticks that the initial
# synchronization phase can take
initLimit=10

# The number of ticks that can pass between
# sending a request and getting an acknowledgement
syncLimit=5

# the directory where the snapshot is stored.
dataDir=/usr/local/zk/data_1

# the port at which the clients will connect
clientPort=2181

#the location of the log file
dataLogDir=/usr/local/zk/logs_1

server.0=localhost:2287:3387
server.1=localhost:2288:3388
server.2=localhost:2289:3389

代码清单 1.2 zoo2.cfg

# The number of milliseconds of each tick
tickTime=2000

# The number of ticks that the initial
# synchronization phase can take
initLimit=10

# The number of ticks that can pass between
# sending a request and getting an acknowledgement
syncLimit=5

# the directory where the snapshot is stored.
dataDir=/usr/local/zk/data_2

# the port at which the clients will connect
clientPort=2182

#the location of the log file
dataLogDir=/usr/local/zk/logs_2

server.0=localhost:2287:3387
server.1=localhost:2288:3388
server.2=localhost:2289:3389

代码清单 zoo3.cfg

# The number of milliseconds of each tick
tickTime=2000

# The number of ticks that the initial
# synchronization phase can take
initLimit=10

# The number of ticks that can pass between
# sending a request and getting an acknowledgement
syncLimit=5

# the directory where the snapshot is stored.
dataDir=/usr/local/zk/data_3

# the port at which the clients will connect
clientPort=2183

#the location of the log file
dataLogDir=/usr/local/zk/logs_3

server.0=localhost:2287:3387
server.1=localhost:2288:3388
server.2=localhost:2289:3389

1.2.2 启动

在集群为分布式下,我们只有一台机器,按时要运行三个Zookeeper实例。此时,如果在使用单机模式的启动命令是行不通的。此时,只要通过下面三条命令就能运行前面所配置的Zookeeper服务。如下所示:

zkServer.sh start zoo1.sh
zkServer.sh start zoo2.sh
zkServer.sh start zoo3.sh

启动过程,如下图1.4-1.5所示:

图 1.4

图 1.5

在运行完第一条指令之后,会出现一些错误异常,产生异常信息的原因是由于Zookeeper服务的每个实例都拥有全局配置信息,他们在启动的时候会随时随地的进行Leader选举操作。此时,第一个启动的Zookeeper需要和另外两个Zookeeper实例进行通信。但是,另外两个Zookeeper实例还没有启动起来,因此就产生了这样的异样信息。

我们直接将其忽略即可,待把图中“2号”和“3号”Zookeeper实例启动起来之后,相应的异常信息自然会消失。此时,可以通过下面三条命令,来查询。

zkServer.sh status zoo1.cfg
 zkServer.sh status zoo2.cfg
 zkServer.sh status zoo3.cfg

Zookeeper服务的运行状态,如图1.6。

图 1.6

1.3 Zookeeper的集群模式搭建

为了获得可靠地Zookeeper服务,用户应该在一个机群上部署Zookeeper。只要机群上大多数的Zookeeper服务启动了,那么总的Zookeeper服务将是可用的。集群的配置方式,和前两种类似,同样需要进行环境变量的配置。在每台机器上conf/zoo.cf配置文件的参数设置相同

1.3.1 创建myid

在dataDir(/usr/local/zk/data)目录创建myid文件

Server0机器的内容为:0

Server1机器的内容为:1

Server2机器的内容为:2

1.3.2 编写配置文件

在conf目录下删除zoo_sample.cfg文件,创建一个配置文件zoo.cfg,如图2.4所示。

代码清单 2.4 zoo.cfg中的参数设置

# The number of milliseconds of each tick
tickTime=2000

# The number of ticks that the initial
# synchronization phase can take
initLimit=10

# The number of ticks that can pass between
# sending a request and getting an acknowledgement
syncLimit=5

# the directory where the snapshot is stored.
dataDir=/usr/local/zk/data

# the port at which the clients will connect
clientPort=2183

#the location of the log file
dataLogDir=/usr/local/zk/log

server.0=hadoop:2288:3388
server.1=hadoop0:2288:3388
server.2=hadoop1:2288:3388

1.3.3 启动

分别在3台机器上启动ZooKeeper的Server:zkServer.sh start;

二、Zookeeper的配置

Zookeeper的功能特性是通过Zookeeper配置文件来进行控制管理的(zoo.cfg).这样的设计其实有其自身的原因,通过前面对Zookeeper的配置可以看出,在对Zookeeper集群进行配置的时候,它的配置文档是完全相同的。集群伪分布模式中,有少部分是不同的。这样的配置方式使得在部署Zookeeper服务的时候非常方便。如果服务器使用不同的配置文件,必须确保不同配置文件中的服务器列表相匹配。

在设置Zookeeper配置文档时候,某些参数是可选的,某些是必须的。这些必须参数就构成了Zookeeper配置文档的最低配置要求。另外,若要对Zookeeper进行更详细的配置,可以参考下面的内容。

2.1 基本配置

下面是在最低配置要求中必须配置的参数:

1.client:监听客户端连接的端口。

2.tickTime:基本事件单元,这个时间是作为Zookeeper服务器之间或客户端与服务器之间维持心跳的时间间隔,每隔tickTime时间就会发送一个心跳;最小的session过期时间为2倍tickTime   

3.dataDir:存储内存中数据库快照的位置,如果不设置参数,更新食物的日志将被存储到默认位置。

应该谨慎的选择日志存放的位置,使用专用的日志存储设备能够大大提高系统的性能,如果将日志存储在比较繁忙的存储设备上,那么将会很大程度上影像系统性能。

2.2 高级配置

下面是高级配置参数中可选配置参数,用户可以使用下面的参数来更好的规定Zookeeper的行为:

(1)dataLogdDir

这个操作让管理机器把事务日志写入“dataLogDir”所指定的目录中,而不是“dataDir”所指定的目录。这将允许使用一个专用的日志设备,帮助我们避免日志和快照的竞争。配置如下:

# the directory where the snapshot is stored
   dataDir=/usr/local/zk/data 

(2)maxClientCnxns

这个操作将限制连接到Zookeeper的客户端数量,并限制并发连接的数量,通过IP来区分不同的客户端。此配置选项可以阻止某些类别的Dos攻击。将他设置为零或忽略不进行设置将会取消对并发连接的限制。

例如,此时我们将maxClientCnxns的值设为1,如下所示:

# set maxClientCnxns
   maxClientCnxns=1

启动Zookeeper之后,首先用一个客户端连接到Zookeeper服务器上。之后如果有第二个客户端尝试对Zookeeper进行连接,或者有某些隐式的对客户端的连接操作,将会触发Zookeeper的上述配置。

(3)minSessionTimeout和maxSessionTimeout

即最小的会话超时和最大的会话超时时间。在默认情况下,minSession=2*tickTime;maxSession=20*tickTime。

2.3 集群配置

(1)initLimit

此配置表示,允许follower(相对于Leaderer言的“客户端”)连接并同步到Leader的初始化连接时间,以tickTime为单位。当初始化连接时间超过该值,则表示连接失败。

(2)syncLimit

此配置项表示Leader与Follower之间发送消息时,请求和应答时间长度。如果follower在设置时间内不能与leader通信,那么此follower将会被丢弃。

(3)server.A=B:C:D

A:其中 A 是一个数字,表示这个是服务器的编号;

B:是这个服务器的 ip 地址;

C:Leader选举的端口;

D:Zookeeper服务器之间的通信端口。

(3)myid和zoo.cfg

除了修改 zoo.cfg 配置文件,集群模式下还要配置一个文件 myid,这个文件在 dataDir 目录下,这个文件里面就有一个数据就是 A 的值,Zookeeper 启动时会读取这个文件,拿到里面的数据与 zoo.cfg 里面的配置信息比较从而判断到底是那个 server。

三、搭建ZooKeeper服务器集群

搭建要求:

1> zk服务器集群规模不小于3个节点

2> 要求各服务器之间系统时间要保持一致。

3.1 安装配置ZK

(1)使用WinScp将Zk传输到Hadoop主机上的/usr/local,我用的版本是zookeeper-3.4.5.tar.gz。

(2)在hadoop的/usr/local目录下,解压缩zk....tar.gz,设置环境变量

a解压缩:在/usr/local目录下,执行命令:tar -zxvf zookeeper-3.4.5.tar.gz,如图2.1。

图 2.1

b重命名:解压后将文件夹,重命名为zk,执行命令: mv zookeeper-3.4.5 zk如图2.2。

图 2.2

c)设置环境变量:执行命令: vi /etc/profile ,添加 :export ZOOKEEPER_HOME=/usr/local/zk,如图2.3所示的内容。执行命令:source /etc/profile 如图2.4所示。

图 2.3

图2.4

3.2 修改ZK配置文件

(1)重命名:将/usr/local/zk/conf目录下zoo_sample.cfg,重命名为zoo.cfg,执行命令:mv zoo_sample.cfg zoo.cfg。如图2.5所示。

图 2.5

(2)查看:在/usr/local/zk/conf目录下,修改文件 vi zoo.cfg,文件内容如下图2.6所示。在该文件中dataDir表示文件存放目录,它的默认设置为/tmp/zookeeper这是一个临时存放目录,每次重启后会丢失,在这我们自己设一个目录,/usr/local/zk/data。

图 2.6

(2)创建文件夹:mkdir /usr/local/zk/data

(3)创建myid:在data目录下,创建文件myid,值为0;vi myid ;内容为0。

(4)编辑:编辑该文件,执行vi zoo.cfg ,修改dataDir=/usr/local/zk/data。

新增:server.0=hadoop:2888:3888

server.1=hadoop0:2888:3888

server.2=hadoop1:2888:3888

tickTime :这个时间是作为 Zookeeper 服务器之间或客户端与服务器之间维持心跳的时间间隔,也就是每个 tickTime 时间就会发送一个心跳;

dataDir:顾名思义就是 Zookeeper 保存数据的目录,默认情况下,Zookeeper 将写数据的日志文件也保存在这个目录里;

clientPort:这个端口就是客户端连接 Zookeeper 服务器的端口,Zookeeper 会监听这个端口,接受客户端的访问请求。

当这些配置项配置好后,就可以启动 Zookeeper 了,启动后使用命令echo ruok | nc localhost 2181检查 Zookeeper 是否已经在服务。

3.3 配置其他节点

(1)把haooop主机的zk目录和/etc/profile目录,复制到hadoop0和hadoop1中。执行命令:

scp -r /usr/local/zk/ hadoop0:/usr/local/
      scp -r /usr/local/zk/ hadoop1:/usr/local/
      scp /etc/profile hadoop0:/etc/
      scp /etc/profile hadoop1:/etc/

      ssh hadoop0
      suorce /etc/profile
      vi /usr/local/zk/data/myid
      exit

      ssh hadoop1
      suorce /etc/profile
      vi /usr/local/zk/data/myid
      exit

(2)把hadoop1中相应的myid的值改为1,把hadoop2中相应的myid的值改为2。   

四、启动检验

(1)启动,在三个节点上分别执行命令zkServer.sh start

hadoop节点:如图3.1所示。

图 3.1

hadoop0节点:如图3.2所示。

图 3.2

hadoop1节点:如图3.3所示。

图 3.3

(2)检验,在三个节点上分别执行命令zkServer.sh status,从下面的图中我们会发现hadoop和hadoop1为Follower,hadoop0为Leader。

hadoop节点:如图3.4

图 3.4

hadoop0节点:如图3.5


前面虽然配置了集群模式的Zookeeper,但是为了方面学建议在伪分布式模式的Zookeeper学习Zookeeper的shell命令。

一、Zookeeper的四字命令

Zookeeper支持某些特定的四字命令字母与其的交互。他们大多数是查询命令,用来获取Zookeeper服务的当前状态及相关信息。用户在客户端可以通过telnet或nc向Zookeeper提交相应的命令。Zookeeper常用的四字命令见图1.1所示。

图 1.1

图1.2是Zookeeper四字命令的一个简单用例。

[root@hadoop ~]# echo ruok|nc localhost 2181
[root@hadoop ~]# zkServer.sh start zoo1.cfg
JMX enabled by default
Using config: /usr/local/zk/bin/../conf/zoo1.cfg
Starting zookeeper ... STARTED
[root@hadoop ~]#  zkServer.sh start zoo2.cfg
JMX enabled by default
Using config: /usr/local/zk/bin/../conf/zoo2.cfg
Starting zookeeper ... STARTED
[root@hadoop ~]#  zkServer.sh start zoo3.cfg
JMX enabled by default
Using config: /usr/local/zk/bin/../conf/zoo3.cfg
Starting zookeeper ... STARTED
[root@hadoop ~]# echo ruok|nc localhost 2181
imok[root@hadoop ~]# echo ruok|nc localhost 2182
imok[root@hadoop ~]# echo ruok|nc localhost 2183
imok[root@hadoop ~]# echo conf|nc localhost 2181
clientPort=2181
dataDir=/usr/local/zk/data_1/version-2
dataLogDir=/usr/local/zk/logs_1/version-2
tickTime=2000
maxClientCnxns=60
minSessionTimeout=4000
maxSessionTimeout=40000
serverId=0
initLimit=10
syncLimit=5
electionAlg=3
electionPort=3387
quorumPort=2287
peerType=0
[root@hadoop ~]#

二、Zookeeper的简单操作

2.1 Zookeeper的shell操作

2.1.1 Zookeeper命令工具

再启动Zookeeper服务之后,输入以下命令,连接到Zookeeper服务:

zkCli.sh -server localhost:2181

执行结果如下所示:

[root@hadoop ~]# zkCli.sh -server localhost:2181
Connecting to localhost:2181
2014-10-17 03:35:51,051 [myid:] - INFO  [main:Environment@100] 
- Client environment:zookeeper.version=3.4.5-1392090, built on 09/30/2012 17:52 GMT
2014-10-17 03:35:51,055 [myid:] - INFO  [main:Environment@100] - Client environment:host.name=hadoop
2014-10-17 03:35:51,057 [myid:] - INFO  [main:Environment@100] - Client environment:java.version=1.6.0_24
2014-10-17 03:35:51,057 [myid:] - INFO  [main:Environment@100] - Client environment:java.vendor=Sun Microsystems Inc.
2014-10-17 03:35:51,066 [myid:] - INFO  [main:Environment@100] - Client environment:java.home=/usr/local/jdk/jre
2014-10-17 03:35:51,079 [myid:] - INFO  [main:Environment@100]
 - Client environment:java.class.path=/usr/local/zk/bin/../build/classes:/usr/local/zk/bin/../build/lib/
*.jar:/usr/local/zk/bin/../lib/slf4j-log4j12-1.6.1.jar:/usr/local/zk/bin/../lib/slf4j-api-1.6.1.
jar:/usr/local/zk/bin/../lib/netty-3.2.2.Final.jar:/usr/local/zk/bin/
../lib/log4j-1.2.15.jar:/usr/local/zk/bin/../lib/jline-0.9.94.jar:/usr/local/zk/bin/../
zookeeper-3.4.5.jar:/usr/local/zk/bin/../src/java/lib/*.jar:/usr/local/zk/bin/../conf:
2014-10-17 03:35:51,083 [myid:] - INFO  [main:Environment@100]
 - Client environment:java.library.path=/usr/local/jdk/jre/lib/
i386/client:/usr/local/jdk/jre/lib/i386:/usr/local/jdk/jre/../lib/i386:/usr/java/packages/lib/i386:/lib:/usr/lib
2014-10-17 03:35:51,084 [myid:] - INFO  [main:Environment@100] - Client environment:java.io.tmpdir=/tmp
2014-10-17 03:35:51,086 [myid:] - INFO  [main:Environment@100] - Client environment:java.compiler=<NA>
2014-10-17 03:35:51,099 [myid:] - INFO  [main:Environment@100] - Client environment:os.name=Linux
2014-10-17 03:35:51,100 [myid:] - INFO  [main:Environment@100] - Client environment:os.arch=i386
2014-10-17 03:35:51,101 [myid:] - INFO  [main:Environment@100] - Client environment:os.version=2.6.32-358.el6.i686
2014-10-17 03:35:51,101 [myid:] - INFO  [main:Environment@100] - Client environment:user.name=root
2014-10-17 03:35:51,102 [myid:] - INFO  [main:Environment@100] - Client environment:user.home=/root
2014-10-17 03:35:51,106 [myid:] - INFO  [main:Environment@100] - Client environment:user.dir=/root
2014-10-17 03:35:51,120 [myid:] - INFO  [main:ZooKeeper@438] - Initiating client connection,
 connectString=localhost:2181 sessionTimeout=30000 watcher=org.apache.zookeeper.ZooKeeperMain$MyWatcher@b02e7a
Welcome to ZooKeeper!
JLine support is enabled
2014-10-17 03:35:51,233 [myid:] - INFO  [main-SendThread(localhost:2181):ClientCnxn$SendThread@966] 
- Opening socket connection to server localhost/127.0.0.1:2181. 
Will not attempt to authenticate using SASL (Unable to locate a login configuration)
2014-10-17 03:35:51,247 [myid:] - INFO  [main-SendThread(localhost:2181):ClientCnxn$SendThread@849] 
- Socket connection established to localhost/127.0.0.1:2181, initiating session
[zk: localhost:2181(CONNECTING) 0] 2014-10-17 03:35:51,290 [myid:] - INFO
  [main-SendThread(localhost:2181):ClientCnxn$SendThread@1207] 
- Session establishment complete on server localhost/127.0.0.1:2181, sessionid = 0x491da0e20b0000, negotiated timeout = 30000

WATCHER::

WatchedEvent state:SyncConnected type:None path:null

[zk: localhost:2181(CONNECTED) 0]

连接成功之后,系统会输出Zookeeper的相关环境及配置信息,并在屏幕输出“welcome to Zookeeper!”等信息。输入help之后,屏幕会输出可用的Zookeeper命令,如图2.1所示。

图 2.1

2.1.2 使用Zookeeper命令的简单操作步骤

(1)使用ls命令查看当前Zookeeper中所包含的内容:ls /

[zk: localhost:2181(CONNECTED) 1] ls /
[zookeeper]
[zk: localhost:2181(CONNECTED) 2]

(2)创建一个新的Znode节点"zk",以及和它相关字符,执行命令:create /zk myData

[zk: localhost:2181(CONNECTED) 2] create /zk myData
Created /zk

(3)再次使用ls命令来查看现在Zookeeper的中所包含的内容:ls /

[zk: localhost:2181(CONNECTED) 3] ls /
[zk, zookeeper]

此时看到,zk节点已经被创建。

(4)使用get命令来确认第二步中所创建的Znode是否包含我们创建的字符串,执行命令:get /zk

[zk: localhost:2181(CONNECTED) 4] get /zk
myData
cZxid = 0x500000006
ctime = Fri Oct 17 03:54:20 PDT 2014
mZxid = 0x500000006
mtime = Fri Oct 17 03:54:20 PDT 2014
pZxid = 0x500000006
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 6
numChildren = 0

(4)接下来通过set命令来对zk所关联的字符串进行设置,执行命令:set /zk jiang1234

[zk: localhost:2181(CONNECTED) 5] set /zk jiang2014
cZxid = 0x500000006
ctime = Fri Oct 17 03:54:20 PDT 2014
mZxid = 0x500000007
mtime = Fri Oct 17 03:55:50 PDT 2014
pZxid = 0x500000006
cversion = 0
dataVersion = 1
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 9
numChildren = 0

(5)再次使用get命令来查看,上次修改的内容,执行命令:get /zk

[zk: localhost:2181(CONNECTED) 6] get /zk
jiang2014
cZxid = 0x500000006
ctime = Fri Oct 17 03:54:20 PDT 2014
mZxid = 0x500000007
mtime = Fri Oct 17 03:55:50 PDT 2014
pZxid = 0x500000006
cversion = 0
dataVersion = 1
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 9
numChildren = 0

(6)下面我们将刚才创建的Znode删除,执行命令:delete /zk

[zk: localhost:2181(CONNECTED) 7] delete /zk

(7)最后再次使用ls命令查看Zookeeper中的内容,执行命令:ls /

[zk: localhost:2181(CONNECTED) 8] ls /
[zookeeper]

经过验证,zk节点已经删除。

2.2 Zookeeper的api的简单使用

2.2.1 ZookeeperAPI简介

Zookeeper API共包含五个包,分别为:

(1)org.apache.zookeeper

(2)org.apache.zookeeper.data

(3)org.apache.zookeeper.server

(4)org.apache.zookeeper.server.quorum

(5)org.apache.zookeeper.server.upgrade

其中org.apache.zookeeper,包含Zookeeper类,他是我们编程时最常用的类文件。这个类是Zookeeper客户端的主要类文件。如果要使用Zookeeper服务,应用程序首先必须创建一个Zookeeper实例,这时就需要使用此类。一旦客户端和Zookeeper服务建立起了连接,Zookeeper系统将会给次连接会话分配一个ID值,并且客户端将会周期性的向服务器端发送心跳来维持会话连接。只要连接有效,客户端就可以使用Zookeeper API来做相应处理了。

Zookeeper类提供了如图2.2所示的几类主要方法

图 2.2

2.2.2 Zookeeper API的使用

这里通过一个例子来简单介绍如何使用Zookeeper API 编写自己的应用程序,代码如下:

package org.zk;

import java.io.IOException;
import java.util.List;

import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.ZooKeeper;

public class ListGroup extends ConnectionWatcher {
    public void list(String groupNmae) throws KeeperException, InterruptedException{
        String path ="/"+groupNmae;
        try {
            List children = zk.getChildren(path, false);
            if(children.isEmpty()){
                System.out.printf("No memebers in group %s\n",groupNmae);
                System.exit(1);
            }
            for(String child:children){
                System.out.println(child);
            }
        } catch (KeeperException.NoNodeException e) {
            System.out.printf("Group %s does not exist \n", groupNmae);
            System.exit(1);
        } 
    }
    public static void main(String[] args) throws IOException, InterruptedException, KeeperException {
        ListGroup listGroup = new ListGroup();
        listGroup.connect(args[0]);
        listGroup.list(args[1]);
        listGroup.close();
    }
}

此类包含两个主要的 ZooKeeper 函数,分别为 createZKInstance ()和 ZKOperations()。其中:

(1) createZKInstance()函数负责对 ZooKeeper 实例 zk 进行初始化。

ZooKeeper 类有两个构造函数,我们这里使用“ ZooKeeper (String connectString, int sessionTimeout ,Watcher watcher )”对其进行初始化。因此,我们需要提供初始化所需的,连接字符串信息,会话超时时间,以及一个 watcher 实例。 19行到 25行代码,是程序所构造的一个watcher 实例,它能够输出所发生的事件。

(2)ZKOperations ()函数是我们所定义的对节点的一系列操作。

它包括:创建 ZooKeeper 节点( 35行到 36行代码)、查看节点( 38 行到 39 行代码)、修改节点数据( 41 行到 42 行代码)、查看修改后节点数据( 44 行到 45行代码)、删除节点( 47行到 48行代码)、查看节点是否存在( 50 行到 51 行代码)。

注意:

1.在创建节点的时候,需要提供节点的名称、数据、权限以及节点类型。此外,使用 exists 函数时,如果节点不存在将返回一个 null 值。

2.关于 ZooKeeper API 的更多详细信息,读者可以查看 ZooKeeper 的 API 文档,如下所示:

代码的运行结果如下:

1. 创建ZooKeeper节点(Znode:/znode;数据:myData2;权限:OPEN_ACL_UNSAFE;节点类型:Persistent)
 None
2. 查看节点是否创建成功:
 /znode myData2
3. 修改节点数据:
4. 查看是否修改成功:
 jiang1234
5. 删除节点:
6. 查看/znode节点状态:
 节点间状态:[null]

三、ZooKeeper示例

假设一组服务器,用于为客户端提供一些服务。我们希望每个客户端都能够能够找到其中一台服务器,使其能够使用这些服务,挑战之一就是维护这组服务器列表。这组服务器的成员列表明显不能存在网络中的单个节点上,因为如果那个节点发生故障,就意味着是整个系统的故障(我们希望这个列表有很高的可用性)。假设我们有了一个可靠的方法解决了这个成员列表的存储问题。如果其中一台服务器出现故障,我们仍然需要解决如何从服务器成员列表中将它删除的问题。某个进程需要负责删除故障服务器,但注意不能由故障服务器自己来完成,因为故障服务器已经不再运行。
  我们所描述的不是一个被动的分布式数据结构,而是一个主动的、能够在某个外部事件发生时修改数据项状态的数据结构。ZooKeeper提供这种服务,所以让我们看看如何使用它来实现这种众所周知的组成员管理应用。

ZooKeeper中的组成员关系

·理解ZooKeeper的一种方法就是将其看作一个具有高可用性的文件系统。但这个文件系统中没有文件和目录,而是统一使用“节点”(node)的概念,称为znode。znode既可以作为保存数据的容器(如同文件),也可以作为保存其他znode的容器(如同目录)。所有的znode构成一个层次化的命名空间。一种自然的建立组成员列表的方式就是利用这种层次结构,创建一个以组名为节点名的znode作为父节点,然后以组成员名(服务器名)为节点名来创建作为子节点的znode。如下图给出了一组具有层次结构的znode。

图 ZooKeeper中的znode

在这个示例中,我们没有在任何znode中存储数据,但在一个真实的应用中,你可以将“关于成员的数据”存储在它们的znode中,例如主机名。

3.1 创建组

3.1.1 代码示例

让我们通过编写一段程序的方式来再次详细介绍ZooKeeper的Java API,这段示例程序用于为组名为/zoo的组创建一个znode。代码参见代码3.1:

代码 3.1 该程序在ZooKeeper中新建表示组的Znode

package org.zk;

import java.io.IOException;
import java.util.concurrent.CountDownLatch;

import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.Watcher.Event.KeeperState;
import org.apache.zookeeper.ZooDefs.Ids;
import org.apache.zookeeper.ZooKeeper;

public class CreateGroup implements Watcher{
    private static final int SESSION_TIMEOUT=5000;
    
    private ZooKeeper zk;
    private CountDownLatch connectedSignal=new CountDownLatch(1);
    @Override
    public void process(WatchedEvent event) {
        if(event.getState()==KeeperState.SyncConnected){
            connectedSignal.countDown();
        }
    }
    
    public static void main(String[] args) throws IOException, InterruptedException, KeeperException {
        CreateGroup createGroup = new CreateGroup();
        createGroup.connect(args[0]);
        createGroup.create(args[1]);
        createGroup.close();
    }

    private void close() throws InterruptedException {
        zk.close();
    }

    private void create(String groupName) throws KeeperException, InterruptedException {
        String path="/"+groupName;
        if(zk.exists(path, false)== null){
            zk.create(path, null/*data*/, Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
        }
        System.out.println("Created:"+path);
    } 

    private void connect(String hosts) throws IOException, InterruptedException {
        zk = new ZooKeeper(hosts, SESSION_TIMEOUT, this);
        connectedSignal.await();
    }
}

运行该程序需要配置classpath环境变量或者在执行Java命令时添加-classpath选项,具体运行方式参见:http://www.cnblogs.com/sunddenly/p/4050812.html

运行后的结果为:

[root@hadoop code]# ls
build  classes  CreateGroup.java  HelloWorld.java  jar.jar  PackageTest.java  zookeeper.out
[root@hadoop code]# javac -d ./classes CreateGroup.java
[root@hadoop code]# java org.zk.CreateGroup localhost:2181 zoo
2014-10-28 18:00:26,154 [myid:] - INFO  [main:Environment@100] 
- Client environment:zookeeper.version=3.4.5-1392090, built on 
2014-10-28 18:00:26,157 [myid:] - INFO  [main:Environment@100] - Client environment:host.name=hadoop
2014-10-28 18:00:26,157 [myid:] - INFO  [main:Environment@100] - Client environment:java.version=1.6.0_24
2014-10-28 18:00:26,157 [myid:] - INFO  [main:Environment@100] - Client environment:java.vendor=Sun Microsystems Inc.
2014-10-28 18:00:26,158 [myid:] - INFO  [main:Environment@100] - Client environment:java.home=/usr/local/jdk/jre
2014-10-28 18:00:26,158 [myid:] - INFO  [main:Environment@100] - Client environment:java.class.path=……
……
Created:/zoo
2014-10-28 18:00:26,236 [myid:] - INFO  [main:ZooKeeper@684] - Session: 0x4956f7f1d70005 closed
2014-10-28 18:00:26,237 [myid:] - INFO  [main-EventThread:ClientCnxn$EventThread@509] - EventThread shut down
[root@hadoop code]#

3.1.2 代码分析

在上面代码中,main()方法执行时,创建了一个CreateGroup的实例并且调用了这个实例的connect()方法。connect方法实例化了一个新的ZooKeeper类的对象,这个类
是客户端API中的主要类,并且负责维护客户端和ZooKeeper服务之间的连接。ZooKeeper类的构造函数有三个参数:

第一个是:ZooKeeper服务的主机地址,可指定端口,默认端口是2181。

第二个是:以毫秒为单位的会话超时参数,这里我们设成5秒。

第三个是:参数是一个Watcher对象的实例。

Watcher对象接收来自于ZooKeeper的回调,以获得各种事件的通知。在这个例子中,CreateGroup是一个Watcher对象,因此我们将它传递给ZooKeeper的构造函数。

当一个ZooKeeper的实例被创建时,会启动一个线程连接到ZooKeeper服务。由于对构造函数的调用是立即返回的,因此在使用新建的ZooKeeper对象之前一定要等待其与ZooKeeper服务之间的连接建立成功。我们使用Java的CountDownLatch类来阻止使用新建的ZooKeeper对象,直到这个ZooKeeper对象已经准备就绪。这就是Watcher类的用途,在它的接口中只有一个方法:

public void process(WatcherEvent event);

客户端已经与ZooKeeper建立连接后,Watcher的process()方法会被调用,参数是一个表示该连接的事件。在接收到一个连接事件(由Watcher.Event.KeeperState的枚举型值SyncConnected来表示)时,我们通过调用CountDownLatch的countDown()方法来递减它的计数器。锁存器(latch)被创建时带有一个值为1的计数器,用于表示在它释放所有等待线程之前需要发生的事件数。在调用一欢countDown()方法之后,计数器的值变为0,则await()方法返回。

现在connect()方法已经返回,下一个执行的是CreateGroup的create()方法。在这个方法中,我们使用ZooKeeper实例中的create()方法来创建一个新的ZooKeeper的znode。所需的参数包括:

1.路径:用字符串表示。

2.znode的内容:字节数组,本例中使用空值。

3.访问控制列表:简称ACL,本例中使用了完全开放的ACL,允许任何客户端对znode进行读写。

4.创建znode的类型:有两种类型的znode:短暂的和持久的。

创建znode的客户端断开连接时,无论客户端是明确断开还是因为任何原因而终止,短暂znode都会被ZooKeeper服务删除。与之相反,当客户端断开连接时,持久znode不会被删除。我们希望代表一个组的znode存活的时间应当比创建程序的生命周期要长,因此在本例中我们创建了一个持久的znode。
  create()方法的返回值是ZooKeeper所创建的路径,我们用这个返回值来打印一条表示路径成功创建的消息。当我们查看“顺序znode”(sequential znode)时.会发现create()方法返回的路径与传递给该方法的路径不同。

3.2 加入组

下面的这一段程序用于注册组的成员。每个组成员将作为一个程序运行,并且加入到组中。当程序退出时,这个组成员应当从组中被删除。为了实现这一点,我们在ZooKeeper的命名空间中使用短暂znode来代表一个组成员。

代码3.2 中的程序JoinGroup 实现了这个想法。在基类ConnectionWatcher中,对创建和连接ZooKeeper实例的程序逻辑进行了重构,参见代码3.2。

代码 3.2 用于将成员加入组的程序

package org.zk;

import java.io.IOException;

import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.ZooDefs.Ids;

public class JoinGroup extends ConnectionWatcher{
    public void join(String groupName,String memberName) throws KeeperException, InterruptedException{
        String path="/"+groupName+"/"+memberName;
        String createdPath=zk.create(path, null, Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
        System.out.println("Created:"+createdPath);
    }
    public static void main(String[] args) throws InterruptedException, IOException, KeeperException {
        JoinGroup joinGroup = new JoinGroup();
        joinGroup.connect(args[0]);
        joinGroup.join(args[1], args[2]);
        
        //stay alive until process is killed or thread is interrupted
        Thread.sleep(Long.MAX_VALUE);
    }
}

代码 3.3 用于等待建立与ZooKeeper连接的辅助类

package org.zk;

import java.io.IOException;
import java.util.concurrent.CountDownLatch;

import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.Watcher.Event.KeeperState;
import org.apache.zookeeper.ZooKeeper;

public class ConnectionWatcher implements Watcher{
    private static final int SESSION_TIMEOUT=5000;
    
    protected ZooKeeper zk;
    CountDownLatch connectedSignal=new CountDownLatch(1);
    public void connect(String host) throws IOException, InterruptedException{
        zk=new ZooKeeper(host, SESSION_TIMEOUT, this);
        connectedSignal.await();
    }
    @Override
    public void process(WatchedEvent event) {
        if(event.getState()==KeeperState.SyncConnected){
            connectedSignal.countDown();
        }
    }
    public void close() throws InterruptedException{
        zk.close();
    }

}

JoinGroup的代码与CreateGroup非常相似,在它的join()方法中,创建短暂znode,作为组znode的子节点,然后通过休眠来模拟正在做某种工作,直到该进程被强行终止。接着,你会看到随着进程终止,这个短暂znode被ZooKeeper删除。

3.3 列出组成员

现在,我们需要一段程序来查看组成员,参见代码3.4。

代码 3.4 用于列出组成员的程序

package org.zk;

import java.io.IOException;
import java.util.List;

import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.ZooKeeper;

public class ListGroup extends ConnectionWatcher {
    public void list(String groupNmae) throws KeeperException, InterruptedException{
        String path ="/"+groupNmae;
        try {
            List children = zk.getChildren(path, false);
            if(children.isEmpty()){
                System.out.printf("No memebers in group %s\n",groupNmae);
                System.exit(1);
            }
            for(String child:children){
                System.out.println(child);
            }
        } catch (KeeperException.NoNodeException e) {
            System.out.printf("Group %s does not exist \n", groupNmae);
            System.exit(1);
        } 
    }
    public static void main(String[] args) throws IOException, InterruptedException, KeeperException {
        ListGroup listGroup = new ListGroup();
        listGroup.connect(args[0]);
        listGroup.list(args[1]);
        listGroup.close();
    }
}

在list()方法中,我们调用了getChildren()方法来检索并打印输出一个znode的子节点列表,调用参数为:该znode的路径和设为false的观察标志。如果在一个znode上设置了观察标志,那么一旦该znode的状态改变,关联的观察(Watcher)会被触发。虽然在这里我们可以不使用观察,但在查看一个znode的子节点时,也可以设置观察,让应用程序接收到组成员加入、退出和组被删除的有关通知。

在这段程序中,我们捕捉了KeeperException.NoNodeException异常,代表组的znode不存在时,这个异常就会被抛出。

下面看一下ListGroup程序的工作过程:虽然搭建了分布式的ZooKeeper,但分布式ZooKeeper启动运行比较耗时,我在这采用前面提到的复制模式下的ZooKeeper来进行测试

首先我们得启动ZooKeeper,启动以后将上面的源程序放到Linux目录中并进行编译,我将其放到了"/usr/code"目录下,并在该目录下创建一个classes文件夹,用于存放生成字节码文件:

[root@hadoop ~]# cd /usr/code
[root@hadoop code]# ls
ConnectionWatcher.java  DeleteGroup.java  ListGroup.java
classes  CreateGroup.java  JoinGroup.java  PackageTest.java
[root@hadoop code]# javac -d ./classes ConnectionWatcher.java
[root@hadoop code]# javac -d ./classes *.java

由于目前我们还没有在组中添加任何成员,因此zoo是空的:

[root@hadoop code]# java org.zk.ListGroup  localhost zoo
2014-10-30 01:52:19,703 [myid:] - INFO  [main:Environment@100] - Client environment:……
……
No memebers in group zoo

我们可以使用JoinGroup来向组中添加成员。在sleep语句的作用下,这些作为组成员的znode不会自己终止,所以我们可以,以后台进程的方式来启动他们:

[root@hadoop code]# java org.zk.JoinGroup localhost zoo duck &
2014-10-30 02:06:05,018 [myid:] - INFO  [main:Environment@100] - Client environment:……
……
Created:/zoo/duck
[root@hadoop code]# java org.zk.JoinGroup localhost zoo cow &
2014-10-30 02:06:05,018 [myid:] - INFO  [main:Environment@100] - Client environment:……
……
Created:/zoo/cow
[root@hadoop code]# java org.zk.JoinGroup localhost zoo goat &
2014-10-30 02:06:05,018 [myid:] - INFO  [main:Environment@100] - Client environment:……
……
Created:/zoo/goat

最后一行命令保存了将goat添加到组中的java进程的ID。我们需要保存这个进程的ID,以便能够在查看组成员之后杀死进程。

[root@hadoop code]# 
2014-10-30 03:15:30,619 [myid:] - INFO  [main:Environment@100] - Client environment:……
……
duck
cow
goat

为了从组中删除一个成员,我们杀死了goat所对应的进程:

[root@hadoop code]# kill $goat_pid
几秒钟之后,由于进程的ZooKeeper会话已经结束(超时为5秒),
所以goat会从组成员列表消失,并且对应的短暂znode也已经被删除。
[root@hadoop code]# java org.zk.ListGroup localhost zoo
2014-10-30 03:23:41,120 [myid:] - INFO  [main:Environment@100] - Client environment:……
……
duck
cow

对于参与到一个分布式系统中的节点,这样就有了一个建立节点列表的方法。这些节点也许彼此并不了解。例如,一个想使用列表中节点来完成某些工作的客户端,能够在这些节点不知道客户端的情况下发现它们。

最后,注意,组成员关系管理并不能解决与节点通信过程中出现的网络问题。即使一个节点是一个组中的成员,在与其通信的过程中仍然会出现故障,这种故障必须以一种合适的方式解决(重试、使用组中另外一个成员等)。

3.4 ZooKeeper命令行工具

ZooKeeper提供了一个命令行工具用于在其命名空间内进行交互。我们可以使用这个命令工具列出/zoo节点之下的znode列表,如下所示

[root@hadoop code]# zkCli.sh -server localhost ls /zoo
Connecting to localhost
……
WATCHER::
WatchedEvent state:SyncConnected type:None path:null
[duck, cow]
[root@hadoop code]#

3.5 删除组

下面来看如何删除一个组。ZooKeeper类提供了一个delete()方法,该方法有两个参数:

1.路径

2.版本号

如果所提供的版本号与znode的版本号一致,ZooKeeper会删除这个znode。这是一种乐观的加锁机制,使客户端能够检测出对znode的修改冲突。通过将版本号设置为-1,可以绕过这个版本检测机制,不管znode的版本号是什么而直接将其删除。ZooKeeper不支持递归的删除操作,因此在删除父节点之前必须先删除子节点。

在代码3.5中,DeleteGroup类用于删除一个组及其所有成员。

代码3.5用于删除一个组及其所有成员的程序

package org.zk;

import java.io.IOException;
import java.util.List;

import org.apache.zookeeper.KeeperException;

public class DeleteGroup extends ConnectionWatcher{
    public void delete(String groupName) throws InterruptedException, KeeperException{
        String path="/"+groupName;
        List children;
        try {
            children = zk.getChildren(path, false);
            for(String child:children){
                zk.delete(path+"/"+child, -1);            
            }
            zk.delete(path, -1);
        } catch (KeeperException.NoNodeException e) {
            System.out.printf("Group %s does not exist\n", groupName);
            System.exit(1);
        }    
    }
    public static void main(String[] args) throws InterruptedException, IOException, KeeperException {
        DeleteGroup deleteGroup = new DeleteGroup();
        deleteGroup.connect(args[0]);
        deleteGroup.delete(args[1]);
        deleteGroup.close();
    }
}

最后,我们可以删除之前所创建的zoo组:

[root@hadoop code]# java org.zk.DeleteGroup localhost zoo
……
[root@hadoop code]# java org.zk.ListGroup localhost zoo
2014-10-30 05:39:41,974 [myid:] - INFO  [main:Environment@100] - Client environment:……
Group zoo does not exist
[root@hadoop code]#

一、配置服务

配置服务是分布式应用所需要的基本服务之一,它使集群中的机器可以共享配置信息中那些公共的部分。简单地说,ZooKeeper可以作为一个具有高可用性的配置存储器,允许分布式应用的参与者检索和更新配置文件。使用ZooKeeper中的观察机制,可以建立一个活跃的配置服务,使那些感兴趣的客户端能够获得配置信息修改的通知。

下面来编写一个这样的服务。我们通过两个假设来简化所需实现的服务(稍加修改就可以取消这两个假设)。

第一,我们唯一需要存储的配置数据是字符串,关键字是znode的路径,因此我们在每个znode上存储了一个键/值对。

第二,在任何时候只有一个客户端会执行更新操作。

除此之外,这个模型看起来就像是有一个主人(类似于HDFS中的namenode)在更新信息,而他的工人则需要遵循这些信息。

在名为ActiveKeyValueStore的类中编写了如下代码:

package org.zk;

import java.nio.charset.Charset;

import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooDefs.Ids;
import org.apache.zookeeper.data.Stat;

public class ActiveKeyValueStore extends ConnectionWatcher {
    private static final Charset CHARSET=Charset.forName("UTF-8");
    public void write(String path,String value) throws KeeperException, InterruptedException {
        Stat stat = zk.exists(path, false);
        if(stat==null){
            zk.create(path, value.getBytes(CHARSET),Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
        }else{
            zk.setData(path, value.getBytes(CHARSET),-1);
        }
    }
    public String read(String path,Watcher watch) throws KeeperException, InterruptedException{
        byte[] data = zk.getData(path, watch, null);
        return new String(data,CHARSET);
        
    }
    
}

write()方法的任务是将一个关键字及其值写到ZooKeeper。它隐藏了创建一个新的znode和用一个新值更新现有znode之间的区别,而是使用exists操作来检测znode是否存在,然后再执行相应的操作。其他值得一提的细节是需要将字符串值转换为字节数组,因为我们只用了UTF-8编码的getBytes()方法。

read()方法的任务是读取一个节点的配置属性。ZooKeeper的getData()方法有三个参数:

(1)路径

(2)一个观察对象

(3)一个Stat对象

Stat对象由getData()方法返回的值填充,用来将信息回传给调用者。通过这个方法,调用者可以获得一个znode的数据和元数据,但在这个例子中,由于我们对元数据不感兴趣,因此将Stat参数设为null。

为了说明ActiveKeyValueStore的用法,我们编写了一个用来更新配置属性值的类ConfigUpdater,如代码1.1所示。

代码1.1 用于随机更新ZooKeeper中的属性

package org.zk;

import java.io.IOException;
import java.util.Random;
import java.util.concurrent.TimeUnit;

import org.apache.zookeeper.KeeperException;

public class ConfigUpdater {
    
    public static final String  PATH="/config";
    
    private ActiveKeyValueStore store;
    private Random random=new Random();
    
    public ConfigUpdater(String hosts) throws IOException, InterruptedException {
        store = new ActiveKeyValueStore();
        store.connect(hosts);
    }
    public void run() throws InterruptedException, KeeperException{
        while(true){
            String value=random.nextInt(100)+"";
            store.write(PATH, value);
            System.out.printf("Set %s to %s\n",PATH,value);
            TimeUnit.SECONDS.sleep(random.nextInt(100));
            
        }
    }
    public static void main(String[] args) throws IOException, InterruptedException, KeeperException {
        ConfigUpdater configUpdater = new ConfigUpdater(args[0]);
        configUpdater.run();
    }
}

这个程序很简单,ConfigUpdater中定义了一个ActiveKeyValueStore,它在ConfigUpdater的构造函数中连接到ZooKeeper。run()方法永远在循环,在随机时间以随机值更新/config znode。

作为配置服务的用户,ConfigWatcher创建了一个ActiveKeyValueStore对象store,并且在启动之后通过displayConfig()调用了store的read()方法,显示它所读到的配置信息的初始值,并将自身作为观察传递给store。当节点状态发生变化时,再次通过displayConfig()显示配置信息,并再次将自身作为观察传递给store,参见代码1.2:

例1.2 该用应观察ZooKeeper中属性的更新情况,并将其打印到控制台

package org.zk;

import java.io.IOException;

import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.Watcher.Event.EventType;

public class ConfigWatcher implements Watcher{
    private ActiveKeyValueStore store;

    @Override
    public void process(WatchedEvent event) {
        if(event.getType()==EventType.NodeDataChanged){
            try{
                dispalyConfig();
            }catch(InterruptedException e){
                System.err.println("Interrupted. exiting. ");
                Thread.currentThread().interrupt();
            }catch(KeeperException e){
                System.out.printf("KeeperException锛?s. Exiting.\n", e);
            }
            
        }
        
    }
    public ConfigWatcher(String hosts) throws IOException, InterruptedException {
        store=new ActiveKeyValueStore();
        store.connect(hosts);
    }
    public void dispalyConfig() throws KeeperException, InterruptedException{
        String value=store.read(ConfigUpdater.PATH, this);
        System.out.printf("Read %s as %s\n",ConfigUpdater.PATH,value);
    }

    public static void main(String[] args) throws IOException, InterruptedException, KeeperException {
        ConfigWatcher configWatcher = new ConfigWatcher(args[0]);
        configWatcher.dispalyConfig();
        //stay alive until process is killed or Thread is interrupted
        Thread.sleep(Long.MAX_VALUE);
    }
}

当ConfigUpdater更新znode时,ZooKeeper产生一个类型为EventType.NodeDataChanged的事件,从而触发观察。ConfigWatcher在它的process()方法中对这个事件做出反应,读取并显示配置的最新版本。由于观察仅发送单次信号,因此每次我们调用ActiveKeyValueStore的read()方法时,都将一个新的观察告知ZooKeeper来确保我们可以看到将来的更新。但是,我们还是不能保证接收到每一个更新,因为在收到观察事件通知与下一次读之间,znode可能已经被更新过,而且可能是很多次,由于客户端在这段时间没有注册任何观察,因此不会收到通知。对于示例中的配置服务,这不是问题,因为客户端只关心属性的最新值,最新值优先于之前的值。但是,一般情况下,这个潜在的问题是不容忽视的。

让我们看看如何使用这个程序。在一个终端窗口中运行ConfigUpdater,然后在另一个客户端运行ConfigWatcher,我们可以预先分别在两个客户端输入命令,先不按回车,等两个客户端的命令输入好后,先在运行ConfigUpdater的客户端按回车,再在另一个客户端按回车,运行结果如下:

二、可恢复的ZooKeeper应用

关于分布式计算的第一个误区是“网络是可靠的”。按照他们的观点,程序总是有一个可靠的网络,因此当程序运行在真正的网络中时,往往会出现各种备样的故障。让我们看看各种可能的故障模式,以及能够解决故障的措施,使我们的程序在面对故障时能够及时复原。

2.1 ZooKeeper异常

Java API中的每一个ZooKeeper操作都在其throws子句中声明了两种类型的异常,分别是InterruptedException和KeeperException。

(一)InterruptedException异常

如果操作被中断,则会有一个InterruptedException异常被抛出。在Java语言中有一个取消阻塞方法的标准机制,即针对存在阻塞方法的线程调用interrupt()。一个成功的取消操作将产生一个InterruptedException异常。

ZooKeeper也遵循这一机制,因此你可以使用这种方法来取消一个ZooKeeper操作。使用了ZooKeeper的类或库通常会传播InterruptedException异常,使客户端能够取消它们的操作。InterruptedException异常并不意味着有故障,而是表明相应的操作已经被取消,所以在配置服务的示例中,可以通过传播异常来中止应用程序的运行。

(二)KeeperException异常

(1) 如果ZooKeeper服务器发出一个错误信号或与服务器存在通信问题,抛出的则是KeeperException异常。

①针对不同的错误情况,KeeperException异常存在不同的子类。

例如: KeeperException.NoNodeException是KeeperException的一个子类,如果你试图针对一个不存在的znode执行操作,抛出的则是该异常。

②每一个KeeperException异常的子类都对应一个关于错误类型信息的代码。

例如: KeeperException.NoNodeException异常的代码是KeeperException.Code.NONODE

(2) 有两种方法被用来处理KeeperException异常:

①捕捉KeeperException异常,并且通过检测它的代码来决定采取何种补救措施;

②另一种是捕捉等价的KeeperException子类,并且在每段捕捉代码中执行相应的操作。

(3) KeeperException异常分为三大类

① 状态异常

当一个操作因不能被应用于znode树而导致失败时,就会出现状态异常。状态异常产生的原因通常是在同一时间有另外一个进程正在修改znode。例如,如果一个znode先被另外一个进程更新了,根据版本号执行setData操作的进程就会失败,并收到一个KeeperException.BadVersionException异常,这是因为版本号不匹配。程序员通常都知道这种冲突总是存在的,也都会编写代码来进行处理。

一些状态异常会指出程序中的错误,例如KeeperException.NoChildrenForEphemeralsException异常,试图在短暂znode下创建子节点时就会抛出该异常。

② 可恢复异常

可恢复的异常是指那些应用程序能够在同一个ZooKeeper会话中恢复的异常。一个可恢复的异常是通过KeeperException.ConnectionLossException来表示的,它意味着已经丢失了与ZooKeeper的连接。ZooKeeper会尝试重新连接,并且在大多数情况下重新连接会成功,并确保会话是完整的。

但是ZooKeeper不能判断与KeeperException.ConnectionLossException异常相关的操作是否成功执行。这种情况就是部分失败的一个例子。这时程序员有责任来解决这种不确定性,并且根据应用的情况来采取适当的操作。在这一点上,就需要对“幂等”(idempotent)操作和“非幂等”(Nonidempotent)操作进行区分。幂等操作是指那些一次或多次执行都会产生相同结果的操作,例如读请求或无条件执行的setData操作。对于幂等操作,只需要简单地进行重试即可。对于非幂等操作,就不能盲目地进行重试,因为它们多次执行的结果与一次执行是完全不同的。程序可以通过在znode的路径和它的数据中编码信息来检测是否非幂等操怍的更新已经完成。

③不可恢复的异常

在某些情况下,ZooKeeper会话会失效——也许因为超时或因为会话被关闭,两种情况下都会收到KeeperException.SessionExpiredException异常,或因为身份验证失败,KeeperException.AuthFailedException异常。无论上述哪种情况,所有与会话相关联的短暂znode都将丢失,因此应用程序需要在重新连接到ZooKeeper之前重建它的状态。

2.2 可靠地服务配置

首先我们先回顾一下ActivityKeyValueStore的write()的方法,他由一个exists操作紧跟着一个create操作或setData操作组成:

public class ActiveKeyValueStore extends ConnectionWatcher {
    private static final Charset CHARSET=Charset.forName("UTF-8");
    public void write(String path,String value) throws KeeperException, InterruptedException {
        Stat stat = zk.exists(path, false);
        if(stat==null){
            zk.create(path, value.getBytes(CHARSET),Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
        }else{
            zk.setData(path, value.getBytes(CHARSET),-1);
        }
    }
    public String read(String path,Watcher watch) throws KeeperException, InterruptedException{
        byte[] data = zk.getData(path, watch, null);
        return new String(data,CHARSET);
        
    }
    
}

作为一个整体,write()方法是一个“幂等”操作,所以我们可以对他进行无条件重试。我们新建一个类ChangedActiveKeyValueStore,代码如下:

package org.zk;

import java.nio.charset.Charset;
import java.util.concurrent.TimeUnit;

import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooDefs.Ids;
import org.apache.zookeeper.data.Stat;

public class ChangedActiveKeyValueStore extends ConnectionWatcher{
    private static final Charset CHARSET=Charset.forName("UTF-8");
    private static final int MAX_RETRIES = 5; 
    private static final long RETRY_PERIOD_SECONDS = 5;
    
    public void write(String path,String value) throws InterruptedException, KeeperException{
        int retries=0;
        while(true){
            try {
                Stat stat = zk.exists(path, false);
                if(stat==null){
                    zk.create(path, value.getBytes(CHARSET),Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
                }else{
                    zk.setData(path, value.getBytes(CHARSET),stat.getVersion());
                }
            } catch (KeeperException.SessionExpiredException e) {
                throw e;
            } catch (KeeperException e) {
                if(retries++==MAX_RETRIES){
                    throw e;
                }
                //sleep then retry
                TimeUnit.SECONDS.sleep(RETRY_PERIOD_SECONDS);
            }
        }
    }
    public String read(String path,Watcher watch) throws KeeperException, InterruptedException{
        byte[] data = zk.getData(path, watch, null);
        return new String(data,CHARSET);
    }
}

在该类中,对前面的write()进行了修改,该版本的wirte()能够循环执行重试。其中设置了重试的最大次数MAX_RETRIES和两次重试之间的间隔RETRY_PERIOD_SECONDS.

我们再新建一个类ResilientConfigUpdater,该类对前面的ConfigUpdater进行了修改,代码如下:

package org.zk;

import java.io.IOException;
import java.nio.charset.Charset;
import java.util.Random;
import java.util.concurrent.TimeUnit;

import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.KeeperException.SessionExpiredException;
import org.apache.zookeeper.ZooDefs.Ids;
import org.apache.zookeeper.data.Stat;

public class ResilientConfigUpdater extends ConnectionWatcher{
    public static final String PATH="/config";
    private ChangedActiveKeyValueStore store;
    private Random random=new Random();
    
    public ResilientConfigUpdater(String hosts) throws IOException, InterruptedException {
        store=new ChangedActiveKeyValueStore();
        store.connect(hosts);
    }
    public void run() throws InterruptedException, KeeperException{
        while(true){
            String value=random.nextInt(100)+"";
            store.write(PATH,value);
            System.out.printf("Set %s to %s\n",PATH,value);
            TimeUnit.SECONDS.sleep(random.nextInt(10));
        }
    }

    public static void main(String[] args) throws Exception {
        while(true){
            try {
                ResilientConfigUpdater configUpdater = new ResilientConfigUpdater(args[0]);
                configUpdater.run();
            }catch (KeeperException.SessionExpiredException e) {
                // start a new session
            }catch (KeeperException e) {
                // already retried ,so exit
                e.printStackTrace();
                break;
            }
        }
    }
}

在这段代码中没有对KeepException.SeeionExpiredException异常进行重试,因为一个会话过期时,ZooKeeper对象会进入CLOSED状态,此状态下它不能进行重试连接。我们只能将这个异常简单抛出并让拥有着创建一个新实例,以重试整个write()方法。一个简单的创建新实例的方法是创建一个新的ResilientConfigUpdater用于恢复过期会话。

处理会话过期的另一种方法是在观察中(在这个例子中应该是ConnectionWatcher)寻找类型为ExpiredKeepState,然后再找到的时候创建一个新连接。即使我们收到KeeperException.SessionExpiredEception异常,这种方法还是可以让我们在write()方法内不断重试,因为连接最终是能够重新建立的。不管我们采用何种机制从过期会话中恢复,重要的是,这种不同于连接丢失的故障类型,需要进行不同的处理。

注意:

实际上,这里忽略了另一种故障模式。当ZooKeeper对象被创建时,他会尝试连接另一个ZooKeeper服务器。如果连接失败或超时,那么他会尝试连接集合体中的另一台服务器。如果在尝试集合体中的所有服务器之后仍然无法建立连接,它会抛出一个IOException异常。由于所有的ZooKeeper服务器都不可用的可能性很小,所以某些应用程序选择循环重试操作,直到ZooKeeper服务为止。

这仅仅是一种重试处理策略,还有许多其他处理策略,例如使用“指数返回”,每次将重试的间隔乘以一个常数。Hadoop内核中org.apache.hadoop.io.retry包是一组工具,用于可以重用的方式将重试逻辑加入代码,因此他对于构建ZooKeeper应用非常有用。

三、锁服务

3.1分布式锁概述

分布式锁在一组进程之间提供了一种互斥机制。在任何时刻,在任何时刻只有一个进程可以持有锁。分布式锁可以在大型分布式系统中实现领导者选举,在任何时间点,持有锁的那个进程就是系统的领导者。

注意

不要将ZooKeeper自己的领导者选举和使用了ZooKeeper基本操作实现的一般领导者选混为一谈。ZooKeeper自己的领导者选举机制是对外不公开的,我们这里所描述的一般领导者选举服务则不同,他是对那些需要与主进程保持一致的分布式系统所设计的。

(1) 为了使用ZooKeeper来实现分布式锁服务,我们使用顺序znode来为那些竞争锁的进程强制排序。

思路很简单:

① 首先指定一个作为锁的znode,通常用它来描述被锁定的实体,称为/leader;

② 然后希望获得锁的客户端创建一些短暂顺序znode,作为锁znode的子节点。

③ 在任何时间点,顺序号最小的客户端将持有锁。

例如,有两个客户端差不多同时创建znode,分别为/leader/lock-1和/leader/lock-2,那么创建/leader/lock-1的客户端将会持有锁,因为它的znode顺序号最小。ZooKeeper服务是顺序的仲裁者,因为它负责分配顺序号。

④ 通过删除znode /leader/lock-l即可简单地将锁释放;

⑤ 另外,如果客户端进程死亡,对应的短暂znode也会被删除。

⑥ 接下来,创建/leader/lock-2的客户端将持有锁,因为它顺序号紧跟前一个。

⑦ 通过创建一个关于znode删除的观察,可以使客户端在获得锁时得到通知。

(2) 如下是申请获取锁的伪代码。

①在锁znode下创建一个名为lock-的短暂顺序znode,并且记住它的实际路径名(create操作的返回值)。

②查询锁znode的子节点并且设置一个观察。

③如果步骤l中所创建的znode在步骤2中所返回的所有子节点中具有最小的顺序号,则获取到锁。退出。

④等待步骤2中所设观察的通知并且转到步骤2。

3.2 当前问题与方案

3.2.1 羊群效应

(1) 问题

虽然这个算法是正确的,但还是存在一些问题。第一个问题是这种实现会受到“羊群效应”(herd effect)的影响。考虑有成百上千客户端的情况,所有的客户端都在尝试获得锁,每个客户端都会在锁znode上设置一个观察,用于捕捉子节点的变化。每次锁被释放或另外一个进程开始申请获取锁的时候,观察都会被触发并且每个客户端都会收到一个通知。 “羊群效应“就是指大量客户端收到同一事件的通知,但实际上只有很少一部分需要处理这一事件。在这种情况下,只有一个客户端会成功地获取锁,但是维护过程及向所有客户端发送观察事件会产生峰值流量,这会对ZooKeeper服务器造成压力。

(2) 方案解决方案

为了避免出现羊群效应,我们需要优化通知的条件。关键在于只有在前一个顺序号的子节点消失时才需要通知下一个客户端,而不是删除(或创建)任何子节点时都需要通知。在我们的例子中,如果客户端创建了znode /leader/lock-1、/leader/lock-2和/leader/lock-3,那么只有当/leader/lock-2消失时才需要通知/leader/lock-3对照的客户端;/leader/lock-1消失或有新的znode /leader/lock-4加入时,不需要通知该客户端。

3.2.2 可恢复的异常

(1) 问题

这个申请锁的算法目前还存在另一个问题,就是不能处理因连接丢失而导致的create操作失败。如前所述,在这种情况下,我们不知道操作是成功还是失败。由于创建一个顺序znode是非幂等操作,所以我们不能简单地重试,因为如果第一次创建已经成功,重试会使我们多出一个永远删不掉的孤儿zriode(至少到客户端会话结束前)。不幸的结果是将会出现死锁。

(2) 解决方案

问题在于,在重新连接之后客户端不能够判断它是否已经创建过子节点。解决方案是在znode的名称中嵌入一个ID,如果客户端出现连接丢失的情况,重新连接之后它便可以对锁节点的所有于节点进行检查,看看是否有子节点的名称中包含其ID。如果有一个子节点的名称包含其ID,它便知道创建操作已经成功,不需要再创建子节点。如果没有子节点的名称中包含其ID,则客户端可以安全地创建一个新的顺序子节点。
客户端会话的ID是一个长整数,并且在ZooKeeper服务中是唯一的,因此非常适合在连接丢失后用于识别客户端。可以通过调用Java ZooKeeper类的getSessionld()方法来获得会话的ID。

在创建短暂顺序znode时应当采用lock-<sessionld>-这样的命名方式,ZooKeeper在其尾部添加顺序号之后,znode的名称会形如lock-<sessionld>-<sequenceNumber>。由于顺序号对于父节点来说是唯一的,但对于子节点名并不唯一,因此采用这样的命名方式可以诖子节点在保持创建顺序的同时能够确定自己的创建者。

3.2.3 不可恢复的异常

如果一个客户端的ZooKeeper会话过期,那么它所创建的短暂znode将会被删除,已持有的锁会被释放,或是放弃了申请锁的位置。使用锁的应用程序应当意识到它已经不再持有锁,应当清理它的状态,然后通过创建并尝试申请一个新的锁对象来重新启动。注意,这个过程是由应用程序控制的,而不是锁,因为锁是不能预知应用程序需要如何清理自己的状态。

四、ZooKeeper实现共享锁

实现正确地实现一个分布式锁是一件棘手的事,因为很难对所有类型的故障都进行正确的解释处理。ZooKeeper带有一个JavaWriteLock,客户端可以很方便地使用它。更多分布式数据结构和协议例如“屏障”(bafrier)、队列和两阶段提交协议。有趣的是它们都是同步协议,即使我们使用异步ZooKeeper基本操作(如通知)来实现它们。使用ZooKeeper可以实现很多不同的分布式数据结构和协议,ZooKeeper网站(http://hadoop.apache.org/zookeeper/)提供了一些用于实现分布式数据结构和协议的伪代码。ZooKeeper本身也带有一些棕准方法的实现,放在安装位置下的recipes目录中。

4.1 场景描述

大家也许都很熟悉了多个线程或者多个进程间的共享锁的实现方式了,但是在分布式场景中我们会面临多个Server之间的锁的问题。

假设有这样一个场景:两台server :serverA,serverB需要在C机器上的/usr/local/a.txt文件上进行写操作,如果两台机器同时写该文件,那么该文件的最终结果可能会产生乱序等问题。最先能想到的是serverA在写文件前告诉ServerB “我要开始写文件了,你先别写”,等待收到ServerB的确认回复后ServerA开始写文件,写完文件后再通知ServerB“我已经写完了”。假设在我们场景中有100台机器呢,中间任意一台机器通信中断了又该如何处理?容错和性能问题呢?要能健壮,稳定,高可用并保持高性能,系统实现的复杂度比较高,从头开发这样的系统代价也很大。幸运的是,我们有了基于googlechubby原理开发的开源的ZooKeeper系统。接下来本文将介绍两种ZooKeeper实现分布式共享锁的方法。

4.2 利用节点名称的唯一性来实现共享锁

ZooKeeper表面上的节点结构是一个和unix文件系统类似的小型的树状的目录结构,ZooKeeper机制规定:同一个目录下只能有一个唯一的文件名。

例如:

我们在Zookeeper目录/test目录下创建,两个客户端创建一个名为lock节点,只有一个能够成功。

(1) 算法思路:利用名称唯一性,加锁操作时,只需要所有客户端一起创建/Leader/lock节点,只有一个创建成功,成功者获得锁。解锁时,只需删除/test/Lock节点,其余客户端再次进入竞争创建节点,直到所有客户端都获得锁。

基于以上机制,利用节点名称唯一性机制的共享锁算法流程如图所示:

4.3 利用顺序节点实现共享锁

首先介绍一下,Zookeeper中有一种节点叫做顺序节点,故名思议,假如我们在/lock/目录下创建节3个点,ZooKeeper集群会按照提起创建的顺序来创建节点,节点分别为/lock/0000000001、/lock/0000000002、/lock/0000000003。

ZooKeeper中还有一种名为临时节点的节点,临时节点由某个客户端创建,当客户端与ZooKeeper集群断开连接,。则该节点自动被删除。

算法思路:对于加锁操作,可以让所有客户端都去/lock目录下创建临时、顺序节点,如果创建的客户端发现自身创建节点序列号是/lock/目录下最小的节点,则获得锁。否则,监视比自己创建节点的序列号小的节点(当前序列在自己前面一个的节点),进入等待。解锁操作,只需要将自身创建的节点删除即可。具体算法流程如下图所示:

4.4 ZooKeeper提供的一个写锁实现

按照ZooKeeper提供的分布式锁的伪代码,实现了一个分布式锁的简单测试代码如下:

(1)分布式锁,实现了Lock接口 DistributedLock.java

package com.concurrent;
 
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
 
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooDefs;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.data.Stat;
 
/**
    DistributedLock lock = null;
    try {
        lock = new DistributedLock("127.0.0.1:2182","test");
        lock.lock();
        //do something...
    } catch (Exception e) {
        e.printStackTrace();
    }
    finally {
        if(lock != null)
            lock.unlock();
    }
 * @author xueliang
 *
 */
public class DistributedLock implements Lock, Watcher{
    private ZooKeeper zk;
    private String root = "/locks";//根
    private String lockName;//竞争资源的标志
    private String waitNode;//等待前一个锁
    private String myZnode;//当前锁
    private CountDownLatch latch;//计数器
    private int sessionTimeout = 30000;
    private List exception = new ArrayList();
     
    /**
     * 创建分布式锁,使用前请确认config配置的zookeeper服务可用
     * @param config 127.0.0.1:2181
     * @param lockName 竞争资源标志,lockName中不能包含单词lock
     */
    public DistributedLock(String config, String lockName){
        this.lockName = lockName;
        // 创建一个与服务器的连接
         try {
            zk = new ZooKeeper(config, sessionTimeout, this);
            Stat stat = zk.exists(root, false);
            if(stat == null){
                // 创建根节点
                zk.create(root, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE,CreateMode.PERSISTENT);
            }
        } catch (IOException e) {
            exception.add(e);
        } catch (KeeperException e) {
            exception.add(e);
        } catch (InterruptedException e) {
            exception.add(e);
        }
    }
 
    /**
     * zookeeper节点的监视器
     */
    public void process(WatchedEvent event) {
        if(this.latch != null) { 
            this.latch.countDown(); 
        }
    }
     
    public void lock() {
        if(exception.size() > 0){
            throw new LockException(exception.get(0));
        }
        try {
            if(this.tryLock()){
                System.out.println("Thread " + Thread.currentThread().getId() + " " +myZnode + " get lock true");
                return;
            }
            else{
                waitForLock(waitNode, sessionTimeout);//等待锁
            }
        } catch (KeeperException e) {
            throw new LockException(e);
        } catch (InterruptedException e) {
            throw new LockException(e);
        }
    }
 
    public boolean tryLock() {
        try {
            String splitStr = "_lock_";
            if(lockName.contains(splitStr))
                throw new LockException("lockName can not contains \\u000B");
            //创建临时子节点
            myZnode = zk.create(root + "/" + lockName + splitStr, new byte[0], 
ZooDefs.Ids.OPEN_ACL_UNSAFE,CreateMode.EPHEMERAL_SEQUENTIAL);
            System.out.println(myZnode + " is created ");
            //取出所有子节点
            List subNodes = zk.getChildren(root, false);
            //取出所有lockName的锁
            List lockObjNodes = new ArrayList();
            for (String node : subNodes) {
                String _node = node.split(splitStr)[0];
                if(_node.equals(lockName)){
                    lockObjNodes.add(node);
                }
            }
            Collections.sort(lockObjNodes);
            System.out.println(myZnode + "==" + lockObjNodes.get(0));
            if(myZnode.equals(root+"/"+lockObjNodes.get(0))){
                //如果是最小的节点,则表示取得锁
                return true;
            }
            //如果不是最小的节点,找到比自己小1的节点
            String subMyZnode = myZnode.substring(myZnode.lastIndexOf("/") + 1);
            waitNode = lockObjNodes.get(Collections.binarySearch(lockObjNodes, subMyZnode) - 1);
        } catch (KeeperException e) {
            throw new LockException(e);
        } catch (InterruptedException e) {
            throw new LockException(e);
        }
        return false;
    }
 
    public boolean tryLock(long time, TimeUnit unit) {
        try {
            if(this.tryLock()){
                return true;
            }
            return waitForLock(waitNode,time);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }
 
    private boolean waitForLock(String lower, long waitTime) throws InterruptedException, KeeperException {
        Stat stat = zk.exists(root + "/" + lower,true);
        //判断比自己小一个数的节点是否存在,如果不存在则无需等待锁,同时注册监听
        if(stat != null){
            System.out.println("Thread " + Thread.currentThread().getId() + " waiting for " + root + "/" + lower);
            this.latch = new CountDownLatch(1);
            this.latch.await(waitTime, TimeUnit.MILLISECONDS);
            this.latch = null;
        }
        return true;
    }
 
    public void unlock() {
        try {
            System.out.println("unlock " + myZnode);
            zk.delete(myZnode,-1);
            myZnode = null;
            zk.close();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (KeeperException e) {
            e.printStackTrace();
        }
    }
 
    public void lockInterruptibly() throws InterruptedException {
        this.lock();
    }
 
    public Condition newCondition() {
        return null;
    }
     
    public class LockException extends RuntimeException {
        private static final long serialVersionUID = 1L;
        public LockException(String e){
            super(e);
        }
        public LockException(Exception e){
            super(e);
        }
    }
 
}

(2)并发测试工具 ConcurrentTest.java

package com.concurrent;
 
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
 
/**
  ConcurrentTask[] task = new ConcurrentTask[5];
  for(int i=0;i<task.length;i++){
       task[i] = new ConcurrentTask(){
            public void run() {
                System.out.println("==============");
                 
            }};
  }
  new ConcurrentTest(task);
 * @author xueliang
 *
 */
public class ConcurrentTest {
    private CountDownLatch startSignal = new CountDownLatch(1);//开始阀门
    private CountDownLatch doneSignal = null;//结束阀门
    private CopyOnWriteArrayList<Long> list = new CopyOnWriteArrayList<Long>();
    private AtomicInteger err = new AtomicInteger();//原子递增
    private ConcurrentTask[] task = null;
     
    public ConcurrentTest(ConcurrentTask... task){
        this.task = task;
        if(task == null){
            System.out.println("task can not null");
            System.exit(1);
        }
        doneSignal = new CountDownLatch(task.length);
        start();
    }
    /**
     * @param args
     * @throws ClassNotFoundException
     */
    private void start(){
        //创建线程,并将所有线程等待在阀门处
        createThread();
        //打开阀门
        startSignal.countDown();//递减锁存器的计数,如果计数到达零,则释放所有等待的线程
        try {
            doneSignal.await();//等待所有线程都执行完毕
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //计算执行时间
        getExeTime();
    }
    /**
     * 初始化所有线程,并在阀门处等待
     */
    private void createThread() {
        long len = doneSignal.getCount();
        for (int i = 0; i < len; i++) {
            final int j = i;
            new Thread(new Runnable(){
                public void run() {
                    try {
                        startSignal.await();//使当前线程在锁存器倒计数至零之前一直等待
                        long start = System.currentTimeMillis();
                        task[j].run();
                        long end = (System.currentTimeMillis() - start);
                        list.add(end);
                    } catch (Exception e) {
                        err.getAndIncrement();//相当于err++
                    }
                    doneSignal.countDown();
                }
            }).start();
        }
    }
    /**
     * 计算平均响应时间
     */
    private void getExeTime() {
        int size = list.size();
        List<Long> _list = new ArrayList<Long>(size);
        _list.addAll(list);
        Collections.sort(_list);
        long min = _list.get(0);
        long max = _list.get(size-1);
        long sum = 0L;
        for (Long t : _list) {
            sum += t;
        }
        long avg = sum/size;
        System.out.println("min: " + min);
        System.out.println("max: " + max);
        System.out.println("avg: " + avg);
        System.out.println("err: " + err.get());
    }
     
    public interface ConcurrentTask {
        void run();
    }
 
}

(3)测试 ZkTest.java

package com.concurrent;

import com.concurrent.ConcurrentTest.ConcurrentTask;

public class ZkTest {
public static void main(String[] args) {
Runnable task1 = new Runnable(){
public void run() {
DistributedLock lock = null;
try {
lock = new DistributedLock("127.0.0.1:2182","test1");
//lock = new DistributedLock("127.0.0.1:2182","test2");
lock.lock();
Thread.sleep(3000);
System.out.println("===Thread " + Thread.currentThread().getId() + " running");
} catch (Exception e) {
e.printStackTrace();
}
finally {
if(lock != null)
lock.unlock();
}

}

};
new Thread(task1).start();
try {
Thread.sleep(1000);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
ConcurrentTask[] tasks = new ConcurrentTask[60];
for(int i=0;i<tasks.length;i++){
ConcurrentTask task3 = new ConcurrentTask(){
public void run() {
DistributedLock lock = null;
try {
lock = new DistributedLock("127.0.0.1:2183","test2");
lock.lock();
System.out.println("Thread " + Thread.currentThread().getId() + " running");
} catch (Exception e) {
e.printStackTrace();
}
finally {
lock.unlock();
}

}
};
tasks[i] = task3;
}
new ConcurrentTest(tasks);
}
}

4.5 更多分布式数据结构和协议

使用ZooKeeper可以实现很多不同的分布式数据结构和协议,例如“屏障”(bafrier)、队列和两阶段提交协议。有趣的是它们都是同步协议,即使我们使用异步ZooKeeper基本操作(如通知)来实现它们。

ZooKeeper网站(http://hadoop.apache.org/zookeeper)提供了一些用于实现分布式数据结构和协议的伪代码。ZooKeeper本身也带有一些棕准方法的实现,放在安装位置下的recipes目录中。

五、BooKeeper

5.1 BooKeeper概述

BooKeeper具有副本功能,目的是提供可靠的日志记录。在BooKeeper中,服务器被称为账本(Bookies),在账本之中有不同的账户(Ledgers),每一个账户由一条条记录(Entry)组成。如果使用普通的磁盘存储日志数据,那么日志数据可能遭到破坏,当磁盘发生故障的时候,日志也可能被丢失。BooKeeper为每一份日志提供了分布式的存储,并采用了大多数(quorum,相对于全体)的概念。也就是说,只要集群中的大多数机器可用,那么该日志一直有效。

BooKeeper通过客户端进行操作,客户端可以对BooKeeper进行添加账户、打开账户、添加账户记录、读取账户记录等操作。另外,BooKeeper的服务依赖于ZooKeeper,可以说BooKeeper依赖于ZooKeeper的一致性及其分布式特点,在其之上提供另外一种可靠性服务。BooKeeper的架构如下图所示:

5.2 BooKeeper角色

从上图中可以看出,BooKeeper中总共包含四类角色:

① 账本:Bookies

② 账户:Ledger

③ 客户端:Client

④ 元数据及存储服务:Metadata Storage Service

下面简单介绍这四类角色的功能:

(1) 账本 BooKies

账本是BooKeeper的存储服务器,他存储的是一个个的账本,可以将账本理解为一个个节点。在一个BooKeeper系统中存在多个账本(节点),每个账户被不同的账本所存储。若要写一条记录到指定的账户中,该记录将被写到维护该账户所有帐本节点中。为了提高系统的性能,这条记录并不是真正的被写入到所有的节点中,而是选择集群的一个大多数集进行存储。该系统独有的特性,使得BooKeeper系统有良好的扩展性。即,我们可以通过简单的添加机器节点的方法提高系统容量。☆☆

(2) 账户 Ledger

账户中存储的是一系列记录,每一条记录包含一定的字段。记录通过写操作一次性写入,只能进行附加操作不能进行修改。每条记录包含如下字段:

当满足下列两个条件时,某条记录才被认为是存储成功:

① 之前所记录的数据被账本节点的大多数集所存储。

② 该记录被账本节点的大多数集所存储。

(3) 客户端 BooKeeper Client

客户端通常与BooKeeper应用程序进行交互,它允许应用程序在系统上进行操作,包括创建账户,写账户等。

(4) 元数据存储服务 Metadata Storage Service

元数据信息存储在ZooKeeper集群当中,它存储关于账户和账本的信息。例如,账本由集群中的哪些节点进行维护,账户由哪个账本进行维护。应用程序在使用账本的时候,首先要创建一个账户。在创建账户时,系统首先将该账本的Metadata信息写入到ZooKeeper中。每一个账户在某一时刻只能有一个写实例(分布式锁)。在其他实例进行读操作之前首先需要将写实例关闭。如果写操作实例由于故障未能正常关闭,那么下一个尝试打开账户的实例将需要首先对其进行恢复,并正确关闭写操作。在进行写操作的同时需要将最后一次的写记录存储到ZooKeeper中,因此恢复程序仅需要在ZooKeeper中查看该账户所对应的最后一条写记录,然后将其正确的写入到账户中,再在正确关闭写操作。在BooKeeper中该恢复程序有系统自动执行不需要用户参与。


https://my.oschina.net/xianggao/blog/532010

http://newliferen.github.io/2015/07/27/ZooKeeper%E5%BA%94%E7%94%A8%E4%B9%8B%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81/

http://www.cnblogs.com/viviman/archive/2013/03/11/2954118.html

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值