赞
踩
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]时,临时节点很有用。
ZooKeeper支持watch概念。客户端能设置一个watch在一个节点上。当znode改变,一个watch将会被触发和移除。当一个watch被触发,客户端接收一个包,告知znode已经被改变。如果客户端和某个Zookeeper服务器连接中断,客户端将收到一个本地通知。
ZooKeeper是非常的快而且非常的简单。但是,它的目标是成为构建更复杂服务的基础,比如,同步,它提供了一系列的保障:
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
这个文件可以在任何地方调用,文件中每个字段含义如下:
现在你已经创建了配置文件,你能开启Zookeeper:
bin/zkServer.sh start
$ 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在复制模式。一个服务器的副本组在相同的应用中称为: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服务都会离线。完全的冗余需要每个服务有自己的机器。它必须是一个完全分开的物理服务器。多个虚拟机在相同的物理机上仍然是弱势的对于主机完全的失败。
还有一些其他配置参数可以大大提高性能:
这个文档是一个指导,为那些希望充分利用Zookeeper的协调服务优势来创建分布式应用的开发者。它包含概念上和实际的信息。
Zookeeper有一个层次命名空间,就像一个分布式文件系统。仅有的不同是命名空间中的每个节点都可以包含与之关联的数据以及子节点。它就像有一个文件系统,允许一个文件成为一个目录。节点的路径始终表示为规范,绝对,斜线分隔的路径;没有相对的参考。任何unicode字符都可以在受以下约束限制的路径中使用:
ZooKeeper树中的每个节点都称为znode,Znode维护一个状态结构,包含版本号,当数据改变、acl改变时。这个状态结构也有时间戳。版本号和时间戳允许Zookeeper验证缓存以及协调更新。每次一个znode的数据改变,版本号增加,比如,每当客户端检索数据时,它也会收到数据的版本。并且,当一个客户端执行一个更新或者删除时,它必须提供它正在改变的znode数据的版本。如果他提供的版本和实际的版本不匹配,这个更新将会失败。
**注意:**在分布式应用中,node可以代表一个通用主机、一台服务器、众多成员、一个客户端进程等等。在Zookeeper文档中,znodes代表数据节点。服务器代表Zookeeper服务组成的机器。quorum peers是指构成整体的服务器。客户端指的是任何使用Zookeeper服务的主机或者进程。
Znodes是程序员访问的主要实体。它们有几个值得一提的特征。
ZooKeeper以多种方式跟踪时间:
zookeeper中,znode的状态结构由以下部分组成:
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看到的状态过渡:
ZooKeeper会话建立调用的另一个参数是默认watcher。当任何改变发生在这个客户端时,比如,如果客户端与服务器丢失连接,这个客户端将会收到通知,或者如果客户端会话过期等等。watcher应该考虑初始状态至失联状态(比如,在任何状态改变之前,事件通过客户端库发送到watcher)。在新连接情况下,发送到watcher中的第一个事件一般是连接事件。
通过客户端请求发送,会话保持存活。如果会话空闲一段时间将要会话超时,客户端将会发送一个PING请求来保持会话存活。PING请求不仅让Zookeeper服务器知道客户端仍然存活,也让客户端来验证连接到的Zookeeper服务器仍然存活。PING的时机是非常保守的来保证合理的时间去侦测一个死亡连接并且重连到一个新服务器。
一旦一个连接成功在服务器中开启,当客户端lib生成连接丢失,基本上有两种情况,当执行以下之一同步或异步操作时:
3.2.0新增 — SessionMovedException: 有个内部异常,客户端通常看不到,叫做SessionMovedException。这个异常出现是由于一个请求接收一个会话连接之前在其它服务器被重连。这个错误的合理解释是一个客户端发送请求到服务端,但由于网络延迟,因此客户端超时并连接到一个新服务器。(旧的连接通常已经关闭了),可以看到这种情况的一种情况是两个客户端尝试使用保存的会话ID和密码重新建立相同的连接。其中一个客户端将重新打开连接,另一个客户端将断开(导致它们尝试无限期地重新建立其连接/会话)
更新服务器列表 Zookeeper允许客户端更新连接字符串,通过提供一个新的逗号分隔的主机:端口列表,每个对应一个Zookeeper服务器。该函数调用概率负载平衡算法,该算法可能导致客户端与其当前主机断开连接,目标是在新列表中实现每个服务器的预期统一连接数。如果客户端连接的当前主机不在新列表中,则此调用将始终导致连接被删除。否则,决定是基于服务器数量是增加还是减少以及增加多少。
比如,如果先前的连接字符串包含3个主机,现在列表中包含这3个主机以及2个额外的主机。为了负载均衡,40%连接到这3台主机的每个客户端都将移动到新的主机。这个算法将造成客户端以0.4的概率丢失与当前主机的连接。在这种情况下,导致客户端随机连接到2个新主机之一。
另一个示例,假如我们有5个主机,现在更新列表来移除2个主机,连接到剩下3个主机的客户端将继续保持连接,然而连接到2个已移除的主机的客户端需要连接到剩下3台主机之一,这也是随机的。如果连接中断,客户端进入特殊模式,在那里,它选择一个新的服务器来连接,使用概率算法,而不仅使用轮询。
在第一个示例中,每个客户端决定断开的概率是0.4,但是一旦决定,它将尝试随机的连接到一个新的服务器,如果它连接不到任何一个新服务器,它将连接到旧的服务器。发现一个服务器之后,或者尝试连接所有新服务器失败后,客户端回到正常操作模式,它从连接字符串中挑出任意一个服务器企图去连接到它,如果失败,它将在轮询中继续尝试不同的服务器。
Zookeeper中所有读操作——getData(), getChildren(), 和 exists() ,都有设置一个watcher作为副作用的选项。以下是Zookeeper对watch的定义:一个watch事件是一次性触发,发送到设置了watch的客户端,这仅当设置了watch的数据发生变化,将会触发。这有3个关键点来思考watch的定义:
客户端连接到Zookeeper服务器的watch在本地维护。这使watches很容易设置、维护、以及转发。当客户端连接到一个新服务器,watch将会触发对于任何会话事件。当与服务器断开,将不会收到watch。当一个客户端重连,任何先前注册的watch将会被重新注册并且在需要时触发。一般来说,这一切都是透明的。这有一个watch可能会丢失的案例:如果znode已创建,并且丢失连接后删除,当已存在的znode还没有创建,一个watch将会丢失。
你能够设置watch用三个调用来读取ZooKeeper的状态:exists, getData, 和 getChildren。以下列表详细说明了watch可以触发的事件以及启用它们的调用
你能够移除znode中已注册的watch,通过调用removeWatches。除此之外,Zookeeper能够移除本地watch即使没有服务器连接,通过设置本地flag为true
关于watches,Zookeeper主要有以下保证:
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开头。
ZooKeeper支持以下权限:
CREATE和DELETE权限已从WRITE权限中删除,以获得更精细的访问控制。CREATE和DELETE的情况如下:
你希望A能够在ZooKeeper节点上执行设置,但不能CREATE 或 DELETE 子节点。
CREATE 没有 DELETE: 客户端创建请求通过创建zookeeper节点在父目录。你希望客户端能够添加,但只有请求处理器才能删除(这有点像文件的APPEND权限)。
此外,由于ZooKeeper没有文件所有者的概念,因此存在ADMIN权限。在某种意义上,ADMIN权限将实体指定为所有者。Zookeeper不支持LOOKUP权限(对目录执行权限位,即使无法列出目录也允许LOOKUP),每个人都隐含地拥有LOOKUP权限。
ADMIN权限在ACL方面也有特殊作用,为了检索znode的ACL,用户必须具有READ或ADMIN权限,但是没有ADMIN权限,摘要哈希值将被屏蔽掉。
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中。
这有两个内置的鉴权插件,ip 和 digest。其它的插件能使用系统属性添加。在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的一致性保证。
通过这些一致性保证,很容易建立更高级别的功能,比如领导选举,阻塞,队列,读/写撤销锁,仅需在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
Zookeeper的Java绑定由两个包组成:org.apache.zookeeper 和 org.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事件都是在事件线程中处理。对于这样的设计结果,有些事情需要注意:
最终,与关闭相关的规则很简单:一旦一个Zookeeper对象关闭或者接收一个致命的事件(SESSION_EXPIRED 和 AUTH_FAILED),Zookeeper对象变得不合理。在结束时,两个线程关闭而且对zookeeper的任何进一步访问都是未定义的行为并且应当避免。
以下列举出了Java客户端包含的配置属性。你能设置这些任意属性使用Java系统属性。请检查以下参考Server configuration section
首先创建一个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
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。