引言
这个文档是为了想利用ZooKeeper的协调服务来创建分布式应用的开发者提供的指南。它包括概念和实践的信息。
这个文档的一开始的的四部分呈现了不同ZooKeeper高级概念的的讨论。理解Zookeeper是怎么工作的和如何使用它同等重要。它不包含源码,但是它确实假设你熟悉分布式计算的问题。在这第一组的部分是:
- ZooKeeper数据模型
- ZooKeeper会话
- ZooKeeper监听
- 一致性保证
下面的四部分提供了实践编程信息。他们是:
- 构造阻塞:ZooKeeper操作的指南
- 绑定
- 程序结构,和简单的例子
- 陷阱:常见问题和故障排除
本书最后的附录包含其它有用的ZooKeeper相关的信息
本文档的大部分信息可以作为独立的参考材料。然而,在开始你第一个ZooKeeper应用之前,你应该至少阅读ZooKeeper数据模型和ZooKeeper基本操作的章节。同时,简单的编程例子对于理解ZooKeeper客户端应用的基本结构是不帮助的。
ZooKeeper有一个层次结构的命名空间,就像是一个分布的文件系统。唯一的不同是每一个节点可以有一个和它关联的数据,孩子节点也是。它好像是有一个文件系统允许文件是一个目录。节点的路径总是表达为一个典型的绝对的斜线分隔的路径:没有相对路径。任何nuicode字符可以被用在路径中同时受以下的约束:
- null字符(u0000)可能是路径名字的一部分。(在C绑定里面有问题)
- 下面的字符不能被使用因为它们不能被很好的展示,或渲染得很混乱: u0001 - u001F和u007F - u009F
- 下面的字段不允许:ud800 - uF8FF, uFFF0 - uFFFF
- "."字符可以被用来作为名字的一部分。但是"."和".."不能单独被用来表示一个节点的路径,因为ZooKeeper不能用相对路径。下面的将会是不合法的:"/a/b/./'c"或者”/a/b/../c“.
- "zookeeper"是保留字
ZNodes
每一个ZooKeeper树中的每一个节点被称为znode。Znodes维护了一个包括数据改变,acl改变的版本号的数据结构。这个数据结构同样也有时间戳。版本号和时间戳,允许ZooKeeper校验缓存和协调更新。每次znode的数据改变,版本号相应地增加。例如,
每当客户端检索数据,它同样也收到数据的版本号。当客户端执行一个更新或删除,它必须提供正在改变的znode的版本号。如果它提供了版本号和实际的版本号不匹配,更新将会失败。(这个行为可以被覆盖。更多信息请参考...)
注意
在分布式应用的工程中,单词node被可以称为一个真实的主机,一个服务器,一个集群中的成员,一个客户进程,等等。在ZooKeeper的文档中,znodes指的是数据节点。Servers指的是组成ZooKeeper服务的机器;quorum peers指的是组成集群的servers;
client指的是任何使用ZooKeeper服务的主机或进程。
Znodes是开发者主要访问的实体。他们有一些在这里值得提起的特性。
Watches
客户端可以在znodes上设置监视器(watches)。这个znode的改变将触发这个监视器然后清除这个监视器。当一个监视器被触发,ZooKeeper给客户端发送一个通知。更多关于监视器的信息可以在ZooKeeper Watches部分找到。
Data Access
存储在命名空间中的每一个znode的数据被原子性地读和写。读获取所有跟这个znode关联的所有数据,写替换所有的数据。每一个节点有一个访问控制列表(ACL)限制谁可以做和可以做什么 。
ZooKeeper不是设计用来作为一个数据库或大对象的存储。相反地,它管理协调数据。这些数据可以是配置,状态信息等等。不同形式的协调数据有一个共同的特性就是它们相对来说数据量小:以千字节为单位。ZooKeeper客户端和服务端实现必需检查确保znode的数据小于1M,但是数据平均来说应该小于这个值。对相对大的数据的操作将引起一些操作耗费比其它更多的时间并且将影响一些操作的延迟,因为需要更多的时间在网络上移动数据和移动到存储媒介上。如果需要存储大数据,通常处理这种数据的模式是把它们存储在容量存储系统上。例如NFS或HDFS,并且在ZooKeeper上存储指针。
Ephemeral Nodes
ZooKeeper也有短暂节点的概念。这些节点只要创建它的会话是活跃的就会一直存在。当会话结束的时候,这个节点就会被删除。因为这样的行为特性,短暂的节点不允许有孩子节点。
Sequence Nodes - 唯一命名
当创建一个znode你也可以要求ZooKeeper追加一个单调递增的计数器在路径的结尾。这个计数器对父节点来说是唯一的。这个计数器的格式是%010d -- 也就是说10位数和0(数字0)衬垫(不足10位补0)(计算器被格式化为这种形式是为了简化存储)。也就是"<path>0000000001"。参考Queue Recipe获取这个特性的例子。注意:被用来存储下一个序列数字的计算器是一个被父节点维护的有符号的(signed)int,计算器将会溢出当增加超过2147483647(产生一个名字”<path>-2147483647“)。
Time in ZooKeeper
ZooKeeper以多种方式记录时间:
- Zxid
每次改变ZooKeeper状态将会收到一个zxid形式的标记(ZooKeeper的事务Id)。这暴露了ZooKeeper的所有改变的总序列。每一个改变将有一个唯一和zxid并且如果zxid1比zxid2小那么zxid1在zxid2之前发生(happend before zxid2).
- 版本号
一个节点每改变一次将引起 这个节点的版本号增加一次。这三个版本号是version(znode的数据改变的次数),cversion(znode字节点的改变的次数),和aversion(znode节点的ACL改变的次数)。
- Ticks
当使用多服务器的ZooKeeper,服务器之间使用ticks来定义比如状态上传,会话超时,节点之前是连接超时等等的时间。tick时间只通过最小的会话超时时间来暴露(2倍的tick时间);如果一个客户端请求会话超时小于最小的会话超时时间,那么服务端将会告诉客户端真实的会话超时时间为最小的会话超时时间。
- 真实时间(Real time)
ZooKeeper不使用真实的时间或时钟时间,除了把时间戳在znode创建和修改的时候加入到数据结构。
ZooKeeper数据结构
ZooKeeper中的每一个znode的数据结构是由下面的字段组成的:
- czxid
创建znode时的zxid
- mzxid
最后修改znode的zxid
- ctime
从znode创建以来的毫秒时间
- mtime
从znode最后修改以来的毫秒时间
- version
znode数据改变的次数
- cversion
znode孩子节点改变的次数
- aversion
znode的ACL改变的次数
- ephemeralOwner
如果是一个短暂节点,这个值就是znode的拥有者的会话id。如果不是短暂节点,它的值为0。
- dataLength
znode的数据字段的长度
- numChildren
znode孩子节点的数量
ZooKeeper Sessions
一个ZooKeeper客户端通过使用一个语言绑定创建一个握手来建立和ZooKeeper服务的会话。一旦建立,处理器以CONNECTING状态开始并且客户端库试图连接组成ZooKeeper服务的其中一个服务端,这时它就变成CONNECTED状态。在正常操作下将会处于这两者之中的状态。如果发生不可恢复的错误,例如会话过期或授权失败,如果如果应用显式地关闭了这次握手。这次握手将会变成CLOSED状态。下面的图表展示了ZooKeeper客户端可能出现的状态转换。
为了创建客户端会话应用代码必需提供一个包括以逗号分割主机:端口(host:port)列表对组成的连接字符串,每一个对应一个ZooKeeper服务端(例如:"127.0.0.1:4545"或者”127.0.0.1:3000,127.0.0.1:3001,127.0.0.1:3002“)。ZooKeeper客户端库将挑选一个任一的服务端并且试图进行连接。如果这个连接失败,或者如果因为任何原因客户端断开连接,客户端就自动地尝试连接列表中的下一个服务端,直到(重新)建立一个连接。
3.2.0新加 一个可选的"chroot"后缀可以加入到连接字符串。这将会运行客户端命令并相对于这个root解释所有的路径(和nuix的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服务的每一个用户是不同根目录的多用户环境是非常有用的。这使重用变得更简单因为每一个用户可以改变他的/她的应用使它好像在"/"的根目录下,同时真实的路径(也就是 /app/a)可以被确定在部署的时候。
当客户端得到一个到服务端句柄,ZooKeeper创建一个ZooKeeper会话,以64位的数字表示,分配给客户端。如果客户端连接到不同的ZooKeeper服务端,它将会发送会话id作为连接握手中的一部分。作为一个安全措施,服务端为会话Id创建一个密码任何ZooKeeper服务端可以用来校验。这个密码和会话id被发送到客户端当连接建立的时候。当和新的服务端重新建立连接的时候客户端发送这个密码和会话id到服务端。
ZooKeeper客户端库创建ZooKeeper会话的参数中的一个是以毫秒表示的会话超时时间。客户端发送一个请求超时时间,服务端在超时时间内回复客户端。目前的实现要求这个超时时间最小是2位的tickTime(在服务端的配置文件里设置),最多20倍的tickTime.ZooKeeper客户端API允许访问这个超时时间。当一个客户端(会话)被ZK服务集群隔离开来,它将开始搜索在会话建立里的服务器列表。最终,当客户端和任一服务端的连接重新建立,这个会话将要么再次转变为“connected”状态(如果在会话超时时间内建立连接)要么转变为"过期"状态(如果在会话超时后建立连接)。不建议为断开建立一个新的会话对象(一个新的ZooKeeper.class或zookeeper句柄)。ZK客户端将为你处理重连。特别地我们把启发式算法内建到客户端库来处理像"羊群效应"(herd effect),等等,只有当你被通知了会话到期了才新建一个会话(强制性的)。
会话到期时间被ZooKeeper集群本身管理,而不是客户端。当客户端跟ZK集群建立一个会话,它提供一个"过期时间"值。这个值被集群决定客户端会话什么时候过期。过期发生当集群不能听到客户端的消息时在指定的会话超时周期内(也就是没有心跳)。当会话过期集群将会删除所有的属于这个会话的短暂节点并通知所有的链接的客户端这一改变(任何监视这些节点的客户端)。这时会话过期的会话的客户端仍然和集群处于断开情况。它将不会被通知会话过期只到/除非它和集群重新建立链接。客户端一直处于断开状态只到它和集群重新建立链接。这时过期会话的监视器将会收到"会话过期"通知。
对于一个会话过期的状态转换可以被过期会话的监视器看到的例子:
- 'connected':会话被建立并且客户端正在和集群通信(客户端/服务端的通信正在正常地操作)
- .... 客户端被集群隔离
- 'disconnected':客户端已经和集群失去链接
- .... 时间流逝,在'超时时间'周期过后集群使会话过期,客户端不会看到任何东西因为它已经和集群失去链接了
- .... 时间流逝,客户端恢复了和集群的网络层的连接
- 'expired':最终客户端恢复了和集群的链接,然后它被通知到已经过期。
另一个ZooKeeper的会话建立和参数是默认的监视器。监视器被通知当在客户端发生任何改变。例如如果客户端失去了服务端的连接它将会被通知,或者客户端的会话过期,等等。这个监视器应该考虑初始状态到失去连接的状态(也就是说在任何状态改变前事务被客户端库送到观察者)。在一个新连接的情况下,第一个送给观察者的事件通常是会话建立事件。
会话通过客户端发送请求保持存活。如果会话空闲一段超时会话的时间,客户端将发送一个PING请求来保持会话是活着的。这个PING请求不仅使ZooKeeper服务端知道客户端是仍然存活着,它也使客户端检验到ZooKeeper服务端的连接仍然活跃。PING的时机是相当保守以使确保合理的时间来检测一个死去的连接和重新连接一个新的服务端。
一旦到服务端的连接成功地建立(connected)这里有最基本的客户端库产生连接的两个例子,当或者同步或异步的操作被执行并且下面的其中一个持有:
- 应用在一个不在存活会话上调用一个操作
- ZooKeeper客户端和一个服务端断开连接当还有后续的操作作用到这个服务端,也就是说还有后续的异步调用。
3.2.0新加 -- SessionMovedException.有一个内部的异常叫做SessionMovedException,通常不被客户端看到。这个异常发生因为在一个连接中收到一个请求,这个会话已经被连接到一个不同的服务端。这个错误的通常原因是一个服务端发送一个请求到服务端,但是网络包有延迟,所有客户端超时并且连接到一个新的服务端。当延迟的数据包到达了第一个服务端,老的服务端检测到这个会话已经移动 了,并且关闭客户端连接。客户端通常不会看到这个错误因为他们不会从这个老的连接中读数据(老的连接通常已经关闭)。这个条件可以被看到的情况是当两个客户端试图重新建立相同连接用一个保存的会话id和密码。其中一个客户端将重新建立连接而另一个将被断开连接(导致试图重新连接它的会话的对无期限地)
Updating the list of servers 我们允许一个客户端通过一个新的逗号分隔的host:port列表对来更新连接字符串,这每一个对应一个ZooKeeper服务端。这个功能调用 一个概率的负载均衡逻辑,这个逻辑可能引起客户端和它的当前主机断开连接,以便达到新列表的每一个服务端都有均一连接数。如果当前连接的主机不在新的列表里面,那么这次调用将总是引起这个连接被丢弃。否则,这个决定是基于是否服务端的数量是增加了还是减小了和增加减小了多少。
例如,如果选择的连接字符串包括3个主机并且现在 的列表包含3个主机和2个新主机。3个主机中的每一个主机的40%将转移到新主机中的一台为了平衡压力。这个逻辑将导致这个客户端有40%的概率丢掉当前连接的主机,并且在这个例子当中这个客户端连接到2个新主机中的一个,随机选择。
另一个例子 -- 假如我们有5个主机,现在更新这个列表来删除其中两个主机,剩下的3个主机的连接仍然保持连接,然后所有连接到删除的两个主机的连接将需要移动到剩下3台中的一台,随机选择。如果连接被丢掉,客户端移动到一个特殊的模式,它利用概率算法来选择一个新的主机,而不仅仅是轮询。
在第一个例子中,第一个客户端有40%的概率决定断掉连接,如果这个决定确定,它将试图随机连接一个新主机,并且只有它不能连接到任何一个新主机的时候,它将试图连接老的主机。在找到一个服务端之后,或者尝试了所有新列表中的服务端之后并且连接失败,客户端返回普通操作模式,它从连接字符串选择任一一个服务端并且尝试连接它。如果失败,它将轮训地尝试不同的主机。(参考上面开始选择服务端的逻辑)
ZooKeeper Watches
所有ZooKeeper中的读操作 - getData(),getChildren(),和exist() - 有一个设置监视器选项。这里是ZooKeeper的监视器的定义:一个监视器事件是一次性触发,发送给设置这个监视器的客户端,这个事件当设置监视器的数据发生改变的时候发生。监视器的定义有三个关键点需要考虑:
- 一次性触发
一个监听事件将会被发给客户端在数据已经改变的时候。例如,如果一个客户端做了一个getData("/znode1", true)操作,然后/znode1的数据被改变或删除,客户端将等到一个/znode1的监听事件。如果/znode1再次改变,将没有监听事件被发生,除非客户端做了另一个读操作并且设置 一个新的监视器。
- 发送给客户端
这意为着一个事件正在发送给客户端的路上,但是可能还没有到达客户端在成功返回之前改变操作到达客户端之前。监视器被异步地发送给监听听。ZooKeper提供了一个顺序保证:一个客户端将不会看到它设置监视器的数据的改变直接它看到了监视事件。网络延迟或其它因素可能导致不同看到监视器并且返回代码在不同的时候点。关键点是不同的客户端看到的任务东西都是有顺序的。
- 设置监视器的数据
这是指一个节点可以以不同的方式改变。它有助于把ZooKeeper想像成一个维护两个监视器的列表:数据监视器和孩子监视器。getData()和exists()设置数据监视器。getChildren()设置孩子监视器。另外,它可能有帮助的根据数据返回的类型想像 正在设置的监视器。getData()和exixts()返回关于这个节点的信息,然而getChildred()返回一个孩子的列表。因此,setData()将触发给znode设置的数据监视器(假设成功设置)。一个成功的create()将触发正在被创建的这个znode的数据监视器,父节点的孩子监视器。一个成功的delete()将触发正在被删除的znode的数据监视器和孩子监视器(因为没有了孩子节点)同时也触发这个被删除节点的父节点的孩子触发器。
监视器被维护在客户端连接的ZooKeeper服务端的本地。这使监视器设置,维护,和分发都很轻量。当一个客户端连接到一个新的服务端,监视器将会触发任何会话事件。监视器将不会被收到当正在和服务端处于断开状态。当一个客户端重新连接,先前注册的监视器将被重新注册如果需要被触发。通常这些透明地发生。有一种情况一个监视器可能丢失:还没有创建的znode的存在性的监视器将被丢失如果znode被创建并且删除在处于断开连接的时候。
监视器的语义
我们有三个读取ZooKeeper状态的的调用可以设置监视器:exists(),getData(),和getChildren().下面的列表详细说明了一个监视器可以触发的事件和可以触发的调用:
- 创建事件:
调用exists()时候触发。
- 删除事件:
调用exists(),getData(),和getChildren()的时候触发。
- 改变事件:
调用exists()和getData()的时候触发
- 孩子事件:
调用getChildren的时候触发
删除监视器
我们可能调用removeWatches来删除注册到一个znode的监视器。同时,ZooKeeper客户端可以在本地删除监视器即使没有服务器跟它连接通过设置本地标志为true.下面的列表详细描述了将被触发的事件在成功删除监视器之后。
- 孩子删除事件
调用getChildren增加的监视器
- 数据删除事件
调用exists或getData增加的监视器
有关Watches的ZooKeeper担保
关于监视器,ZooKeeper维护三个担保
- 监视器被排序和其它事件,其它监视器和异步回复。ZooKeeper客户端库确保分发的所有事件 都是有序的。
- 一个客户端将看到它正在监视的znode的监视事件在它看到这个znode的新整数之前。
- ZooKeeper的监视事件的顺序和被ZooKeeper服务端的看到的更新顺序一一对应。
关于监视器需要记住的事
- 监视器是一次性触发器;如果你得到一个监听事件,并且你想在以后的改变时被通知,你必须设置另一个监视器。
- 因为监视器是一次性触发器并且在获取事件和发送新请求来获取一个监视器之前有延迟。你不能可靠地看到ZooKeeper中的znode的每一个改变。请准备处理在获取事件和设置再次设置监视器之间znode改变很多次的情况。(你可以不关心,但最少知道它可能发生)。
- 一个监视器对象,或者函数/上下文对,对于一个给定的通知将会被触发一次。例如,如果给相同的文件的exists和getData调用设置相同的监视器对象,并且这个文件随后被删除,那么这个监视器对象将只被触发一次这个文件删除的通知。
- 当你和一个服务端断开连接(例如,当服务端失败),你将不会得到 任何监视器只到连接被重新建立。因为这个原因会话事件被发送给所有未完成的监视器处理器。使用会话事件进入一个安全模式:你将不会收到事件在断开连接的时候,所以你的进程在这个模式下应该保守地采取行动。
使用ACLs的ZooKeeper的访问控制
ZooKeeper使用ACLs来控制对znodes的访问(ZooKeeper的数据树的数据节点)。ACL的实现和UNIX文件访问权限非常相似:它用权限位来允许/不允许对应节点的不同的操作和权限位应用的范围。和标准的NUIX权限不同的是,ZooKeeper节点没有对用户,组,傲世界(其它的)的三个标准的范围限制(文件的拥有者)。ZooKeeper没有znode拥有者这种概念。相反地,ACL指定了一组id和这些id关联的权限。
也注意ACL只适用一些特定的znode.特别地它没有应用到孩子。例如,如果/app 只对ip:172.16.16.1可读并且/app/status是全局可读的,任何人将可以读取/app/status;ACL是不可递归的。
ZooKeeper支持可插拔的认证方案。Ids被以cheme:id的形式指定,这里scheme是id对象的认证方案。例如, ip:172.16.16.1是主机172.16.16.1的id.
当客户端连接到一个ZooKeeperu并且认证了它自己,ZooKeeper把客户端连接关联到这个客户端对应的id上。这个ids根据znode的ACLs被检查当客户端试图访问一个节点的时候。ACLs被以成对的(scheme:expression, perms)组成。expression的格式对scheme来说是特定的。例如,(ip:19.22.0.0/16, READ)对任何ip地址以19.22开头的客户端有读权限。
ACL权限
ZooKeeper支持如下的权限:
- CREATE:你可以创建一个节点
- READ:你可以从这个节点和这个节点的孩子列表获取数据
- WRITE:你可以设置一个节点的数据
- DELETE:你可以删除这个节点
- ADMIN:你可以设置权限
对于细粒度的访问控制CREATE和DELETE权限已经被WRITE权限给打破。下面是CREATE和DELETE的例子:
你希望A可以设置ZooKeeper节点的值,但是不能创建或删除子节点。
没有DELETE的CREATE:客户端创建请求通过在父节点创建ZooKeeper节点。你想所有的客户端可以增加,但是只有请求进程可以删除。(这就好像文件的APPEND权限)
同时,ADMIN权限在这里是因为ZooKeeper没有一个文件所有者的概念。在某种意义上ADMIN权限指明了拥有者。ZooKeeper不支持LOOKUP权限(在目录上的执行权限位允许你LOOKUP即使你不能罗列这个目录)。每一个人显式地拥有LOOKUP权限。这允许你一个节点,但是不会有更多。(问题是,如果你想在不存在的节点上调用zoo_exists(),没有任何权限检查)
内置的ACL方案
ZooKeeper有以下内置的方案:
world有一个单独的id,anyone,这代表任何人。
auth 不使用任何id,表示任何授权的用户
digest 使用一个 username:password字符串来生成 一个MD5哈希。然后被用来作为ACL ID标示。授权通过发送一个明文username:password来完成。当在ACL中使用这个表达式将会是username:base64 encoded SHA1 password digest
ip 用客户端的主机ip作为ACL ID标示。ACl表达式addr/bits,这里的addr的前bits位和客户端主机ip的前bits位来匹配。
-----------------------------------------------------这里有一段关于c语言的部分被省略了--------------------------------------------------------------------
可插拔的ZooKeeper认证
ZooKeeper运行不同的认证方案的不同的环境中,所以它有一个完全可插拔的认证框架。甚至内置的认证方案也是用的这个可插拔的认证框架。
为了理解这个认证框架是怎么工作的,首先你必须理解两个主要的认证操作。框架首先必需认证客户端。这个通常一旦客户端连接服务端的时候被完成并且包含从客户端发送的校验信息并且把这些和连接关联起来。第二个被框架处理的操作是在ACL中找一个对应这个客户端的条目。ACL条目是<idspec, permissions> 对。idspec可能是一个简单的string和匹配这个连接关联的认证信息或者它可能是一对这个信息计算的表达式。这取决于认证插件做这个匹配的具体实现。
这里是认证插件必须实现的接口:
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服务端使用通过认证插件返回的scheme来决定scheme应用到那一个id。
handleAuthentication被调用当一个客户端发送被关联到这个连接的认证信息时。客户端指定这个信息对应的scheme.ZooKeeper服务端传递这个信息给认证插件。插件 getScheme匹配被客户端传过来的scheme。handleAuthentication的实现者通常将返回一个错误如果它确定信息是坏的,或者它将把这个信息和这个连接关联起来使用cnxn.getAuthInfo().add(new Id(getScheme(), data))
认证插件被参与到设置和使用ACLs,当一个ACL被设置给一个znode,ZooKeeper服务端将传递条目的id部分给 isValid(String id) 方法。它取决于插件来验证id是否是一个正确的形式。例如,ip:172.16.0.0/16 是一个合法的id,但是ip:host.com不是。如果新ACL包含一个"auth"条目,isAuthenticated 被用来查看是否跟这个连接关联的这个scheme的认证信息应该被加到这个ACL。一些schemes不应该包含在auth。例如,客户端的IP地址不被认为应该被作为id加入到ACL如果auth被指定。
ZooKeeper在检查ACL的时候调用matches(String id, String aclExpr)。它需要这个客户端的认证信息和相关的ACL条目匹配。为了找一个应用这个客户端了条目,ZooKeeper服务端将找到每一个条目的scheme并且如果有这个scheme的认证信息,matches(String id, String aclExpr)将被调用,然后id赋值给先前被handleAuthentication和aclExpr设置ACL条件的id被加入到连接的认证信息。认证插件使用它自己的逻辑并且匹配scheme来决定是否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的一致性保证:
顺序的一致性
从客户端来的更新将被按照它们发送的顺序应用。
原子性
更新要么成功要么失败 -- 没有部分结果。
单系统镜像
客户端将会看到服务端相同的视图不管它连接的是那一个服务端
可靠性
一旦更新被应用,它将会被留存到直到一个客户端覆盖这个更新。这个担保有两个结果:
- 如果客户端得到 一个成功的返回结果,这个更新已经被应用。在一些失败(通信错误,超时,等等)客户端将不会知道是否更新被应用。我们采取措施来减小这种失败,但是这担保是在成功返回的时候出现。(在Paxos中这被称为单调性条件)。
- 任务被客户端看到的更新,带过读请求或成功更新,将永远不会被回滚当从服务端失败恢复过来的时候。
时效性
系统的客户端视图在一定的时限内(几十秒的顺序)保证是最新的。要么系统改变将被客户端看到,要么客户端将检测到服务端过期。
利用这些一致性保证,非常容易构建高级别的功能,例如领导者选举,屏障,队列和读写可撤销锁在ZooKeeper客户端(对ZooKeeper来说不需要更多)。更详细的信息请参考Recipes and Solutions。
注意
有时候开发者错误地认为另一个保证ZooKeeper没有实现。它就是:
同时一致的跨客户端视图
ZooKeeper没有及时地在每一个实例上做保证,两个不同的客户端将有ZooKeeper数据一致的视图。因为像网络延迟之样的因素,一个客户端可能在另一个客户端获取这个改变之前执行一个更新。考虑有两个客户端A和B的场景。如果客户端A设置节点/a的值从0到1,然后告诉客户端B读取/a,客户端B可能读到老的数据0,取决于它连接是那一个服务端。如果客户端A和B读到相同的值非常重要,客户端B应该调用sync()方法在它执行读操作之前。
所以,ZooKeeper本身不保证改变在所有的服务端两步发生,但是ZooKeeper原语可能被用来构造更高级的功能来提供有用的客户端同步。(更多信息请参考ZooKeeper Recipes)。
绑定(Bindings)
ZooKeeper客户端库由两种语言:Java和C.下面的部分描述这些内容。
Java Binding
ZooKeeper的Java bingding有两个包:org.apache.zookeeper 和 org.apache.zookeeper.data。组成ZooKeeper的剩下的其它包被用来在内部使用或是服务端实现的一部分。org.apache.zookeeper.data包被生成的classes组成,这些类被简单地用作容器。
被ZooKeeper Java客户端使用的最主要类是ZooKeeper类。它的两个构造器只是在可选的session id和password上不同。ZooKeeper支持在整个进程实例中会话恢复。Java程序可以保存它的session id和password到一个完全的存储设备上,重启,并且恢复这个被先前程序的实现使用的会话。
当一个ZooKeeper对象被创建,两个线程也被创建:一个IO线程和一个事件线程。所有IO操作都在IO线程(NIO)上发生。所有事件调用发生在事件线程上。例如到ZooKeeper服务端的重连的会话维护和维护心跳是在IO线程上完成。同步方法的回复也是在IO线程中处理。所有异步方法的回复和监听事件在事件线程中处理。这种设计有一些事情需要注意:
- 所有异步调用的完成和监视器回调将被按顺序完成。一次一个。调用者可以做它想做的任何处理,但是在这期间没有其它回调将被处理。
- 回调不会阻塞IO线程的处理或者同步调用的处理。
- 同步调用可能不以正确的顺序返回。例如,假如一个客户端这个如下的处理:发起一个对节点/a的异步读并且设置watch为true,然后在这个读完成的回调中它做了一个同步的/a读。(可能不是一个好的实践,但是也不非法,并且它只是一个简单的例子)。注意如果在异步读和同步读之间有一个改变,客户端库将会收到一个监视器事件说/a被改变在同步读之前,但是因为完成回调正在阻塞事件队列,同步的读将会返回/a的新值在监视器事件被处理之前。
最后,与关闭相关的法则是直接的:一旦一个ZooKeeper对象被关闭或者收到一个致使的事件(SESSION_EXPIRED 和 AUTH_FAILED),ZooKeeper对象变得无效。在关闭时,两个线程关闭并且任何对ZooKeeper的进一步的访问是未定义的行为并且应该被避免。
-------------------------------------有关C的内容被忽略---------------------------------------------
Building Blocks: ZooKeeper操作指南
本节调查一个开发人员可以对ZooKeeper服务端执行的所有操作。这相比前面的概念章节是比较低级别的信息,但是比ZooKeeper API手册高级一些。它包含这些主题:
- Connecting to ZooKeeper
错误处理
Java和C客户端绑定都可能报告错误。Java客户端绑定通过抛出KeeperExecption来报告错误,调用异常的code()方法将返回指定的错误代码。C客户端绑定返回一个ZOO_ERRORS枚举的错误代码。API回调表示两种语言绑定的返回代码。更多信息请参考API文档关于可能的错误和它们的意思。
Connecting to ZooKeeper
Read Operations
Write Operations
Handling Watches
Miscelleaneous ZooKeeper Operations
Program Structure, with Simple Example
陷阱:常见问题和故障排除
现在你了解了ZooKeeper。它使你的应用很快,简单,但是等等。。。有一点问题。这里有一些ZooKeeper用户可能要掉进去的陷阱:
- 如果你正使用监视器,你必需寻找连接的监听事件。当一个ZooKeepe客户端和一个服务端断开,你将不会收到这一改变的通知直到你重新连接。如果你正监视一个znode是否存在,你将错过这一事件如果znode在你断开连接的时候被创始和删除。
- 你必需测试ZooKeeper服务端失败。ZooKeeper服务端可能从失效中存活只到大多数的服务端是活跃的。要问的问题是:你的应用程序可以处理它么?在真实世界一个客户端到ZooKeeper的连接可能断掉。(ZooKeeper服务端失效和网络隔离是连接丢失的常见原因)ZooKeeper客户端库负责处理你的连接恢复和让你知道发生了什么 事,但是你必须确保你恢复了你的状态和任何失败的没有处理的请求。在测试 环境中找到是否你是对的,而不是在生产环境 - 测试由一些服务端组成的ZooKeeper服务并且重启他们。
- 被客户端使用的ZooKeeper服务端列表必须和每一个ZooKeeper服务端拥有的ZooKeeper服务端列表匹配。如果客户端列表是真实ZooKeeper服务端列表的子集,事件可以工作,尽管不是太完美。但是不能是客户端的ZooKeeper服务端列表不在ZooKeeper集群里。
- 关于你在那里存放事务日志应该小心。ZooKeeper的性能最关键部分是事务日志。ZooKeeper必须在返回响应之前同步事务到一个媒介中。一个专门的事务日志设备是一贯性能良好的关键。把日志放在一个比较繁忙的设置上将会影响性能。如果你只有一个存储设备,把日志文件放在NFS并且增加快照数量;它没有消除这个问题,但是它能减轻它。
- 正确地设置你的Java最大堆的值。避免交换是非常重要的。不必要的进入磁盘将几乎肯定地降低你的性能。记住,在ZooKeeper中,所有事件都是有序的,如果如果一个请求点击磁盘,所有的其它请求也点南磁盘。为了避免磁盘交换,尝试设置你拥有的物理内在的数量,再减去操作系统和缓存需要的值。最好的决定最优的堆大小的方法是运行压力测试。如果因为一些原因你不能运行压力测试,保守估计并且选择一个低于引起你机器交换的值。例如,在一个4G内在的机器上,3G堆大小是一个保守的估计。
正式文档除外,有一些ZooKeeper开发者的其它的信息资源。
ZooKeeepr白板[tbd:find url]
Yahoo! Research的ZooKeeper设计和性能的最终讨论
API Reference [tbd: find url]
ZooKeeeper API的完成参考手册
ZooKeeper Talk at the Hadoup Summit 2008
A video introduction to ZooKeeper, by Benjamin Reed of Yahoo! Research
Flavio Junqueira的相当好的Java指南,使用ZooKeeper实现简单的屏障和生产-消费者队列
ZooKeeper - A Reliable, Scalable Distributed Coordination System
An article by Todd Hoff (07/15/2008)
Pseudo-level discussion of the implementation of various synchronization solutions with ZooKeeper: Event Handles, Queues, Locks, and Two-phase Commits.
[tbd]
Any other good sources anyone can think of...
插播个广告
老丈人家的粉皮儿,农产品,没有乱七八糟的添加剂,欢迎惠顾