ZooKeeper使用详解

ZooKeeper使用详解

一、概述

Zookeeper:一个分布式协调服务为分布式应用

ZooKeeper是一个分布式、开源的协调服务为分布式应用。它暴露了一组简单的原始集合,分布式应用能构建在此之上来实现更高级的服务,比如:同步、维护配置、以及分组命名。它的设计是易于编程的,使用的数据模型风格类似于文件系统的目录树结构。它运行在Java中,并且在Java和C语言中都有绑定。

众所周知,协调服务是很难的。它们是很容易出错的,比如竞争条件和死锁。ZooKeeper背后的动机是减轻分布式应用程序从头开始实施协调服务的职责。

设计目标

Zookeeper是简单的 ZooKeeper允许分布式进程通过共享的分层命名空间相互协调,该命名空间的组织方式与标准文件系统类似。这个命名空间由数据寄存器组成,成为znode,在zookeeper中的说法,这些类似于文件和目录。不像典型的文件系统是用来存储数据,zookeeper的数据是保留在内存中,这意味着ZooKeeper可以实现高吞吐量和低延迟数量。

Zookeeper是多副本的 就像它协调的分布式流程一样,ZooKeeper本身旨在通过称为集合的一组主机进行复制。

[外链图片转存失败(img-BbpuQGhN-1569074381600)(https://zookeeper.apache.org/doc/r3.5.5/images/zkservice.jpg)]

组成zookeeper服务的服务器必须相互了解,他们维护着一个在内存中的状态镜像,以及事务日志和一个持久化存储的快照。只要大多数服务器可用,ZooKeeper服务就可用。

客户端连接到单个Zookeeper服务。客户端维护着一个TCP连接,通过它发送请求,获得响应,获得watch事件,以及发送心跳。如果客户端与服务器的连接断开,客户端将重新连接到一个不同的服务器。

ZooKeeper是有序的 Zookeeper标记每个更新通过一个编号,对应着所有Zookeeper事务的顺序。随后的操作能够使用这个顺序来实现更高等级的抽象,比如,同步原型。

ZooKeeper是快速的 它是非常快的在以读为主的工作负载,ZooKeeper应用运行在数千台机器上,并且在读取比写入更常见的情况下,它表现最佳,这个比例大约是10:1。

数据模型以及层次命名

ZooKeeper提供的命名空间就像一个标准的文件系统。名称是由斜杠(/)分隔的路径元素序列。每个节点在Zookeeper的命名空间中通过一个路径是独一无二的。

ZooKeeper的层次空间

在这里插入图片描述

节点和临时节点

不像标准的文件系统,ZooKeeper命名空间中的每个节点都可以包含与之关联的数据以及子节点。它就像有一个文件系统,允许一个文件也是一个目录。(Zookeeper是设计用来存储协调数据:状态信息、配置、位置信息等等,因此,存储在每个节点的数据通常是小的,在字节到千字节之间。)我们使用znode术语来明确我们正在讨论Zookeeper数据节点。

Znode维护着一个状态结构包含数据改变的版本号,ACL改变以及时间戳,用来允许缓存验证和协调更新。每次Znode的数据改变,版本号将会增加。比如,每当客户端检索数据时,它也会收到数据的版本。

存储在命名空间中每个znode的数据以原子方式读取和写入。读获得一个znode关联的所有数据字节,写操作替换所有数据。每一个节点有一个权限控制列表(ACL),用来限制谁能做什么。

ZooKeeper也有一个临时节点概念。只要创建这个znode的会话是活跃的,这些znode就会一直存在。当会话终止,这个znode将会被删除。当你想要实现[tbd]时,临时节点很有用。

条件更新与watch

ZooKeeper支持watch概念。客户端能设置一个watch在一个节点上。当znode改变,一个watch将会被触发和移除。当一个watch被触发,客户端接收一个包,告知znode已经被改变。如果客户端和某个Zookeeper服务器连接中断,客户端将收到一个本地通知。

保障

ZooKeeper是非常的快而且非常的简单。但是,它的目标是成为构建更复杂服务的基础,比如,同步,它提供了一系列的保障:

  • 序列一致——客户端的更新将按发送顺序应用。
  • 原子性——更新只有成功或失败,没有其它结果。
  • 单系统镜像——一个客户端无论连接到哪个服务器,它将看到相同的服务视图。
  • 可靠的——一旦一个更新已经被应用,从那时起它将持续存在,直到客户端覆盖更新。
  • 及时性——系统的客户端视图保证在特定时间范围内是最新的。

简单的API

ZooKeeper的设计目标之一是提供一个非常简单的编程接口。因此,他仅支持以下操作:

  • 创建:在树的一个位置上创建一个节点
  • 删除:删除一个节点
  • 存在:判断某个位置中的一个节点是否存在
  • 获得数据:从一个节点中读数据
  • 设置数据:写数据到一个节点
  • 获得子节点:取回一个子节点列表
  • 同步:等待数据传播

实现

ZooKeeper 组件显示ZooKeeper服务的高级组件。请求处理器除外,组成ZooKeeper服务的每个服务器都复制其自己的每个组件的副本。

在这里插入图片描述

副本数据库是一个在内存中的数据库,包含完整的数据树。为了可恢复的,更新将会被记录到磁盘中。并且写操作在应用到内存数据库之前将会被序列化到磁盘中。

每个ZooKeeper服务器都为客户端服务。客户端只连接到一台服务器以提交请求。读取请求由每个服务器数据库的本地副本提供服务。写请求通过一个协商协议处理。

作为协商协议的一部分,客户端的所有写请求将会转发到单个服务器,称为领导节点。剩下的Zookeeper服务器称为从服务器,接收来自领导节点的消息提议并就消息传递达成一致。消息层关注失败时替换领导节点以及将领导节点与从节点同步。

Zookeeper使用一个自定义的原子消息协议。由于消息层是原子的,Zookeeper能保证本地副本将不会偏离。当领导节点接收到一个写请求,它计算系统的状态,当写将会被应用并且转换它到一个事务中来刻画这个新的状态。

使用

ZooKeeper的编程接口非常简单,你能实现更高级的操作,比如同步原型,成员分组,所有权等等。

性能

ZooKeeper旨在提供高性能。但它是这样吗?ZooKeeper在雅虎开发团队的成果,研究表明它是。在读取数量超过写入的应用程序中,它的性能尤其高,因为写入涉及同步所有服务器的状态。(读取数量超过写入通常是协调服务的情况)

在这里插入图片描述

ZooKeeper吞吐量作为读写比率变化是ZooKeeper版本3.2的吞吐量图,在具有双2Ghz Xeon和两个SATA 15K RPM驱动器的服务器上运行。一个驱动器用于一个专门的Zookeeper日志设备。快照写入OS驱动器。写请求是1K写入,读取是1K读取。“Servers”表示ZooKeeper集群的大小。众多的服务器组成一个服务。

注意: 在3.2版本中,读/写性能和先前的3.1版本提高大概两倍。

基准也表明它是可靠的,存在错误时的可靠性显示了部署如何响应各种故障,图中标记的事件如下:

  • 从节点的失败和恢复
  • 不同的从节点恢复和失败
  • 领导节点的失败
  • 两个从节点的失败和恢复
  • 另一个领导节点的失败

可靠性

为了显示当注入失败时系统消耗的时间,我们运行一个由7台服务器组成的Zookeeper服务。我们运行与以前相同的饱和度基准,但是这次我们将写入百分比保持在30%不变,这是我们预期工作量的保守比率。

在这里插入图片描述

该图中有一些重要的观察结果,首先,如果从节点失败并迅速恢复,那么,尽管失败,ZooKeeper仍能保持高吞吐量。但也许更重要的是,领导者选举算法允许系统足够快地恢复以防止吞吐量大幅下降。通过我们的观察,Zookeeper花费少于200毫秒来选举一个新领导。第三,随着追随者的恢复,ZooKeeper能够在开始处理请求后再次提高吞吐量。

二、入门

这个文档包含让你快速开始使用ZooKeeper信息,它主要针对希望尝试的开发人员,包含安装指南对应单个服务器,一些命令行,用来验证它正在运行,以及一些简单的编程示例。最后,为方便起见,有一些关于更复杂的安装的部分,比如:运行副本部署,优化事务日志。然而,完整的商业部署指导,参考ZooKeeper Administrator’s Guide

下载

为了获得Zookeeper发行版,下载最近的 stable版本从某个Apache镜像中心。

独立操作

在独立模式下设置ZooKeeper服务器非常简单。服务器包含在单个JAR文件中,所以安装包括创建配置。

下载完一个稳定的ZooKeeper版本后,将其解压缩并cd到root

为了启动zookeeper你需要一个配置文件,这是一个示例,创建它在conf/zoo.cfg:

tickTime=2000
dataDir=/var/lib/zookeeper
clientPort=2181

这个文件可以在任何地方调用,文件中每个字段含义如下:

  • tickTime: ZooKeeper使用的基本时间单位(以毫秒为单位)。它用于心跳以及最小会话超时将是两次tickTime
  • dataDir:存储内存数据库快照位置,除非另有说明,否则将更新数据库的事务日志。
  • clientPort:监听客户端连接的端口

现在你已经创建了配置文件,你能开启Zookeeper:

bin/zkServer.sh start

连接到Zookeeper

$ bin/zkCli.sh -server 127.0.0.1:2181

这让你表现得很简单,类似文件操作

一旦你已经连接,你能看到以下事物:

Connecting to localhost:2181
log4j:WARN No appenders could be found for logger (org.apache.zookeeper.ZooKeeper).
log4j:WARN Please initialize the log4j system properly.
Welcome to ZooKeeper!
JLine support is enabled
[zkshell: 0]

从这个shell中,输入help来获取可以从客户端执行的命令列表:

[zkshell: 0] help
ZooKeeper host:port cmd args
    get path [watch]
    ls path [watch]
    set path data [version]
    delquota [-n|-b] path
    quit
    printwatches on|off
    create path data acl
    stat path [watch]
    listquota path
    history
    setAcl path acl
    getAcl path
    sync path
    redo cmdno
    addauth scheme auth
    delete path [version]
    deleteall path
    setquota -n|-b val path

从这里,你能尝试一些简单的命令行,感受这个简单的命令行交互。首先发出list命令:

[zkshell: 8] ls /
[zookeeper]

接下来创建一个新的节点,通过运行create /zk_test my_data。这里创建一个节点并且通过节点关联字符串“my_data”

[zkshell: 9] create /zk_test my_data
Created /zk_test

发出另一个 ls / 命令,查看目录是什么样的:

[zkshell: 11] ls /
[zookeeper, zk_test]

注意:zk_test目录已经被创建。

接下来验证znode关联的节点,通过运行 get 命令:

[zkshell: 12] get /zk_test
my_data
cZxid = 5
ctime = Fri Jun 05 13:57:06 PDT 2009
mZxid = 5
mtime = Fri Jun 05 13:57:06 PDT 2009
pZxid = 5
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0
dataLength = 7
numChildren = 0

我们能改变与zk_test的数据关联,通过发出set命令:

[zkshell: 14] set /zk_test junk
cZxid = 5
ctime = Fri Jun 05 13:57:06 PDT 2009
mZxid = 6
mtime = Fri Jun 05 14:01:52 PDT 2009
pZxid = 5
cversion = 0
dataVersion = 1
aclVersion = 0
ephemeralOwner = 0
dataLength = 4
numChildren = 0
[zkshell: 15] get /zk_test
junk
cZxid = 5
ctime = Fri Jun 05 13:57:06 PDT 2009
mZxid = 6
mtime = Fri Jun 05 14:01:52 PDT 2009
pZxid = 5
cversion = 0
dataVersion = 1
aclVersion = 0
ephemeralOwner = 0
dataLength = 4
numChildren = 0

(注意:设置数据之后我们做了一个 get 操作,数据确实改变了)

最后,我们来删除节点,通过发出 delete 命令

[zkshell: 16] delete /zk_test
[zkshell: 17] ls /
[zookeeper]
[zkshell: 18]

运行Zookeeper副本

运行Zookeeper在独立模式是方便的对于评测、一些开发、以及测试。但是在生产环境中,你应当运行Zookeeper在复制模式。一个服务器的副本组在相同的应用中称为:quorum,并且在副本模式中,所有在quorum的服务器都复制相同的配置文件。

注意:

对于复制模式,至少需要三台服务器,并且强烈推荐你有奇数台服务器。如果你只有两台服务器,如果其中一个失败,你处于这种情况,没有足够的机器来形成多数法定人数。两台服务器本质上不如单一服务器稳定,因为有两个单点故障。

conf/zoo.cfg文件对于复制模式的需求和使用独立模式类似,但是有很少的不同,比如:

tickTime=2000
dataDir=/var/lib/zookeeper
clientPort=2181
initLimit=5
syncLimit=2
server.1=zoo1:2888:3888
server.2=zoo2:2888:3888
server.3=zoo3:2888:3888

新的条目,initLimit是ZooKeeper用于限制仲裁中ZooKeeper服务器连接到领导者的时间长度。条目syncLimit限制服务器与领导者的过期时间。有了这两个超时,你能指定时间单位使用tickTime。在这个示例中,initLimit超时 是5个ticks在2000毫秒一个ticks,或者说10秒钟。

server.X 项列出组成Zookeeper服务的服务器。当服务器启动,它通过在数据目录中查找文件myid来了解它是哪个服务器。该文件包含服务器编号,ASCII格式。

最后,注意两个号码,在每个服务名之后:“2888”和“3888”。对等方使用以前的端口连接到其他对等方。这种连接是必要的,以便对等方可以进行通信,比如,为了协商更新顺序,进一步来说,Zookeeper服务器使用这个端口连接从节点到领导节点。当一个新领导出现,从节点打开一个TCP连接使用这个端口连接到领导节点。由于默认的领导选举也使用TCP协议,我们当前需要另一个端口对于领导选举。这是服务器条目中的第二个端口。

注意:

如果你想测试多台服务在单个机器,将servername指定为具有唯一的localhost以及领导选举端口(比如,在以上示例中的2888:3888, 2889:3889, 2890:3890)为每个服务,在那个服务的配置文件中。当然,分开_dataDir_s和区分_clientPort_s也是必要的(在以上副本示例中,运行在单localhost,你仍然要有3个配置文件)

请意识到建立多个服务在单个机器上将不会创建 任何冗余。如果发生导致机器死亡的事情,所有的zookeeper服务都会离线。完全的冗余需要每个服务有自己的机器。它必须是一个完全分开的物理服务器。多个虚拟机在相同的物理机上仍然是弱势的对于主机完全的失败。

其它优化

还有一些其他配置参数可以大大提高性能:

  • 要在更新时获得较低的延迟,请务必拥有专用的事务日志目录,默认情况下,事务日志与数据快照和myid文件放在同一目录中,dataLogDir参数指示用于事务日志的不同目录。

三、Zookeeper编程指南

介绍

这个文档是一个指导,为那些希望充分利用Zookeeper的协调服务优势来创建分布式应用的开发者。它包含概念上和实际的信息。

Zookeeper数据模型

Zookeeper有一个层次命名空间,就像一个分布式文件系统。仅有的不同是命名空间中的每个节点都可以包含与之关联的数据以及子节点。它就像有一个文件系统,允许一个文件成为一个目录。节点的路径始终表示为规范,绝对,斜线分隔的路径;没有相对的参考。任何unicode字符都可以在受以下约束限制的路径中使用:

  • 空字符(\u0000)不能是路径名的一部分(这在C绑定中可能造成问题)
  • 以下字符不能使用,因为他们也不能展示,或者渲染 在一个令人困惑的方式:\u0001 - \u001F 以及 \u007F
  • \u009F。
  • 以下 字符是不允许的:\ud800 - uF8FF, \uFFF0 - uFFFF
  • “.” 字符可以用作另一个名称的一部分,但是“.”以及"…“不能单独用于指示沿路径的节点,因为ZooKeeper不能使用相对路径,以下将是不合理的:”/a/b/./c"或"/a/b/…/c"。
  • 单词"zookeeper"是保留的。

ZNodes

ZooKeeper树中的每个节点都称为znode,Znode维护一个状态结构,包含版本号,当数据改变、acl改变时。这个状态结构也有时间戳。版本号和时间戳允许Zookeeper验证缓存以及协调更新。每次一个znode的数据改变,版本号增加,比如,每当客户端检索数据时,它也会收到数据的版本。并且,当一个客户端执行一个更新或者删除时,它必须提供它正在改变的znode数据的版本。如果他提供的版本和实际的版本不匹配,这个更新将会失败。

**注意:**在分布式应用中,node可以代表一个通用主机、一台服务器、众多成员、一个客户端进程等等。在Zookeeper文档中,znodes代表数据节点。服务器代表Zookeeper服务组成的机器。quorum peers是指构成整体的服务器。客户端指的是任何使用Zookeeper服务的主机或者进程。

Znodes是程序员访问的主要实体。它们有几个值得一提的特征。

Zookeeper中的时间

ZooKeeper以多种方式跟踪时间:

  • Zxid ZooKeeper状态的每次改变,都会从一个zxid(ZooKeeper Transaction Id)收到一个 印记。这暴露了ZooKeeper所有更改的总顺序。每个改变都有唯一的zxid,而且如果zxid1比zxid2小,那么zxid1发生在zxid2之前。
  • Version numbers 对节点的每次更改都将导致该节点的某个版本号增加。有三个版本号,分别是version(znode数据改变的号码),cversion(znode的子节点改变的号码)以及aversion(znode的ACL改变的号码)。
  • Ticks 当使用多服务器的ZooKeeper,服务器使用tick来定义事件的时间,例如状态上传,会话超时,会话超时,peers间的连接超时等等。tick时间仅通过最小会话超时间接暴露,服务器将告诉客户端会话超时实际上是最小会话超时。
  • 实时 Zookeeper没有使用实时,或者定时。除非在znode创建和znode修改上将时间戳放入stat结构

Zookeeper状态结构

zookeeper中,znode的状态结构由以下部分组成:

  • czxid znode的创建造成zxid的改变
  • mzxid 上次修改znode变化的zxid
  • pzxid 上次修改znode的子节点变化的zxid
  • ctime znode创建时的时间毫秒
  • mtime znode上次修改的时间毫秒
  • version znode数据改变的编号
  • cversion znode中子节点改变的编号
  • aversion znode中ACL改变的编号
  • ephemeralOwner 如果znode是一个临时节点,就是拥有这个znode的会话id,如果不是临时节点,它就是0
  • dataLength znode数据域的长度
  • numChildren znode中子节点的数量

Zookeeper 会话

ZooKeeper客户端通过使用语言绑定创建服务句柄,与ZooKeeper服务建立会话。创建后,句柄在CONNECTING状态下启动,客户端库尝试着连接到组成Zookeeper服务的其中一台服务器,此时,它状态变为CONNECTED。客户端句柄将处于这两种状态之一。如果一个不可恢复的错误发生,比如会话过期或者鉴权失败,或者如果应用程序显式关闭句柄,句柄将移至CLOSED状态。下图显示了ZooKeeper客户端的可能状态转换。

在这里插入图片描述

为了创建一个客户端会话,应用程序代码必须提供一个连接字符串,包含一个以逗号分隔的主机:端口列表,每个对应一个Zookeeper服务器。(e.g. “127.0.0.1:4545” or “127.0.0.1:3000,127.0.0.1:3001,127.0.0.1:3002”)。Zookeeper客户端库将随机挑选一个服务器并尝试连接它。如果连接失败,或者如果客户端因为一些原因与服务器失去连接。客户端将会自动尝试列表中的下一个服务器,知道连接开启。

3.2.0中新增:一个选项"chroot"能够追加到连接字符串后面,这将解析所有这个root的相对路径后运行客户端命令(类似于unix chroot 命令),打个比方,可能看起来像"127.0.0.1:4545/app/a" 或者 “127.0.0.1:3000,127.0.0.1:3001,127.0.0.1:3002/app/a”,在那里,客户端将会重置根路径在"/app/a"下面,并且所有的路径都会相对于这个根路径,例如getting/setting/等等。"/foo/bar" 将使操作运行在"/app/a/foo/bar" (从服务器的角度来看)。这个功能是非常有用的在多租户情况下,每个特定的Zookeeper服务用户可能根路径不同,这使得重用更加容易,每个用户都可以编写他的应用程序,就好像它的根目录是“/”,然而实际的位置能够在部署时决定。

当客户端获取ZooKeeper服务的句柄时,ZooKeeper创建一个ZooKeeper会话,表现为一个64为数字分配给客户端。如果客户端连接到不同的Zookeeper服务器,它将发送这个会话id作为连接握手的一部分。作为一个安全措施,服务器为这个会话id创建一个密码,任何Zookeeper服务器都能验证。当客户端会话打开,密码将通过session id发送。只要客户端使用新服务器重新建立会话,客户端就会使用会话ID发送此密码。

ZooKeeper客户端库调用创建ZooKeeper会话的参数之一是会话超时(以毫秒为单位)。客户端发送一个请求超时,服务端响应这个超时。当前实现需要超时最少要两次tickTime,最大20次tickTime。ZooKeeper客户端API允许使用协商的超时。

当一个客户端(会话)从ZK集群中被分区时,它将开始搜索会话创建期间指定的服务列表。最终,客户端和至少一个服务器重新打开连接,会话将再次转换到"connected"状态(如果重连在超时值之内),或者将转换到"expired"状态(如果会话超时后重连)。不建议创建新的会话对象(一个新的ZooKeeper.class或zookeeper句柄绑定在C语言中)来断开连接。ZK客户端库将为你处理重连。特别是我们在客户端库中内置了一些启发式方法来处理诸如“牧群效应”之类的事情,仅在收到会话到期通知时才创建新会话(强制性)。

会话过期通过ZooKeeper集群自身连接,而不通过客户端。当ZK客户端通过集群打开一个会话,它提供一个超时值明细,这个值是集群用来决定什么时候客户端的会话过期。当群集在指定的会话超时期限内没有从客户端收到消息时(比如,没有心跳),就会发生过期。在会话过期时,集群将删除那个会话拥有的所有临时节点并且立刻把这个变化通知所有连接的客户端(任何关注这个节点的人)。此时,过期的会话仍然与集群失联,在那时,过期会话的watcher将收到"会话过期"通知。

示例,通过过期会话的watcher看到的状态过渡:

  1. ‘connected’ : 会话打开,客户端与集群正在交流。
  2. …客户端从群集分区
  3. ‘disconnected’ : 客户端与集群丢失连接
  4. 时间流逝,在’timeout’期之后,集群过期会话,由于客户端与集群失联,所以什么也看不到
  5. 时间流逝,客户端重新获得与群集的网络级连接
  6. ‘expired’ : 最终客户端重新连接到这个集群,然后告知这个过期

ZooKeeper会话建立调用的另一个参数是默认watcher。当任何改变发生在这个客户端时,比如,如果客户端与服务器丢失连接,这个客户端将会收到通知,或者如果客户端会话过期等等。watcher应该考虑初始状态至失联状态(比如,在任何状态改变之前,事件通过客户端库发送到watcher)。在新连接情况下,发送到watcher中的第一个事件一般是连接事件。

通过客户端请求发送,会话保持存活。如果会话空闲一段时间将要会话超时,客户端将会发送一个PING请求来保持会话存活。PING请求不仅让Zookeeper服务器知道客户端仍然存活,也让客户端来验证连接到的Zookeeper服务器仍然存活。PING的时机是非常保守的来保证合理的时间去侦测一个死亡连接并且重连到一个新服务器。

一旦一个连接成功在服务器中开启,当客户端lib生成连接丢失,基本上有两种情况,当执行以下之一同步或异步操作时:

  • 应用调用一个操作在会话中将不再合理/存活
  • 当存在对该服务器的阻塞操作时,ZooKeeper客户端与服务器断开连接,比如一个阻塞的异步调用。

3.2.0新增 — SessionMovedException: 有个内部异常,客户端通常看不到,叫做SessionMovedException。这个异常出现是由于一个请求接收一个会话连接之前在其它服务器被重连。这个错误的合理解释是一个客户端发送请求到服务端,但由于网络延迟,因此客户端超时并连接到一个新服务器。(旧的连接通常已经关闭了),可以看到这种情况的一种情况是两个客户端尝试使用保存的会话ID和密码重新建立相同的连接。其中一个客户端将重新打开连接,另一个客户端将断开(导致它们尝试无限期地重新建立其连接/会话)

更新服务器列表 Zookeeper允许客户端更新连接字符串,通过提供一个新的逗号分隔的主机:端口列表,每个对应一个Zookeeper服务器。该函数调用概率负载平衡算法,该算法可能导致客户端与其当前主机断开连接,目标是在新列表中实现每个服务器的预期统一连接数。如果客户端连接的当前主机不在新列表中,则此调用将始终导致连接被删除。否则,决定是基于服务器数量是增加还是减少以及增加多少。

比如,如果先前的连接字符串包含3个主机,现在列表中包含这3个主机以及2个额外的主机。为了负载均衡,40%连接到这3台主机的每个客户端都将移动到新的主机。这个算法将造成客户端以0.4的概率丢失与当前主机的连接。在这种情况下,导致客户端随机连接到2个新主机之一。

另一个示例,假如我们有5个主机,现在更新列表来移除2个主机,连接到剩下3个主机的客户端将继续保持连接,然而连接到2个已移除的主机的客户端需要连接到剩下3台主机之一,这也是随机的。如果连接中断,客户端进入特殊模式,在那里,它选择一个新的服务器来连接,使用概率算法,而不仅使用轮询。

在第一个示例中,每个客户端决定断开的概率是0.4,但是一旦决定,它将尝试随机的连接到一个新的服务器,如果它连接不到任何一个新服务器,它将连接到旧的服务器。发现一个服务器之后,或者尝试连接所有新服务器失败后,客户端回到正常操作模式,它从连接字符串中挑出任意一个服务器企图去连接到它,如果失败,它将在轮询中继续尝试不同的服务器。

ZooKeeper Watches

Zookeeper中所有读操作——getData(), getChildren(), 和 exists() ,都有设置一个watcher作为副作用的选项。以下是Zookeeper对watch的定义:一个watch事件是一次性触发,发送到设置了watch的客户端,这仅当设置了watch的数据发生变化,将会触发。这有3个关键点来思考watch的定义:

  • 一次性触发 当数据改变,一个watch事件将发送到客户端,比如,如果 一个客户端调用getData("/znode1", true) ,以后/znode1的数据改变或删除,客户端将获得/znode1的watch事件。如果/znode1再次改变,watch事件将不会发送,除非客户端再次读时设置了一个新的watch
  • 发送到客户端 这指的是事件正前往客户端。但是在更改操作的成功返回码到达发起更改的客户端之前,可能无法到达客户端。Watches以异步的方式发送给观察者。Zookeeper提供一个顺序保证,在第一次看到watch事件之前,客户端永远不会看到设置的watch更改。网络延迟或者其它因素可能造成不同的客户端在不同时间查看更新中的watch和返回代码。关键点在于,不同客户看到的所有内容都会有一致的顺序。
  • 为watch设置数据 这表明节点可以改变在不同的方式。这有助于思考Zookeeper作为维护两个watch列表:数据watch以及子watch。getData() 和 exists()设置数据watch。getChildren() 设置子watch。它有助于理解正在设置的watch根据数据返回类型。getData() 和 exists()返回关于数据节点的信息。getChildren() 返回子列表。因此,setData()将触发数据watches当znode正在设置,一个成功的create()将触发数据watch当znode正在创建,以及相对于父znode的子watch。当znode正在被删除,一个成功的delete() 将触发数据watch以及子watch。

客户端连接到Zookeeper服务器的watch在本地维护。这使watches很容易设置、维护、以及转发。当客户端连接到一个新服务器,watch将会触发对于任何会话事件。当与服务器断开,将不会收到watch。当一个客户端重连,任何先前注册的watch将会被重新注册并且在需要时触发。一般来说,这一切都是透明的。这有一个watch可能会丢失的案例:如果znode已创建,并且丢失连接后删除,当已存在的znode还没有创建,一个watch将会丢失。

Watch 语义

你能够设置watch用三个调用来读取ZooKeeper的状态:exists, getData, 和 getChildren。以下列表详细说明了watch可以触发的事件以及启用它们的调用

  • **创建事件:**通过exists调用开启
  • **删除事件:**通过 exists, getData, 和 getChildren调用开启
  • **改变事件:**通过exists 和 getData调用开启
  • **子事件:**通过getChildren调用开启

移除 Watch

你能够移除znode中已注册的watch,通过调用removeWatches。除此之外,Zookeeper能够移除本地watch即使没有服务器连接,通过设置本地flag为true

  • Child Remove event: 通过调用getChildren添加的watch
  • Data Remove event: 通过调用 exists 或 getData添加的watch

Zookeeper对Watch的保证

关于watches,Zookeeper主要有以下保证:

  • Watches根据其它事件,其它Watches、以及异步答复排序。Zookeeper客户端类库确保按顺序发送所有内容。
  • 一个客户端在看见znode对应的新数据之前,将看到正在观察节点的watch事件
  • watch事件的顺序来自于Zookeeper对应的Zookeeper服务看到的更新顺序

Watches 需要注意以下点

  • Watches是一次性触发,如果你获得一个watch事件并且想获得将来改变的通知,你必须设置另一个watch
  • 由于watch是一次性触发,在获取事件和发送新请求以获取watch之间存在延迟,你无法可靠地看到ZooKeeper中节点发生的每个更改。准备处理这种情况——当znode多次改变在获得事件与再次设置watch之间(你可能不在意,但至少要知道它可能会发生)
  • 一个watch对象或者函数/上下文,对于给定的通知将只触发一次。比如,如果相同的watch对象注册了一个exists和getData调用对于相同的文件,并且那个文件然后被删除,这个watch对象将仅仅文件删除时调用一次。
  • 当你连接到一个服务器(比如,当服务器失败),你将不会获得任何watch知道连接重新打开。因此,会话事件发送到所有优秀的watch处理器。使用会话事件将进入安全模式:断开连接时不会收到事件,因此在这种模式下你的处理应该表现保守的。

Zookeeper使用ACLs控制权限

ZooKeeper使用ACL来控制对其znode的访问。ACL的实现是非常类似于UNIX文件使用权限:它使用权限位来允许/禁止针对节点的各种操作以及位应用的范围,与标准的UNIX权限不同,Zookeeper节点是不受限制的,根据3个标准范围:user(文件拥有者),group,world(其它)。ZooKeeper没有znode所有者的概念,相反,ACL指定一组ID和与这些ID相关联的权限。

注意,ACL仅适用于特定的znode。尤其是它不能应用到子节点。比如,如果*/app仅ip:172.16.16.1可读的,并且/app/status是全局可读的,那么任何人都能读到/app/status*,ACLs是非递归的。

Zookeeper支持可拔插的权限方案,使用表单scheme:expression指定ID,其中scheme是id对应的认证方案,有效表达式集由方案定义。比如:ip:172.16.16.1是使用ip方案的地址为172.16.16.1的主机的id,而digest:bob:password是使用digest方案的名称为bob的用户的id。

当客户端连接到zookeeper以及自身鉴权,ZooKeeper将与客户端对应的所有ID与客户端连接相关联。当客户端尝试访问节点时,将根据znode的ACL检查这些ID。ACLs由*(scheme:expression, perms)对组成。表达式的格式特定于该方案。比如,(ip:19.22.0.0/16, READ)*给定读权限对于任何客户端IP地址以19.22开头。

ACL 权限

ZooKeeper支持以下权限:

  • CREATE:你能创建一个子节点
  • READ:你能获得数据从一个节点以及它的子节点
  • WRITE: 你能为一个节点设置数据
  • DELETE: 你能删除一个子节点
  • ADMIN: 你能设置权限

CREATE和DELETE权限已从WRITE权限中删除,以获得更精细的访问控制。CREATE和DELETE的情况如下:

你希望A能够在ZooKeeper节点上执行设置,但不能CREATEDELETE 子节点。

CREATE 没有 DELETE: 客户端创建请求通过创建zookeeper节点在父目录。你希望客户端能够添加,但只有请求处理器才能删除(这有点像文件的APPEND权限)。

此外,由于ZooKeeper没有文件所有者的概念,因此存在ADMIN权限。在某种意义上,ADMIN权限将实体指定为所有者。Zookeeper不支持LOOKUP权限(对目录执行权限位,即使无法列出目录也允许LOOKUP),每个人都隐含地拥有LOOKUP权限。

ADMIN权限在ACL方面也有特殊作用,为了检索znode的ACL,用户必须具有READ或ADMIN权限,但是没有ADMIN权限,摘要哈希值将被屏蔽掉。

内置ACL方案

ZooKeeper具有以下内置方案:

  • world 有一个id,任何人,代表任何人。
  • auth 是一种特殊方案,它忽略任何提供的表达式,而是使用当前用户,凭据和方案。在持久化ACL时,ZooKeeper服务器会忽略所提供的任何表达式(无论是使用SASL身份验证还是用户:密码,如使用DIGEST身份验证)。但是,仍必须在ACL中提供表达式,因为ACL必须与表单scheme:expression:perms匹配。提供此方案是为了方便,因为它是用户创建znode的常见用例,然后将对该znode的访问限制为仅限该用户。
  • digest 使用username:password字符串生成MD5哈希值,然后将其用作ACL ID身份,通过以明文形式发送用户名:密码来完成身份验证。在ACL中使用时,表达式将是用户名:base64编码的SHA1密码摘要。
  • ip 使用客户端主机IP作为ACL ID标识,ACL表达式的形式为addr / bits,其中addr的最高有效位与客户端主机IP的最高有效位匹配。
  • x509 使用客户端X500 主体作为ACL ID标识。ACL表达式是客户端的确切X500主体名称,使用安全端口时,客户端将自动进行身份验证,并设置x509方案的身份验证信息。

可拔插的ZooKeeper权限

Zookeeper运行在各种不同的环境通过各种不同的鉴权方案,因此它有一个完全可拔插的鉴权框架。甚至内置的鉴权方案使用可拔插的鉴权框架。

为了理解鉴权框架如何工作,首先你必须理解两个主要操作。框架首先必须验证客户端。这通常在客户端连接到服务器时完成,包括验证从客户端发送或收集的有关客户端的信息并将其与连接相关联。框架处理的第二个操作是在ACL中查找与客户端对应的条目。ACL条目是<idspec, permissions>对。idspec可以是针对与连接相关联的认证信息的简单字符串匹配,或者它可以是针对该信息评估的表达式。由认证插件的实现来进行匹配。以下是身份验证插件必须实现的接口:

public interface AuthenticationProvider {
    String getScheme();
    KeeperException.Code handleAuthentication(ServerCnxn cnxn, byte authData[]);
    boolean isValid(String id);
    boolean matches(String id, String aclExpr);
    boolean isAuthenticated();
}

第一个方法getScheme返回标识插件的字符串。因为我们支持多种身份验证方法,身份验证凭据或idspec将始终以scheme为前缀。ZooKeeper服务器使用身份验证插件返回的方案来确定该方案适用的ID。

当客户端发送与连接关联的身份验证信息时handleAuthentication被调用。客户端指定信息对应的方案。ZooKeeper服务器将信息传递给身份验证插件,其getScheme与客户端传递的方案匹配。handleAuthentication的实现者通常会在确定信息不良时返回错误,或者它将通过连接使用*cnxn.getAuthInfo().add(new Id(getScheme(), data))*关联信息。

鉴权插件调用在设置和使用ACLs。当为一个znode设置ACL时,ZooKeeper服务器将传递id部分到 isValid(String id)方法。由插件来验证id是否具有正确的形式。比如ip:172.16.0.0/16是合理的,但 ip:host.com不是。如果新的ACL包含一个"auth"项,isAuthenticated用于查看是否应将与该连接关联的此方案的身份验证信息添加到ACL。一些方案不应包括在auth中。例如,如果指定了auth,则不将客户端的IP地址视为应添加到ACL的ID。

检查ACL时,ZooKeeper会调用匹配项matches(String id, String aclExpr),它需要匹配客户端针对相关ACL条目的权限信息。为了发现应用到客户端的条目,Zookeeper服务器将查找每个条目的方案,如果有某个方案的某个客户端的鉴权信息,将调用match(String id,String aclExpr),并将id设置为先前通过handleAuthentication添加到连接的身份验证信息,并将aclExpr设置为ACL条目的id。鉴权插件使用它拥有的逻辑以及匹配方案来决定id是否包含在aclExpr中。

这有两个内置的鉴权插件,ipdigest。其它的插件能使用系统属性添加。在Zookeeper服务器启动时,将查找以"zookeeper.authProvider"开头的系统属性以及将这些属性的值解析为身份验证插件的类名。这些属性能通过 -Dzookeeeper.authProvider.X=com.f.MyAuth设置或者在服务配置文件中添加以下条目:

authProvider.1=com.f.MyAuth
authProvider.2=com.f.MyAuth2

需要注意的是这些属性的前缀应该是唯一的。如果有重复的,比如 -Dzookeeeper.authProvider.X=com.f.MyAuth -Dzookeeper.authProvider.X=com.f.MyAuth2,只有一个会被使用。所有的服务器也必须有相同的定义过的插件,否则,客户端使用插件提供的鉴权方案在连接到一些服务器时将出现问题。

一致性保证

Zookeeper是一个高性能、可扩展的服务。读和写操作都设计的很快,尽管读比写更快。这是因为在进行读操作时,Zookeeper能够用更旧的数据服务,这主要归功于Zookeeper的一致性保证。

  • 顺序一致性:来自客户端的更新将使用他们发送的顺序。
  • 原子性:更新只有成功或者失败,没有其它结果
  • 单系统镜像:客户端将看到相同的服务视图,无论它连接到哪台服务器
  • 可靠性:一旦一个更新被应用,从那时起它将持续存在直到一个客户端重写这个更新。这个保证有两个推论:
    1. 如果一个客户端获得一个成功的返回码,这个更就已经被应用。当出现一些失败(通讯异常,超时等等),客户端将不知道是否这个更新应用与否。我们采取措施尽量减少失败,但保证仅存在成功的返回码
    2. 客户端看到的任何更新,通过读请求或者成功的更新,从服务器故障中恢复时永远不会回滚。
  • 及时性:系统的客户端视图保证在特定时间范围内(大约几十秒)是最新的。在此范围内,客户端将看到系统更改,或者客户端将检测到服务中断。

通过这些一致性保证,很容易建立更高级别的功能,比如领导选举,阻塞,队列,读/写撤销锁,仅需在Zookeeper客户端。

注意:

有时开发者误以为其它的保证,但实际上Zookeeper并没有实现。这就是同时保持一致的跨客户端视图:Zookeeper不能保证每个时刻及时的,两个不同的客户端有统一的Zookeeper数据视图。类似于网络延迟的因素,一个客户端可能执行一个更新之前另一个客户端已经获得变化的通知。考虑两个客户端的场景,A和B,如果客户端A设置znode /a 的值从0到1,然后告诉客户端B去读取/a,客户端B可能读到旧值0,取决于它连接到哪台服务器。如果它是很重要的对于客户端A和客户端B读到相同的值,客户端应该在执行读操作之前调用ZooKeeper API中的sync() 方法。因此,ZooKeeper本身并不保证所有服务器上的更改同步发生。但是Zookeeper原型能够用来构造更高级的功能通过提供有用的客户端同步(详见 ZooKeeper Recipes)。

绑定

ZooKeeper客户端库有两种语言:Java和C,以下描述Java

Java 绑定

Zookeeper的Java绑定由两个包组成:org.apache.zookeeperorg.apache.zookeeper.data。其余组成ZooKeeper的包用于内部或者服务实现的一部分。org.apache.zookeeper.data由生成的类组成,这些仅用作容器。Zookeeper Java客户端使用的主类是ZooKeeper类。它的两个构造函数仅有的差异是一个可选的会话ID和密码。Zookeeper支持跨进程实例的会话恢复。Java程序能够保存它的会话ID和密码来稳定存储、重启、以及用于恢复更早编程实例的会话。

当Zookeeper对象被创建,两个线程也会随之创建:一个IO线程和一个事件线程。所有的IO发生在IO线程(使用Java NIO)。所有的事件回调发生在事件线程。会话维护诸如重新连接到Zookeeper服务器以及在IO线程上完成维护心跳。响应同步方法也是处理在IO线程。对于异步方法的所有响应以及watch事件都是在事件线程中处理。对于这样的设计结果,有些事情需要注意:

  • 所有完成的异步调用以及watcher回调将按顺序产生,一次一个。调用者能做他们希望的任何处理,但此时将不会有其它回调被处理。
  • 回调不会阻止IO线程的处理或者同步调用的处理
  • 同步调用可能不会返回正确的顺序。比如,假设一个客户端做以下处理:发起一个异步读取 /a 节点并设置watch为true,然后完成回调时做同步读取 /a 。注意,如果**/a** 发生变化在异步读取和同步读取之间,在同步响应读取到之前,客户端类库将收到一个watch事件告知**/a** 已经变化,但由于完成回调正在阻塞事件队列,在这个watch事件被处理之前,同步读取将返回**/a** 的新值。

最终,与关闭相关的规则很简单:一旦一个Zookeeper对象关闭或者接收一个致命的事件(SESSION_EXPIRED 和 AUTH_FAILED),Zookeeper对象变得不合理。在结束时,两个线程关闭而且对zookeeper的任何进一步访问都是未定义的行为并且应当避免。

客户端配置参数

以下列举出了Java客户端包含的配置属性。你能设置这些任意属性使用Java系统属性。请检查以下参考Server configuration section

  • zookeeper.sasl.client:设置值为false来关闭SASL鉴权,默认为true
  • zookeeper.sasl.clientconfig : 指定JAAS登录文件中的上下文键,默认是"Client"
  • zookeeper.sasl.client.username : 一般来讲,主要分成3个部分: 首先是实例和领域。典型的Kerberos V5主体的格式为primary / instance @ REALM。zookeeper.sasl.client.username来指定服务主体的主要部分,默认为Default is “zookeeper”。实例部分来自服务器IP。最后服务器的主体是用户名/ IP @ 域,用户名是zookeeper.sasl.client.username的值,IP是服务器的IP,域是zookeeper.server.realm的值。
  • zookeeper.server.realm : 服务器主体的域部分。默认情况下,它是客户端主体域。
  • zookeeper.disableAutoWatchReset : 这个开关控制自动watch重置是否开启。默认情况下,在会话重连时,客户端自动重置watch,这个选项允许客户端关闭这个行为通过设置setting zookeeper.disableAutoWatchReset为true
  • zookeeper.client.secure : 如果你想连接到服务器安全客户端端口,你需要在这个客户端中设置这个属性为true,这将通过指定的证书使用SSL连接到服务器。请注意,它需要Netty客户端。
  • zookeeper.clientCnxnSocket : 指定使用哪个ClientCnxnSocket。可选值是org.apache.zookeeper.ClientCnxnSocketNIOorg.apache.zookeeper.ClientCnxnSocketNetty 。默认值是org.apache.zookeeper.ClientCnxnSocketNIO。如果你想连接到服务器的安全客户端端口,你需要在客户端中设置这个属性为org.apache.zookeeper.ClientCnxnSocketNetty
  • zookeeper.ssl.keyStore.location 和 zookeeper.ssl.keyStore.password : 指定一个JKS的文件路径,包含一个本地证书用于SSL连接,以及解锁文件的密码。
  • zookeeper.ssl.trustStore.location and zookeeper.ssl.trustStore.password : 指定一个JKS的文件路径,包含用于SSL连接的远程证书,以及解锁文件的密码。
  • jute.maxbuffer : 指定从服务器发送来的数据的最大尺寸,默认是4194304字节,或仅4 MB。这是一个非常明智的检查。Zookeeper服务器设计的存储和发送数据大约千字节。如果接下来的数据长度超过这个限制,将会引发IOException。
  • zookeeper.kinit : 指定kinit二进制文件的路径。默认为"/usr/bin/kinit"。

常见问题以及故障排除

  1. 如果你正在使用watches,你必须查找已连接的watch事件。当Zookeeper客户端与服务器失去连接,你将不会收到变化的通知,直到重新连接。如果你正在监视znode的存在与否,当你失去连接时,你将会丢失创建和删除事件。
  2. 你必须测试ZooKeeper服务器故障。Zookeeper服务器能够幸存失败,只要大多数的服务器是活跃的。问题是:你的应用能处理它吗?在现实生活中,一个客户端与Zookeeper的连接可能中断(Zookeeper服务器失败或者网络方面通常是连接丢失)。Zookeeper客户端类库负责恢复你的连接并且让你知道发生了什么,但是您必须确保恢复状态以及所有失败的未完成请求。在测试实验中弄清楚是否正确处理,而不是在生产中。使用由多个服务器组成的ZooKeeper服务进行测试,并使其重启。
  3. 客户端使用的ZooKeeper服务器列表必须与每个ZooKeeper服务器具有的ZooKeeper服务器列表匹配。如果列出的是真实Zookeeper服务器列表的子集,事情可以生效,尽管不是最优的。但如果客户端列举的Zookeeper服务器不在Zookeeper集群中,则不会这样。
  4. 注意事物日志存放的位置。ZooKeeper最关键的性能部分是事务日志。当返回一个响应时,Zookeeper必须同步事务日志到媒介中。一个专用的事务日志设备是非常重要的对于保持好的性能。将事务日志存放在繁忙的设备上,将对性能产生不利的影响。如果你只有一个存储设备,将跟踪文件放在NFS上并增加快照数量。它不会消除这个问题,但能够减轻。
  5. 设置一个恰当的Java最大堆大小。这是非常重要的对于避免交换。不必要地进入磁盘几乎肯定会降低你的性能。记住,在Zookeeper中,每件事都是按顺序的。因此,如果一个请求命中了磁盘,所有其它排队的请求也都会命中磁盘。为了避免交换,尝试将堆大小设置为你的物理内存容量,减去操作系统和缓存所需的数量。决定一个最佳的堆大小的方式是运行一个负载测试。如果因为一些原因你不能这么做,保守估计,并选择一个远低于会导致您的机器交换的限制的数字。例如,在4G内存机器上,3G堆是一个保守的估计。

四、Zookeeper Java示例

创建项目

首先创建一个maven项目并加入以下依赖:

<dependency>
  <groupId>org.apache.zookeeper</groupId>
  <artifactId>zookeeper</artifactId>
  <version>3.4.12</version>
</dependency>

然后创建一个watch来监听数据变化事件

import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;

public class MyWatch implements Watcher {

  	private ZooKeeper zooKeeper;

    public MyWatch(ZooKeeper zooKeeper) {
        this.zooKeeper = zooKeeper;
    }
  
    @Override
    public void process(WatchedEvent event) {
       	//打印当前事件内容
        System.out.println(event);
    }
}

最后创建一个主类

import com.ycw.wach.MyWatch;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.ZooDefs;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.data.Stat;

import java.io.IOException;

public class Main {

    public static void main(String[] args) throws KeeperException, InterruptedException, IOException {
        Stat stat = new Stat();
      	//zookeeper主机端口号
        String hostPort = "localhost:2181";
      	//节点路径
        String path = "/test";
      	//节点值
        String value = "ycw";

      	//创建zookeeper对象
        ZooKeeper zooKeeper = new ZooKeeper(hostPort, 3000, null);
      	//创建watch实例并将zooKeeper对象传入构造函数
        MyWatch watch = new MyWatch(zooKeeper);

      	//判断节点是否存在,如果不存在则创建节点
        Stat exists = zooKeeper.exists(path, watch);
        if (exists == null) {
            String s = zooKeeper.create(path,value.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
            System.out.println("zookeeper create success => " + s);
        }

      	//获取节点值
        byte[] data = zooKeeper.getData(path, watch, stat);
        System.out.println("data ==> " + new String(data));

        Thread.sleep(Integer.MAX_VALUE);
    }
}

在运行以上代码前需要启动zookeeper服务

bin/zkServer.sh start

然后启动程序,此时应用会创建/test节点并设置节点值为ycw。程序会获取/test节点数据,这时控制台将会打印出/test节点的值。

为了验证程序能够监听到znode节点数据的更改,我们首先通过命令行连接到zookeeper服务器。

bin/zkCli.sh -server 127.0.0.1:2181

首先查看/test节点数据

[zk: 127.0.0.1:2181(CONNECTED) 0] get /test
ycw
cZxid = 0x4aa
ctime = Sat Sep 21 15:39:59 CST 2019
mZxid = 0x4cb
mtime = Sat Sep 21 18:53:55 CST 2019
pZxid = 0x4aa
cversion = 0
dataVersion = 12
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 6
numChildren = 0

然后修改/test节点值为hello,然后观察java程序控制台变化

[zk: 127.0.0.1:2181(CONNECTED) 1] set /test hello 
cZxid = 0x4aa
ctime = Sat Sep 21 15:39:59 CST 2019
mZxid = 0x4ce
mtime = Sat Sep 21 19:08:28 CST 2019
pZxid = 0x4aa
cversion = 0
dataVersion = 13
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 5
numChildren = 0

不出意外的话,java控制台将收到/test数据变化,并打印该事件

WatchedEvent state:SyncConnected type:NodeDataChanged path:/test

此时如果我们将/test节点数据改为world,java控制台还会不会打印出变化事件?答案是否定的,由于zookeeper事件是一次性的,因此当收到事件后,如果不注册新的watch将不会收到下个更改事件。如果我们想持续的监听更改事件需要怎么做呢?此时只需要在监听器中进行新的事件注册就可以了。接下来将MyWatch类中的process方法改为如下:

@Override
public void process(WatchedEvent event) {
  try {
    zooKeeper.exists(event.getPath(),this);
  } catch (KeeperException | InterruptedException e) {
    e.printStackTrace();
  }
  System.out.println(event);
}

每当接收到一个事件后,我们再重新监听该节点的是否存在事件。更改完成后重启应用。

为了验证每次数据更改都能监听到事件,我们在命令行中分别执行修改数据、删除节点、创建节点操作。

[zk: 127.0.0.1:2181(CONNECTED) 15] set /test "hello world"
cZxid = 0x4aa
ctime = Sat Sep 21 15:39:59 CST 2019
mZxid = 0x4d1
mtime = Sat Sep 21 21:46:50 CST 2019
pZxid = 0x4aa
cversion = 0
dataVersion = 14
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 11
numChildren = 0
[zk: 127.0.0.1:2181(CONNECTED) 16] delete /test
[zk: 127.0.0.1:2181(CONNECTED) 17] create /test newValue
Created /test

此时查看Java应用控制台,显示出了以下信息:

WatchedEvent state:SyncConnected type:NodeDataChanged path:/test
WatchedEvent state:SyncConnected type:NodeDeleted path:/test
WatchedEvent state:SyncConnected type:NodeCreated path:/test

以上三条事件分别对应数据变更操作、节点删除操作、节点创建操作。

示例代码详见https://gitee.com/yancaowei/zookeeper-sample

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值