Zookeeper
官网:http://zookeeper.apache.org/
Zookeeper 分布式服务框架是 Apache Hadoop 的一个子项目,它主要是用来解决分布式应用中经常遇到的一些数据管理问题,如:统一命名服务、状态同步服务、集群管理、分布式应用配置项的管理等。本文将从使用者角度详细介绍 Zookeeper 的安装和配置文件中各个配置项的意义,以及分析 Zookeeper 的典型的应用场景(配置文件的管理、集群管理、同步锁、Leader 选举、队列管理等),用 Java 实现它们并给出示例代码。
ZooKeeper 是一个为分布式应用所设计的分布的、开源的协调服务。分布式的应用可以建立在同步、配置管理、分组和命名等服务的更高级别的实现的基础之上。 ZooKeeper 意欲设计一个易于编程的环境,它的文件系统使用我们所熟悉的目录树结构。 ZooKeeper 使用Java 所编写,但是支持 Java 和 C 两种编程语言。
众所周知,协调服务非常容易出错,但是却很难恢复正常,例如,协调服务很容易处于竞态以至于出现死锁。我们设计 ZooKeeper 的目的是为了减轻分布式应用程序所承担的协调任务。
ZooKeeper是一个高可用、高可靠的协同工作系统,分布式程序可以用ZooKeeper保存并更新关键共享状态。
zookeeper是针对分布式应用的分布式协作服务,它的目的就是为了减轻分布式应用从头开发协作服务的负担。
Zookeeper作为一个分布式的服务框架,主要用来解决分布式集群中应用系统的一致性问题,它能提供基于类似于文件系统的目录节点树方式的数据存储,但是 Zookeeper 并不是用来专门存储数据的,它的作用主要是用来维护和监控你存储的数据的状态变化。通过监控这些数据状态的变化,从而可以达到基于数据的集群管理。
ZooKeeper是一个分布式的,开放源码的分布式应用程序协调服务,它包含一个简单的原语集,分布式应用程序可以基于它实现同步服务,配置维护 和命名服务等。Zookeeper是hadoop的一个子项目,其发展历程无需赘述。在分布式应用中,由于工程师不能很好地使用锁机制,以及基于消息的协 调机制不适合在某些应用中使用,因此需要有一种可靠的、可扩展的、分布式的、可配置的协调机制来统一系统的状态。Zookeeper的目的就在于此。
Zookeeper中的角色主要有以下三类,如下表所示:
系统模型如图所示:
1. 所有的Server都保存有一份目前zookeeper系统状态的备份;
2. 在zookeeper启动的时候,会自动选取一个Server作为Leader,其余的Server都是Follower;选举出leader 的目的是为了可以在分布式的环境中保证数据的一致性。在接下来的工作中这个被选举出来的Leader死了,而剩下的Zookeeper服务器会知道这个Leader死掉了,在活着的Zookeeper集群中会继续选出一个Leader,选举出leader的目的是为了可以在分布式的环境中保证数据的一致性。
3. 作为Follower的Server服务于Client,接受Client的请求,并把Client的请求转交给Leader,由Leader提交请求。
4. Client只与单个的zookeeper服务器连接。Client维护一个持久TCP连接,通过其发送请求, 获取响应和事件,并发送心跳信息。接受这个连接的 ZooKeeper服务器会为这个客户端建立一个会话。如果Client到Server的TCP连接中断,Client将会连接到另外一个Server。当这个客户端连接到另外的服务器时,这个会话会被新的服务器重新建立。
5. zookeeper集群的可靠性是我们使用它的原因,只要不超过半数的服务器当机(如果正常服务的服务器数目不足一半,那么原有的机群将可能被划分成两个信息无法一致的zookeeper服务),该服务就能正常运行。
1.最终一致性:client不论连接到哪个Server,展示给它都是同一个视图,这是zookeeper最重要的性能。
2 .可靠性:具有简单、健壮、良好的性能,如果消息m被到一台服务器接受,那么它将被所有的服务器接受。
3 .实时性:Zookeeper保证客户端将在一个时间间隔范围内获得服务器的更新信息,或者服务器失效的信息。但由于网络延时等原因,Zookeeper不能保证两个客户端能同时得到刚更新的数据,如果需要最新数据,应该在读数据之前调用sync()接口。
4 .等待无关(wait-free):慢的或者失效的client不得干预快速的client的请求,使得每个client都能有效的等待。
5.原子性:更新只能成功或者失败,没有中间状态。
6 .顺序性:包括全局有序和偏序两种:全局有序是指如果在一台服务器上消息a在消息b前发布,则在所有Server上消息a都将在消息b前被发布;偏序是指如果一个消息b在消息a后被同一个发送者发布,a必将排在b前面。
Zookeeper的核心是原子广播,这个机制保证了各个Server之间的同步。实现这个机制的协议叫做Zab协议。Zab协议有两种模式,它们分别是恢复模式(选主)和广播模式(同步)。当服务启动或者在领导者崩溃后,Zab就进入了恢复模式,当领导者被选举出来,且大多数Server 完成了和leader的状态同步以后,恢复模式就结束了。状态同步保证了leader和Server具有相同的系统状态。
为了保证事务的顺序一致性,zookeeper采用了递增的事务id号(zxid)来标识事务。所有的提议(proposal)都在被提出 的时候加上了zxid。实现中zxid是一个64位的数字,它高32位是epoch用来标识leader关系是否改变,每次一个leader被选出来,它 都会有一个新的epoch,标识当前属于那个leader的统治时期。低32位用于递增计数。
每个Server在工作过程中有三种状态:
l LOOKING:当前Server不知道leader是谁,正在搜寻
l LEADING:当前Server即为选举出来的leader
l FOLLOWING:leader已经选举出来,当前Server与之同步
当leader崩溃或者leader失去大多数的follower,这时候zk进入恢复模式,恢复模式需要重新选举出一个新的 leader,让所有的Server都恢复到一个正确的状态。Zk的选举算法有两种:一种是基于basic paxos实现的,另外一种是基于fast paxos算法实现的。系统默认的选举算法为fast paxos。先介绍basicpaxos流程:
1 .选举线程由当前Server发起选举的线程担任,其主要功能是对投票结果进行统计,并选出推荐的Server;
2 .选举线程首先向所有Server发起一次询问(包括自己);
3.选举线程收到回复后,验证是否是自己发起的询问(验证zxid是否一致),然后获取对方的id(myid),并存储到当前询问对象列表中,最后获取对方提议的leader相关信息(id,zxid),并将这些信息存储到当次选举的投票记录表中;
4. 收到所有Server回复以后,就计算出zxid最大的那个Server,并将这个Server相关信息设置成下一次要投票的Server;
5. 线程将当前zxid最大的Server设置为当前Server要推荐的Leader,如果此时获胜的Server获得n/2 + 1的Server票数, 设置当前推荐的leader为获胜的Server,将根据获胜的Server相关信息设置自己的状态,否则,继续这个过程,直到leader被选举出来。
通过流程分析我们可以得出:要使Leader获得多数Server的支持,则Server总数必须是奇数2n+1,且存活的Server的数目不得少于n+1.
每个Server启动后都会重复以上流程。在恢复模式下,如果是刚从崩溃状态恢复的或者刚启动的server还会从磁盘快照中恢复数据和会话信息,zk会记录事务日志并定期进行快照,方便在恢复时进行状态恢复。选主的具体流程图如下所示:
fast paxos流程是在选举过程中,某Server首先向所有Server提议自己要成为leader,当其它Server收到提议以后,解决epoch和 zxid的冲突,并接受对方的提议,然后向对方发送接受提议完成的消息,重复这个流程,最后一定能选举出Leader。其流程图如下所示:
选完leader以后,zk就进入状态同步过程。
1. leader等待server连接;
2 .Follower连接leader,将最大的zxid发送给leader;
3 .Leader根据follower的zxid确定同步点;
4 .完成同步后通知follower 已经成为uptodate状态;
5 .Follower收到uptodate消息后,又可以重新接受client的请求进行服务了。
流程图如下所示:
Leader主要有三个功能:
1 .恢复数据;
2 .维持与Learner的心跳,接收Learner请求并判断Learner的请求消息类型;
3 .Learner的消息类型主要有PING消息、REQUEST消息、ACK消息、REVALIDATE消息,根据不同的消息类型,进行不同的处理。
PING消息是指Learner的心跳信息;REQUEST消息是Follower发送的提议信息,包括写请求及同步请求;ACK消息是Follower的对提议的回复,超过半数的Follower通过,则commit该提议;REVALIDATE消息是用来延长SESSION有效时间。
Leader的工作流程简图如下所示,在实际实现中,流程要比下图复杂得多,启动了三个线程来实现功能。
Follower主要有四个功能:
1. 向Leader发送请求(PING消息、REQUEST消息、ACK消息、REVALIDATE消息);
2 .接收Leader消息并进行处理;
3 .接收Client的请求,如果为写请求,发送给Leader进行投票;
4 .返回Client结果。
Follower的消息循环处理如下几种来自Leader的消息:
1 .PING消息: 心跳消息;
2 .PROPOSAL消息:Leader发起的提案,要求Follower投票;
3 .COMMIT消息:服务器端最新一次提案的信息;
4 .UPTODATE消息:表明同步完成;
5 .REVALIDATE消息:根据Leader的REVALIDATE结果,关闭待revalidate的session还是允许其接受消息;
6 .SYNC消息:返回SYNC结果到客户端,这个消息最初由客户端发起,用来强制得到最新的更新。
Follower的工作流程简图如下所示,在实际实现中,Follower是通过5个线程来实现功能的。
对于observer的流程不再叙述,observer流程和Follower的唯一不同的地方就是observer不会参加leader发起的投票。
Zookeeper 会维护一个具有层次关系的数据结构,它非常类似于一个标准的文件系统,如下图
Zookeeper 这种数据结构有如下这些特点:
ZooKeeper 的节点是通过像树一样的结构来进行维护的,并且每一个节点通过路径来标示以及访问。除此之外,每一个节点还拥有自身的一些信息,包括:数据、数据长度、创 建时间、修改时间等等。从这样一类既含有数据,又作为路径表标示的节点的特点中,可以看出,ZooKeeper的节点既可以被看做是一个文件,又可以被看 做是一个目录,它同时具有二者的特点。为了便于表达,今后我们将使用Znode来表示所讨论的ZooKeeper节点。
具体地 说,Znode维护着数据、ACL(access control list,访问控制列表)、时间戳等交换版本号等数据结构,它通过对这些数据的管理来让缓存生效并且令协调更新。每当Znode中的数据更新后它所维护的 版本号将增加,这非常类似于数据库中计数器时间戳的操作方式。
另外Znode还具有原子性操作的特点:命名空间中,每一个Znode的数据将被原子地读写。读操作将读取与Znode相关的所有数据,写操作将替换掉所有的数据。除此之外,每一个节点都有一个访问控制列表,这个访问控制列表规定了用户操作的权限。
ZooKeeper支持watch(观察)的概念。客户端可以在每个znode结点上设置一个观察。如果被观察服务端的znode结点有变更,那么watch就会被触发,这 个watch所属的客户端将接收到一个通知包被告知结点已经发生变化。若客户端和所连接的ZooKeeper服务器断开连接时,其他客户端也会收到一个通知,也就说一个Zookeeper服务器端可以对于多个客户端,当然也可以多个Zookeeper服务器端可以对于多个客户端,如图所示:
ZooKeeper的安装模式分为三种,分别为:单机模式(stand-alone)、集群模式和集群伪分布模式。ZooKeeper 单机模式的安装相对比较简单,如果第一次接触ZooKeeper的话,建议安装ZooKeeper单机模式或者集群伪分布模式。
1)单机模式
首先,从Apache官方网站下载一个ZooKeeper 的最近稳定版本。 http://hadoop.apache.org/zookeeper/releases.html
作为国内用户来说,选择最近的的源文件服务器所在地,能够节省不少的时间。 http://labs.renren.com/apache-mirror//hadoop/zookeeper/
ZooKeeper 要求 JAVA 的环境才能运行,并且需要 JAVA6 以上的版本,可以从 SUN 官网上下载,并对 JAVA 环境变量进行设置。除此之外,为了今后操作的方便,我们需要对 ZooKeeper 的环境变量进行配置,方法如下,在 /etc/profile 文件中加入如下的内容:
#Set ZooKeeper Enviroment
exportZOOKEEPER_HOME=/root/hadoop-0.20.2/zookeeper-3.3.1
exportPATH=$PATH:$ZOOKEEPER_HOME/bin:$ZOOKEEPER_HOME/conf
ZooKeeper 服务器包含在单个 JAR 文件中,安装此服务需要用户创建一个配置文档,并对其进行设置。我们在 ZooKeeper-*.*.* 目录(我们以当前 ZooKeeper 的最新版 3.3.1 为例,故此下面的“ ZooKeeper-*.*.* ”都将写为“ ZooKeeper-3.3.1” )的 conf 文件夹下创建一个 zoo.cfg 文件,它包含如下的内容:
tickTime=2000
dataDir=/var/zookeeper
clientPort=2181
在这个文件中,我们需要指定 dataDir 的值,它指向了一个目录,这个目录在开始的时候需要为空。下面是每个参数的含义:
l tickTime :基本事件单元,以毫秒为单位。它用来指示心跳,最小的 session 过期时间为两倍的 tickTime. 。
l dataDir :存储内存中数据库快照的位置,如果不设置参数,更新事务日志将被存储到默认位置。
l clientPort :监听客户端连接的端口
使用单机模式时用户需要注意:这种配置方式下没有 ZooKeeper 副本,所以如果 ZooKeeper 服务器出现故障, ZooKeeper 服务将会停止。以下代码清单 A 是我们的根据自身情况所设置的 zookeeper 配置文档: zoo.cfg
代码清单 A : zoo.cfg
# Thenumber of milliseconds of each tick
tickTime=2000
# thedirectory where the snapshot is stored.
dataDir=/root/hadoop-0.20.2/zookeeper-3.3.1/snapshot/data
# theport at which the clients will connect
clientPort=2181
2)集群模式
为了获得可靠的 ZooKeeper 服务,用户应该在一个集群上部署 ZooKeeper 。只要集群上大多数的 ZooKeeper 服务启动了,那么总的 ZooKeeper 服务将是可用的。另外,最好使用奇数台机器。 如果 zookeeper 拥有 5 台机器,那么它就能处理 2 台机器的故障了。 之后的操作和单机模式的安装类似,我们同样需要对 JAVA 环境进行设置,下载最新的 ZooKeeper 稳定版本并配置相应的环境变量。不同之处在于每台机器上 conf/zoo.cfg 配置文件的参数设置,参考下面的配置:
tickTime=2000
dataDir=/var/zookeeper/
clientPort=2181
initLimit=5
syncLimit=2
server.1=zoo1:2888:3888
server.2=zoo2:2888:3888
server.3=zoo3:2888:3888
“ server.id=host:port:port. ”指示了不同的 ZooKeeper 服务器的自身标识,作为集群的一部分的机器应该知道 ensemble 中的其它机器。用户可以从“ server.id=host:port:port. ”中读取相关的信息。 在服务器的 data ( dataDir 参数所指定的目录)目录下创建一个文件名为 myid 的文件,这个文件中仅含有一行的内容,指定的是自身的 id 值。比如,服务器“ 1 ”应该在 myid 文件中写入“ 1 ”。这个 id 值必须是 ensemble 中唯一的,且大小在 1 到 255 之间。这一行配置中,第一个端口( port )是从( follower )机器连接到主( leader )机器的端口,第二个端口是用来进行 leader 选举的端口。在这个例子中,每台机器使用三个端口,分别是: clientPort , 2181 ; port , 2888 ; port , 3888 。我们在拥有三台机器的 Hadoop 集群上测试使用 ZooKeeper 服务,下面代码清单 B 是我们根据自身情况所设置的 ZooKeeper 配置文档:
代码清单 B : zoo.cfg
# Thenumber of milliseconds of each tick
tickTime=2000
# Thenumber of ticks that the initial
#synchronization phase can take
initLimit=10
# Thenumber of ticks that can pass between
#sending a request and getting an acknowledgement
syncLimit=5
# thedirectory where the snapshot is stored.
dataDir=/root/hadoop-0.20.2/zookeeper-3.3.1/snapshot/d1
# theport at which the clients will connect
clientPort=2181
server.1=IP1:2887:3887
server.2=IP2:2888:3888
server.3=IP3:2889:3889
清单中的 IP 分别对应的配置分布式 ZooKeeper 的 IP 地址。当然,也可以通过机器名访问 zookeeper ,但是需要在 ubuntu 的 hosts 环境中进行设置。读者可以查阅 Ubuntu 以及 Linux 的相关资料进行设置。
3)集群伪分布
简单来说,集群伪分布模式就是在单机下模拟集群的ZooKeeper服务。那么,如何对配置 ZooKeeper 的集群伪分布模式呢?其实很简单,在 zookeeper 配置文档中, clientPort 参数用来设置客户端连接 zookeeper 的端口。 server.1=IP1:2887:3887 中, IP1 指示的是组成 ZooKeeper 服务的机器 IP 地址, 2887 为用来进行 leader 选举的端口, 3887 为组成 ZooKeeper 服务的机器之间通信的端口。集群伪分布模式我们使用每个配置文档模拟一台机器,也就是说,需要在单台机器上运行多个 zookeeper 实例。但是,我们必须要保证各个配置文档的 clientPort 不能冲突。
下面是我们所配置的集群伪分布模式,通过 zoo1.cfg , zoo2.cfg , zoo3.cfg 模拟了三台机器的 ZooKeeper 集群。详见代码清单 C :
代码清单C : zoo1.cfg :
# Thenumber of milliseconds of each tick
tickTime=2000
# Thenumber of ticks that the initial
#synchronization phase can take
initLimit=10
# Thenumber of ticks that can pass between
#sending a request and getting an acknowledgement
syncLimit=5
# thedirectory where the snapshot is stored.
dataDir=/root/hadoop-0.20.2/zookeeper-3.3.1/d_1
# theport at which the clients will connect
clientPort=2181
server.1=localhost:2887:3887
server.2=localhost:2888:3888
server.3=localhost:2889:3889
zoo2.cfg :
#The number of milliseconds of each tick
tickTime=2000
# Thenumber of ticks that the initial
#synchronization phase can take
initLimit=10
# Thenumber of ticks that can pass between
#sending a request and getting an acknowledgement
syncLimit=5
# thedirectory where the snapshot is stored.
dataDir=/root/hadoop-0.20.2/zookeeper-3.3.1/d_2
# theport at which the clients will connect
clientPort=2182
#thelocation of the log file
dataLogDir=/root/hadoop-0.20.2/zookeeper-3.3.1/logs
server.1=localhost:2887:3887
server.2=localhost:2888:3888
server.3=localhost:2889:3889
zoo3.cfg :
# Thenumber of milliseconds of each tick
tickTime=2000
# Thenumber of ticks that the initial
#synchronization phase can take
initLimit=10
# Thenumber of ticks that can pass between
#sending a request and getting an acknowledgement
syncLimit=5
# thedirectory where the snapshot is stored.
dataDir=/root/hadoop-0.20.2/zookeeper-3.3.1/d_2
# theport at which the clients will connect
clientPort=2183
#thelocation of the log file
dataLogDir=/root/hadoop-0.20.2/zookeeper-3.3.1/logs
server.1=localhost:2887:3887
server.2=localhost:2888:3888
server.3=localhost:2889:3889
从上述三个代码清单中可以看到,除了 clientPort 不同之外, dataDir 也不同。另外,不要忘记在 dataDir 所对应的目录中创建 myid 文件来指定对应的 zookeeper 服务器实例。
ZooKeeper 的功能特性通过 ZooKeeper 配置文件来进行控制管理( zoo.cfg 配置文件)。 ZooKeeper 这样的设计其实是有它自身的原因的。通过前面对 ZooKeeper 的配置可以看出,对 ZooKeeper 集群进行配置的时候,它的配置文档是完全相同的(对于集群伪分布模式来说,只有很少的部分是不同的)。这样的配置方使得在部署 ZooKeeper 服务的时候非常地方便。另外,如果服务器使用不同的配置文件,必须要确保不同配置文件中的服务器列表相匹配。
在设置 ZooKeeper 配置文档的时候,某些参数是可选的,但是某些参数是必须的。这些必须的参数就构成了 ZooKeeper 配置文档的最低配置要求。
下面是在最低配置要求中必须配置的参数:
1 )最低配置
clientPort :监听客户端连接的端口;
dataDir :存储内存中数据库快照的位置; 注意 应该谨慎地选择日志存放的位置,使用专用的日志存储设备能够大大地提高系统的性能,如果将日志存储在比较繁忙的存储设备上,那么将会在很大程度上影响系统的性能。
tickTime :基本事件单元,以毫秒为单位。它用来控制心跳和超时,默认情况下最小的会话超时时间为两倍的 tickTime 。
2 )高级配置
下面是高级配置要求中可选的配置参数,用户可以使用下面的参数来更好地规定 ZooKeeper 的行为:
dataLogDir :这个操作将管理机器把事务日志写入到“ dataLogDir ”所指定的目录,而不是“ dataDir ”所指定的目录。这将允许使用一个专用的日志设备并且帮助我们避免日志和快照之间的竞争。配置如下:
#thelocation of the log file
dataLogDir=/root/hadoop-0.20.2/zookeeper-3.3.1/log/data_log
maxClientCnxns
这个操作将限制连接到 ZooKeeper 的客户端的数量,限制并发连接的数量,它通过 IP 来区分不同的客户端。此配置选项可以用来阻止某些类别的 Dos 攻击。将它设置为 0 或者忽略而不进行设置将会取消对并发连接的限制。
例如,此时我们将 maxClientCnxns 的值设置为 1 ,如下所示:
#setmaxClientCnxns
maxClientCnxns=1
启动 ZooKeeper 之后,首先用一个客户端连接到 ZooKeeper 服务器之上。然后,当第二个客户端尝试对 ZooKeeper 进行连接,或者某些隐式的对客户端的连接操作,将会触发 ZooKeeper 的上述配置。系统会提示相关信息,如下图 1 所示:
图 1 : ZooKeepermaxClientCnxns 异常
minSessionTimeout和 maxSessionTimeout
最小的会话超时时间以及最大的会话超时时间。其中,最小的会话超时时间默认情况下为 2 倍的 tickTme 时间,最大的会话超时时间默认情况下为 20 倍的会话超时时间。在启动时,系统会显示相应信息,见下图 2 所示,默认会话超时时间:
图2 :默认会话超时时间
从上图中可以看书, minSessionTimeout 以及 maxSessionTimeout 的值均为 -1 ,现在我们来设置系统的最小会话超时时间以及最大会话超时时间,如下所示:
#setminSessionTimeout
minSessionTimeout=1000
#setmaxSessionTImeout
maxSessionTimeout=10000
在配置 minSessionTmeout 以及 maxSessionTimeout 的值的时候需要注意,如果将此值设置的太小的话,那么会话很可能刚刚建立便由于超时而不得不退出。一般情况下,不能将此值设置的比 tickTime 的值还小。
3 )集群配置
initLimit:此配置表示,允许 follower (相对于 leader 而言的“客户端”)连接并同步到 leader 的初始化连接时间,它以 tickTime 的倍数来表示。当超过设置倍数的 tickTime 时间,则连接失败。
syncLimit :此配置表示, leader 与 follower 之间发送消息,请求和应答时间长度。如果 follower 在设置的时间内不能与 leader 进行通信,那么此 follower 将被丢弃。
1)单机模式
用户可以通过下面的命令来启动 ZooKeeper 服务:
zkServer.shstart
这个命令默认情况下执行 ZooKeeper 的 conf 文件夹下的 zoo.cfg 配置文件。当运行成功用户会看到类似如下的提示界面:
root@ubuntu:~#zkServer.sh start
JMXenabled by default
Usingconfig: /root/hadoop-0.20.2/zookeeper-3.3.1/bin/../conf/zoo.cfg
Startingzookeeper ...
STARTED
... ...
2011-01-1910:04:42,300 - WARN [main:QuorumPeerMain@105] - Either no config or noquorum defined in config, running in standalone mode
... ...
2011-01-1910:04:42,419 - INFO [main:ZooKeeperServer@660] - tickTime set to 2000
2011-01-1910:04:42,419 - INFO [main:ZooKeeperServer@669] - minSessionTimeout set to-1
2011-01-1910:04:42,419 - INFO [main:ZooKeeperServer@678] - maxSessionTimeout set to-1
2011-01-1910:04:42,560 - INFO [main:NIOServerCnxn$Factory@143] - binding to port0.0.0.0/0.0.0.0:2181
2011-01-1910:04:42,806 - INFO [main:FileSnap@82] - Reading snapshot/root/hadoop-0.20.2/zookeeper-3.3.1/data/version-2/snapshot.200000036
2011-01-1910:04:42,927 - INFO [main:FileSnap@82] - Reading snapshot/root/hadoop-0.20.2/zookeeper-3.3.1/data/version-2/snapshot.200000036
2011-01-1910:04:42,950 - INFO [main:FileTxnSnapLog@208] - Snapshotting: 400000058
从上面可以看出,运行成功后,系统会列出 ZooKeeper 运行的相关环境配置信息。
2)集群模式
集群模式下需要用户在每台 ZooKeeper 机器上运行第一部分的命令,这里不再赘述。
3)集群伪分布模式
在集群伪分布模式下,我们只有一台机器,但是要运行三个 ZooKeeper 服务实例。此时,如果再使用上述命令式肯定行不通的。这里,我们通过下面三条命能够令来运行 ZooKeeper系列之三:ZooKeeper的安装 中 我们配置的 ZooKeeper 服务。如下所示:
zkServer.shstart zoo1.cfg
zkServer.shstart zoo2.cfg
zkServer.shstart zoo3.cfg
在运行完第一条命令之后,读者将会发现一些系统错误提示,如下图 1 所示:
图 1 :集群伪分布异常提示
产生如上图所示的异常信息是由于 ZooKeeper 服务的每个实例都拥有全局的配置信息,它们在启动的时候需要随时地进行 Leader 选举操作(此部分内容下面将会详细讲述)。此时第一个启动的 Zookeeper 需要和另外两个 ZooKeeper 实例进行通信。但是,另外两个 ZooKeeper 实例还没有启动起来,因此将会产生上述所示的异常信息。
我们直接将其忽略即可,因为当把图示中的“ 2 号”和“ 3 号” ZooKeeper 实例启动起来之后,相应的异常信息就回自然而然地消失。
ZooKeeper 支持某些特定的四字命令字母与其的交互。它们大多是查询命令,用来获取 ZooKeeper 服务的当前状态及相关信息。用户在客户端可以通过 telnet 或 nc 向 ZooKeeper 提交相应的命令。 ZooKeeper 常用四字命令见下表 1 所示:
表 1 : ZooKeeper 四字命令
ZooKeeper 四字命令 |
功能描述 |
conf |
输出相关服务配置的详细信息。 |
cons |
列出所有连接到服务器的客户端的完全的连接 / 会话的详细信息。包括“接受 / 发送”的包数量、会话 id 、操作延迟、最后的操作执行等等信息。 |
dump |
列出未经处理的会话和临时节点。 |
envi |
输出关于服务环境的详细信息(区别于 conf 命令)。 |
reqs |
列出未经处理的请求 |
ruok |
测试服务是否处于正确状态。如果确实如此,那么服务返回“ imok ”,否则不做任何相应。 |
stat |
输出关于性能和连接的客户端的列表。 |
wchs |
列出服务器 watch 的详细信息。 |
wchc |
通过 session 列出服务器 watch 的详细信息,它的输出是一个与 watch 相关的会话的列表。 |
wchp |
通过路径列出服务器 watch 的详细信息。它输出一个与 session 相关的路径。 |
下图 1 是 ZooKeeper 四字命令的简单用例:
图 1 : ZooKeeper 四字命令用例
当启动 ZooKeeper 服务成功之后,输入下述命令,连接到 ZooKeeper 服务:
zkCli.sh–server 10.77.20.23:2181
连接成功后,系统会输出 ZooKeeper 的相关环境以及配置信息,并在屏幕输出“ Welcome to ZooKeeper ”等信息。
输入 help 之后,屏幕会输出可用的 ZooKeeper 命令,如下图 1 所示:
图 1 : ZooKeeper 命令
1 )使用 ls 命令来查看当前 ZooKeeper 中所包含的内容:
[zk:10.77.20.23:2181(CONNECTED) 1] ls /
[zookeeper]
2 )创建一个新的 znode ,使用 create /zk myData 。这个命令创建了一个新的 znode 节点“ zk ”以及与它关联的字符串:
[zk:10.77.20.23:2181(CONNECTED) 2] create /zk myData
Created/zk
3 )再次使用 ls 命令来查看现在 zookeeper 中所包含的内容:
[zk:10.77.20.23:2181(CONNECTED) 3] ls /
[zk,zookeeper]
此时看到, zk 节点已经被创建。
4 )下面我们运行 get 命令来确认第二步中所创建的 znode 是否包含我们所创建的字符串:
[zk:10.77.20.23:2181(CONNECTED) 4] get /zk
myData
Zxid =0x40000000c
time =Tue Jan 18 18:48:39 CST 2011
Zxid =0x40000000c
mtime =Tue Jan 18 18:48:39 CST 2011
pZxid =0x40000000c
cversion= 0
dataVersion= 0
aclVersion= 0
ephemeralOwner= 0x0
dataLength= 6
numChildren= 0
5 )下面我们通过 set 命令来对 zk 所关联的字符串进行设置:
[zk:10.77.20.23:2181(CONNECTED) 5] set /zk shenlan211314
cZxid =0x40000000c
ctime =Tue Jan 18 18:48:39 CST 2011
mZxid =0x40000000d
mtime =Tue Jan 18 18:52:11 CST 2011
pZxid =0x40000000c
cversion= 0
dataVersion= 1
aclVersion= 0
ephemeralOwner= 0x0
dataLength= 13
numChildren= 0
6 )下面我们将刚才创建的 znode 删除:
[zk:10.77.20.23:2181(CONNECTED) 6] delete /zk
7 )最后再次使用 ls 命令查看 ZooKeeper 所包含的内容:
[zk:10.77.20.23:2181(CONNECTED) 7] ls /
[zookeeper]
经过验证, zk 节点已经被删除。
ZooKeeper API 共包含 5 个包,分别为:
org.apache.zookeeper
org.apache.zookeeper.data
org.apache.zookeeper.server
org.apache.zookeeper.server.quorum
org.apache.zookeeper.server.upgrade
客户端要连接Zookeeper 服务器可以通过创建 org.apache.zookeeper. ZooKeeper 的一个实例对象,然后调用这个类提供的接口来和服务器交互。一旦客户端和 ZooKeeper 服务建立起连接, ZooKeeper 系统将会分配给此连接回话一个 ID 值,并且客户端将会周期地向服务器发送心跳来维持会话的连接。只要连接有效,客户端就可以调用 ZooKeeper API 来做相应的处理。
前面说了ZooKeeper 主要是用来维护和监控一个目录节点树中存储的数据的状态,所有我们能够操作 ZooKeeper 的也和操作目录节点树大体一样,如创建一个目录节点,给某个目录节点设置数据,获取某个目录节点的所有子目录节点,给某个目录节点设置权限和监控这个目录节点的状态变化。
org.apache.zookeeper.ZooKeeper 方法列表
方法名 |
方法功能描述 |
String create(String path, byte[] data, List<ACL> acl, CreateMode createMode) |
创建一个给定的目录节点 path, 并给它设置数据,CreateMode 标识有四种形式的目录节点,分别是 PERSISTENT:持久化目录节点,这个目录节点存储的数据不会丢失;PERSISTENT_SEQUENTIAL:顺序自动编号的目录节点,这种目录节点会根据当前已近存在的节点数自动加 1,然后返回给客户端已经成功创建的目录节点名;EPHEMERAL:临时目录节点,一旦创建这个节点的客户端与服务器端口也就是 session 超时,这种节点会被自动删除;EPHEMERAL_SEQUENTIAL:临时自动编号节点 |
Stat exists(String path, boolean watch) |
判断某个 path 是否存在,并设置是否监控这个目录节点,这里的 watcher 是在创建 ZooKeeper 实例时指定的 watcher,exists方法还有一个重载方法,可以指定特定的 watcher |
Stat exists(String path, Watcher watcher) |
重载方法,这里给某个目录节点设置特定的 watcher,Watcher 在 ZooKeeper 是一个核心功能,Watcher 可以监控目录节点的数据变化以及子目录的变化,一旦这些状态发生变化,服务器就会通知所有设置在这个目录节点上的 Watcher,从而每个客户端都很快知道它所关注的目录节点的状态发生变化,而做出相应的反应 |
void delete(String path, int version) |
删除 path 对应的目录节点,version 为 -1 可以匹配任何版本,也就删除了这个目录节点所有数据 |
List<String> getChildren(String path, boolean watch) |
获取指定 path 下的所有子目录节点,同样 getChildren方法也有一个重载方法可以设置特定的 watcher 监控子节点的状态 |
Stat setData(String path, byte[] data, int version) |
给 path 设置数据,可以指定这个数据的版本号,如果 version 为 -1 怎可以匹配任何版本 |
byte[] getData(String path, boolean watch, Stat stat) |
获取这个 path 对应的目录节点存储的数据,数据的版本等信息可以通过 stat 来指定,同时还可以设置是否监控这个目录节点数据的状态 |
void addAuthInfo(String scheme, byte[] auth) |
客户端将自己的授权信息提交给服务器,服务器将根据这个授权信息验证客户端的访问权限。 |
Stat setACL(String path, List<ACL> acl, int version) |
给某个目录节点重新设置访问权限,需要注意的是 Zookeeper 中的目录节点权限不具有传递性,父目录节点的权限不能传递给子目录节点。目录节点 ACL 由两部分组成:perms 和 id。 |
List<ACL> getACL(String path, Stat stat) |
获取某个目录节点的访问权限列表 |
除了以上这些上表中列出的方法之外还有一些重载方法,如都提供了一个回调类的重载方法以及可以设置特定 Watcher 的重载方法,具体的方法可以参考 org.apache.zookeeper. ZooKeeper 类的 API 说明。http://zookeeper.apache.org/doc/r3.4.5/api/index.html
下面给出基本的操作 ZooKeeper 的示例代码,这样就能对 ZooKeeper 有直观的认识。 代码清单 1 :ZooKeeper API 的使用
1. import java.io.IOException;
2.
3.import org.apache.zookeeper.CreateMode;
4.import org.apache.zookeeper.KeeperException;
5.import org.apache.zookeeper.Watcher;
6.import org.apache.zookeeper.ZooDefs.Ids;
7.import org.apache.zookeeper.ZooKeeper;
8.
9.public class demo {
10. // 会话超时时间,设置为与系统默认时间一致
11. private static final int SESSION_TIMEOUT=30000;
12.
13. // 创建 ZooKeeper 实例
14. ZooKeeper zk;
15.
16. // 创建 Watcher 实例
17. Watcher wh=new Watcher(){
18. public void process(org.apache.zookeeper.WatchedEvent event)
19. {
20. System.out.println(event.toString());
21. }
22. };
23.
24. // 初始化 ZooKeeper 实例
25. private void createZKInstance() throws IOException
26. {
27. zk=new ZooKeeper("localhost:2181",demo.SESSION_TIMEOUT,this.wh);
28.
29. }
30.
31. private void ZKOperations() throwsIOException,InterruptedException,KeeperException
32. {
33. System.out.println("/n1. 创建 ZooKeeper 节点 (znode : zoo2, 数据: myData2 ,权限: OPEN_ACL_UNSAFE ,节点类型: Persistent");
34. zk.create("/zoo2","myData2".getBytes(),Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
35.
36. System.out.println("/n2. 查看是否创建成功: ");
37. System.out.println(new String(zk.getData("/zoo2",false,null)));
38.
39. System.out.println("/n3. 修改节点数据 ");
40. zk.setData("/zoo2", "shenlan211314".getBytes(), -1);
41.
42. System.out.println("/n4. 查看是否修改成功: ");
43. System.out.println(new String(zk.getData("/zoo2", false, null)));
44.
45. System.out.println("/n5. 删除节点 ");
46. zk.delete("/zoo2", -1);
47.
48. System.out.println("/n6. 查看节点是否被删除: ");
49. System.out.println(" 节点状态: ["+zk.exists("/zoo2",false)+"]");
50. }
51.
52. private void ZKClose() throws InterruptedException
53. {
54. zk.close();
55. }
56.
57. public static void main(String[] args) throwsIOException,InterruptedException,KeeperException {
58. demo dm=new demo();
59. dm.createZKInstance( );
60. dm.ZKOperations();
61. dm.ZKClose();
62. }
63.}
运行这个程序的时候,需要将storm的jar包
此类包含两个主要的 ZooKeeper 函数,分别为 createZKInstance ()和 ZKOperations ()。其中 createZKInstance ()函数负责对 ZooKeeper 实例 zk 进行初始化。 ZooKeeper 类有两个构造函数,我们这里使用“ ZooKeeper ( String connectString, , int sessionTimeout, , Watcher watcher )”对其进行初始化。因此,我们需要提供初始化所需的,连接字符串信息,会话超时时间,以及一个 watcher 实例。 17 行到 23 行代码,是程序所构造的一个 watcher 实例,它能够输出所发生的事件。
ZKOperations ()函数是我们所定义的对节点的一系列操作。它包括:创建 ZooKeeper 节点( 33 行到 34 行代码)、查看节点( 36 行到 37 行代码)、修改节点数据( 39 行到 40 行代码)、查看修改后节点数据( 42 行到 43 行代码)、删除节点( 45 行到 46 行代码)、查看节点是否存在( 48 行到 49 行代码)。另外,需要注意的是:在创建节点的时候,需要提供节点的名称、数据、权限以及节点类型。此外,使用 exists 函数时,如果节点不存在将返回一个 null 值。关于 ZooKeeper API 的更多详细信息,读者可以查看 ZooKeeper 的 API 文档,如下所示:
http://zookeeper.apache.org/doc/r3.4.5/api/index.html
分布式应用中,通常需要有一套完整的命名规则,既能够产生唯一的名称又便于人识别和记住,通常情况下用树形的名称结构是一个理想的选择,树形的名称结构是一个有层次的目录结构,既对人友好又不会重复。说到这里你可能想到了 JNDI,没错Zookeeper 的 Name Service 与 JNDI 能够完成的功能是差不多的,它们都是将有层次的目录结构关联到一定资源上,但是 Zookeeper 的 NameService 更加是广泛意义上的关联,也许你并不需要将名称关联到特定资源上,你可能只需要一个不会重复名称,就像数据库中产生一个唯一的数字主键一样。
Name Service 已经是 Zookeeper 内置的功能,你只要调用 Zookeeper 的 API 就能实现。如调用 create 接口就可以很容易创建一个目录节点
配置的管理在分布式应用环境中很常见,例如同一个应用系统需要多台 PC Server 运行,但是它们运行的应用系统的某些配置项是相同的,如果要修改这些相同的配置项,那么就必须同时修改每台运行这个应用系统的 PC Server,这样非常麻烦而且容易出错。
像这样的配置信息完全可以交给 Zookeeper 来管理,将配置信息保存在 Zookeeper 的某个目录节点中,然后将所有需要修改的应用机器监控配置信息的状态,一旦配置信息发生变化,每台应用机器就会收到 Zookeeper 的通知,然后从 Zookeeper 获取新的配置信息应用到系统中。
Zookeeper能够很容易的实现集群管理的功能,如有多台 Server 组成一个服务集群,那么必须要一个“总管”知道当前集群中每台机器的服务状态,一旦有机器不能提供服务,集群中其它集群必须知道,从而做出调整重新分配服务策略。同样当增加集群的服务能力时,就会增加一台或多台 Server,同样也必须让“总管”知道。
Zookeeper 不仅能够帮你维护当前的集群中机器的服务状态,而且能够帮你选出一个“总管”,让这个总管来管理集群,这就是 Zookeeper 的另一个功能Leader Election。
它们的实现方式都是在 Zookeeper 上创建一个EPHEMERAL 类型的目录节点,然后每个 Server 在它们创建目录节点的父目录节点上调用 getChildren(String path,boolean watch) 方法并设置 watch 为 true,由于是 EPHEMERAL 目录节点,当创建它的 Server 死去,这个目录节点也随之被删除,所以 Children 将会变化,这时 getChildren上的Watch 将会被调用,所以其它 Server 就知道已经有某台 Server 死去了。新增Server 也是同样的原理。
Zookeeper 如何实现 Leader Election,也就是选出一个 Master Server。和前面的一样每台 Server 创建一个EPHEMERAL 目录节点,不同的是它还是一个 SEQUENTIAL 目录节点,所以它是个 EPHEMERAL_SEQUENTIAL 目录节点。之所以它是 EPHEMERAL_SEQUENTIAL 目录节点,是因为我们可以给每台 Server 编号,我们可以选择当前是最小编号的 Server 为 Master,假如这个最小编号的 Server 死去,由于是EPHEMERAL 节点,死去的 Server 对应的节点也被删除,所以当前的节点列表中又出现一个最小编号的节点,我们就选择这个节点为当前 Master。这样就实现了动态选择 Master,避免了传统意义上单 Master 容易出现单点故障的问题。
图 3. 集群管理结构图
这部分的示例代码如下
清单 3.Leader Election 关键代码
void findLeader() throws InterruptedException { byte[] leader = null; try { leader = zk.getData(root + "/leader", true, null); } catch (Exception e) { logger.error(e); } if (leader != null) { following(); } else { String newLeader = null; try { byte[] localhost = InetAddress.getLocalHost().getAddress(); newLeader = zk.create(root + "/leader", localhost, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL); } catch (Exception e) { logger.error(e); } if (newLeader != null) { leading(); } else { mutex.wait(); } } } |
共享锁在同一个进程中很容易实现,但是在跨进程或者在不同 Server 之间就不好实现了。Zookeeper 却很容易实现这个功能,实现方式也是需要获得锁的 Server 创建一个EPHEMERAL_SEQUENTIAL 目录节点,然后调用 getChildren方法获取当前的目录节点列表中最小的目录节点是不是就是自己创建的目录节点,如果正是自己创建的,那么它就获得了这个锁,如果不是那么它就调用 exists(String path,boolean watch) 方法并监控 Zookeeper 上目录节点列表的变化,一直到自己创建的节点是列表中最小编号的目录节点,从而获得锁,释放锁很简单,只要删除前面它自己所创建的目录节点就行了。
图 4. Zookeeper 实现 Locks 的流程图
同步锁的实现代码如下,完整的代码请看附件:
清单 4. 同步锁的关键代码
void getLock() throws KeeperException, InterruptedException{ List<String> list = zk.getChildren(root, false); String[] nodes = list.toArray(new String[list.size()]); Arrays.sort(nodes); if(myZnode.equals(root+"/"+nodes[0])){ doAction(); } else{ waitForLock(nodes[0]); } } void waitForLock(String lower) throws InterruptedException, KeeperException { Stat stat = zk.exists(root + "/" + lower,true); if(stat != null){ mutex.wait(); } else{ getLock(); } } |
Zookeeper可以处理两种类型的队列:
同步队列用 Zookeeper 实现的实现思路如下:
创建一个父目录 /synchronizing,每个成员都监控标志(Set Watch)位目录/synchronizing/start 是否存在,然后每个成员都加入这个队列,加入队列的方式就是创建 /synchronizing/member_i 的临时目录节点,然后每个成员获取 / synchronizing 目录的所有目录节点,也就是 member_i。判断i 的值是否已经是成员的个数,如果小于成员个数等待 /synchronizing/start 的出现,如果已经相等就创建 /synchronizing/start。
用下面的流程图更容易理解:
图 5. 同步队列流程图
同步队列的关键代码如下,完整的代码请看附件:
清单 5. 同步队列
void addQueue() throws KeeperException, InterruptedException{ zk.exists(root + "/start",true); zk.create(root + "/" + name, new byte[0], Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL); synchronized (mutex) { List<String> list = zk.getChildren(root, false); if (list.size() < size) { mutex.wait(); } else { zk.create(root + "/start", new byte[0], Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT); } } } |
当队列没满是进入 wait(),然后会一直等待 Watch 的通知,Watch的代码如下:
public void process(WatchedEvent event) { if(event.getPath().equals(root + "/start") && event.getType() == Event.EventType.NodeCreated){ System.out.println("得到通知"); super.process(event); doAction(); } } |
FIFO 队列用 Zookeeper 实现思路如下:
实现的思路也非常简单,就是在特定的目录下创建 SEQUENTIAL 类型的子目录/queue_i,这样就能保证所有成员加入队列时都是有编号的,出队列时通过 getChildren( ) 方法可以返回当前所有的队列中的元素,然后消费其中最小的一个,这样就能保证 FIFO。
下面是生产者和消费者这种队列形式的示例代码,完整的代码请看附件:
清单 6. 生产者代码
boolean produce(int i) throws KeeperException, InterruptedException{ ByteBuffer b = ByteBuffer.allocate(4); byte[] value; b.putInt(i); value = b.array(); zk.create(root + "/element", value, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT_SEQUENTIAL); return true; } |
清单 7. 消费者代码
int consume() throws KeeperException, InterruptedException{ int retvalue = -1; Stat stat = null; while (true) { synchronized (mutex) { List<String> list = zk.getChildren(root, true); if (list.size() == 0) { mutex.wait(); } else { Integer min = new Integer(list.get(0).substring(7)); for(String s : list){ Integer tempValue = new Integer(s.substring(7)); if(tempValue < min) min = tempValue; } byte[] b = zk.getData(root + "/element" + min,false, stat); zk.delete(root + "/element" + min, 0); ByteBuffer buffer = ByteBuffer.wrap(b); retvalue = buffer.getInt(); return retvalue; } } } } |