对于想要利用ZooKeeper的协调服务来创建一个分布式应用的开发人员来说,这篇文章提供了指导。包含了一些概念和实际性操作的信息。

这篇文章的前四个章节介绍了各种ZooKeeper的概念,这对理解ZooKeeper是怎么工作的是必须的。没有包含源代码,但是它假设你对分布式处理有关的问题比较熟悉。这四个章节是:

  • ZooKeeper数据模型

  • ZooKeeper 会话

  • ZooKeeper Watches

  • 一致性保证

随后的四个章节提供了实际的编程信息,他们是:

  • 构建块:ZooKeeper操作指南

  • 绑定

  • 程序结构,简单的例子[tbd]

  • 陷阱:常见问题和故障排查

ZooKeeper数据模型

ZooKeeper有一个层级命名空间,和一个分布式文件系统非常相似。唯一的不同是每个节点可以有关联的数据,子节点也是。就像有一个文件系统并且允许文件是一个目录。一个规范的、绝对的、斜杠分隔的路径来表示一个节点路径,没有相对路径。任何符合下列约束的的unicode字符可以被使用:

  • null字符串(\u0000)不能是一个路径名称。

  • 下列字符不能被使用,因为不能很好的被展示:\u0001 – \u001F 和 \u007F – \u009F。

  • 下列字符是不允许的:\ud800 – uF8FF, \uFFF0 – uFFFF。

  • “.”字符可以作为另一个名字被使用,但是“.”和“..”不能单独使用来表示一个节点路径,因为ZooKeeper不使用相对路径,下列是无效的:”/a/b/./c”或者 “/a/b/../c”。

  • “zookeeper”标记被保留。

ZNodes

在ZooKeeper树中的每个节点被称为一个znode。Znodes包含了一个stat数据结构,这个数据结构包括了数据变更的版本号、acl变更。stat数据结构也有时间戳,版本号和时间戳一起来允许ZooKeeper校验缓存和协调更新。每当一个znode的数据改变,版本号就会增加。例如:当一个客户端取得数据,它同样也接受数据的版本。并且,当一个客户端执行一个更新或删除操作,它必须提供数据的版本号。如果客户端提供的的版本号和实际的版本号不匹配,更新操作将会失败。(该行为可以被覆盖。更多信息参见)[tbd…​]

注意

在分布式应用应用中,node一词可以用来表示一台主机、一个服务器、集中中的一个、一个客户端进程等等。在ZooKeeper文档中,znodes 表示一个数据节点,Servers表示组成ZooKeeper服务中的机器,quorum peers 表示组成集合的机器,客户端表示使用ZooKeeper服务的任何主机或进程。

Znodes是一个程序员访问的主要实体,有许多在这里值得提到特性。

Watches

客户端可以在znodes上设置监听器,znode的改变触发这个监听器然后清空这个监听器。当一个监听器被触发,ZooKeeper发送给客户端一个通知。更多信息可以查看后面的Zookeeper Watches章节。

数据访问

每个znode的上存储的数据读写都是原子的,读操作取出所有的和这个znode有关的所有数据,写操作替换所有的数据。每个节点有一个访问权限列表(ACL)来限制谁可以做这些事情。

ZooKeeper没有被设计成一个一般的数据库或一个大型对象存储。它管理协调数据,数据可以是配置、状态信息、集合点等的形式。各种各样的数据有一个共同的属性就是他们都很小:以千字节为标准。ZooKeeper客户端和服务器有一个健康检查来确保znodes的数据少于1M,但是数据平均应该更小。操作较大的数据将导致一些操作花费更多的时间,并且会影响一些操作的延迟,因为在网络和存储媒介中移动更多的数据将需要额外的时间。如果需要存储大数据,通常的处理是把数据存储在一个大容量存储系统中,并把存储位置的指针存储到ZooKeeper上。

临时节点

ZooKeeper也临时节点的概念。这些znodes存活的时间和创建这个节点的会话有效期是一样的。当会话结束,节点被删除。因为这种临时节点的特性,临时节点不允许有子节点。

顺序节点——唯一名称

当创建一个节点的时候,也可以请求ZooKeeper在路径后面增加一个自增的计数器。对父节点来说,这个计数器是 唯一的。计数器是%010d的格式——是一个十位数,比如:<path>0000000001。

查看Queue Recipe使用这个特性的示例,注意:这个计数器用来存储下一个序列号是一个4字节的数,当增加到2147483647 之后,计数器会溢出。

ZooKeeper中的时间

ZooKeeper以多种方式跟踪时间:

Zxid

ZooKeeper状态的每次变化都接收一个zxid(ZooKeeper事务id)形式的标记。这个展示了所有的ZooKeeper的变更顺序。每次变更会有一个唯一的zxid,如果zxid1小于zxid2说明zxid1在zxid2之前发生。

Version numbers

节点的每次变化都会引起这个节点版本号之一的一次增加。这三个版本号是:version(一个节点的数据变化次数),cversion(一个节点的子节点变化次数),aversion(一个节点的ACL 变化次数)。

Tricks

当使用多个ZooKeeper服务,服务器使用ticks来确定事件的时间,比如说状态上传、会话超时、连接超时等。这个tick时间仅仅通过最小会话超时时间间接的暴露出来;如果一个客户端请求会话的超时时间小于最小超时时间,服务器将会告诉客户端实际的会话超时时间是最小超时时间。

Real Time

ZooKeeper不使用实时、时钟时间。除了把时间戳放在stat结构中。

ZooKeeper Stat 结构

每个节点的Stat结构由下列字段组成:

czxid

该数据节点被创建时的事务id。

mzxid

该节点最后一次被更新时的事务id。

ctime

节点被创建时的时间。

mtime

节点最后一次被更新时的时间。

version

这个节点的数据变化的次数。

cversion

这个节点的子节点 变化次数。

aversion

这个节点的ACL变化次数。

ephemeralOwner

如果这个节点是临时节点,表示创建者的会话id。如果不是临时节点,这个值是0。

dataLength

这个节点的数据长度。

numChildren

这个节点的子节点个数。

ZooKeeper会话

通过使用一种语言绑定来创建服务端的句柄,一个ZooKeeper客户端可以和ZooKeeper服务创建会话。一旦创建,句柄开始在CONNECTING 状态,客户端库尝试连接组成ZooKeeper服务中的其中一个服务器并且切换到CONNECTED状态。在正常的操作期间将会是这两种状态之一。如果一个不可恢复的错误发生了,比如说会话过期或授权失败,或者如果应用显示地关闭了句柄,句柄将会到CLOSED状态。下面的图展示了一个ZooKeeper客户端可能的状态转变。

state dia.jpg

为了创建一个客户端会话,应用程序代码必须提供一个连接字符串列表以逗号分隔开,主机:端口号成对出现,每个都相当于一个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”后缀可以被加在连接字符串后面,这会运行客户端命令导致所有的路径都和这个跟路径相关。如果使用像下面的示例:”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创建了一个会话,表现为一个64位的数字,并把 它分配给客户端。如果客户端连接到一个不同的服务端,在连接握手的时候它将发送这个会话id。作为一个安全措施,服务端给会话id创建了一个密码,让服务端能够校验。当客户端建立会话的时候,这个密码随着会话id一起发送给客户端。每当客户端与一个新的服务端恢复会话的时候,密码会随着会话id一起发送过去。

客户端调用创建会话的时候有一个参数是会话超时时间(毫秒),客户端发送一个要求的超时时间,服务端回复一个他能给客户端的超时时间。当前实现要求超时时间至少是2倍的tickTime,最大是20倍的tickTime。ZooKeeper客户端API允许使用一个协商的超时时间。

当一个客户端从ZK服务集群成为分区,它将开始寻找在会话创建时期指定的服务端列表。最终,当客户端和至少一个服务端联通重新建立的时候,会话要么转变成“connected”状态(如果在会话超时时间内恢复连接),要么转变成“expired”状态(如果在超时时间之外恢复连接)。在断开时创建一个新的会话是不可取的。ZK客户端库将处理连接。尤其是客户端内部有方法来处理像“羊群效应”之类的事情。仅仅在你被通知会话过期的时候去创建一个新的会话。

ZooKeeper集群自己管理会话过期,而不是由客户端管理。当ZK客户端和一个集群建立会话,它提供一个“超时时间”。这个值被集群使用来决定客户端的会话是否过期。当集群不能在指定的会话超时时间内从客户端收到信息,过期发生。在会话过期期间,集群将删除由这个会话创建的所有的临时节点,并且立即通知连接的客户端这个改变。此时,会话过期的客户端依然和集群式断开的,它不会收到通知直到它能和集群重新建立连接。这个客户端将保持断开状态直到和集群的TCP连接重新建立,并且在这个时候,过期会话的监听将会收到“会话过期”通知。

对于一个过期的会话,监听器所看到的状态转变:

  1. “connected”:会话被建立,并且客户端能和集群交流

  2. ……客户端从集群被分割

  3. “disconnected”:客户端与集群丢失了联系

  4. ……时间流逝,在超时时间之后,集群已经让这个会话过期,而客户端没看到什么,因为它已经从集群断开连接了

  5. ……时间流逝,客户端恢复网络和集群联通

  6. “expired”:最后客户端与集群重新连接,然后收到过期的通知

ZooKeeper会话建立的另一个参数是默认监听器。当客户端的一些状态改变发生,监听器会收到通知。比如如果客户端丢失与服务端的连接,客户端将会收到通知,或客户端的会话到期等。这个监听器应该考虑初始状态到断开连接。对于一个新的连接,第一给发给监听器的事件就是会话连接事件。

客户端通过发送请求保持会话存活。如果会话在一段时间内空闲将会导致会话超时,客户端将会发送PING请求保持会话存活。这个PING请求不仅仅让ZooKeeper服务端知道客户端是存活的,而且让客户端检查它的和ZooKeeper 服务端的连接也是存活的。PING的时间是足够保守的合理时间,来发现死掉的连接和一个新的服务端重新连接。

一旦成功建立一个到服务端的连接,当客户端发生connectionloss异常 时有两种基本的情况,在执行一个同步或者非同步的操作时:

  1. 应用调用一个操作,但是会话不再存活。

  2. 当等待一个操作的时候ZooKeeper客户端从服务端断开连接,比如说:等待一个异步调用。

3.2.0新增——SessionMovedException。有一个内部的异常,通常不会被客户端发现,被称为SessionMovedException。一个已经连接的会话但是重新连接到了一个不同的服务器上接收了一个请求,这个异常就会发生。这个错误的正常原因是一个客户端发送了一个请求到一个服务端,但是网络数据包延迟了,所以客户端超时并连接到了一个新的服务器。当延迟的数据包到达了第一个服务器,这个服务端发现这个会话已经被移除了并且关闭了这个客户端连接。客户端一般不会发现这个错误因为它们不在从老的连接读取数据(老的连接一般被关闭了)。这种事情发生的另一种情况是当两个客户端使用一个保存的会话id和密码来尝试恢复相同的连接时,只有一个客户端能够恢复连接,另一个客户端将会断开。

更新服务器列表。我们允许一个客户端更新连接字符串通过提供一个新的逗号分隔的主机:端口号列表,每个都是一个服务器。函数调用一个概率负载均衡算法会引起客户端断开与当前主机的连接,来使在新列表中的每个服务器达到与预期一致的数量。万一客户端连接的当前主机不在新的列表中,这个调用会引起连接被删除。另外,这个决定基于是否服务器的数量增加或减少了多少。

比如说,如果之前的连接包含三个主机,现在的连接多了两个主机,连接到每个主机的客户端的40%为了负载均衡将会移动到新的主机上去。这个算法会引起客户端断掉它当前与服务器的连接,这个概览是0.4,并且客户端随机选择两个新主机中的一个连接。

另一个例子,假设我们有5个主机,然后现在更新列表移除两个主机,连接到剩余三台主机的客户端依然保持连接,然而所有连接到已被移除主机的客户端都需要移到剩下三台主机的一台上,并且这种选择是随机的。如果连接断开,客户端进入一个特殊的模式并使用概率算法选择一个新的服务器,而不仅仅只是循环。

在第一个例子中,每个客户端决定断开连接的概率为0.4,但是一旦做了决定,它将会随机的连接到一个新的服务器,仅仅当它不能连接到任何一台新的服务器上时,它将尝试连接旧的服务器。当找到一个服务器或者新列表中所有的服务器都连接失败的时候,客户端回到操作正常模式,选择一个任意的服务器并尝试连接它,如果连接失败,它会继续尝试不同的随机的服务器,并一直循环下去。

ZooKeeper监听器

ZooKeeper中所有的读操作——getData(), getChildren()exists() — 可以选择设置 一个监听器。这是ZooKeeper’s一个监听器的定义:一个监听事件是一次性触发,当一个被设置监听的数据改变时,发送给设置这个监听器的客户端。在这个监听器的定义中,有三个要点:

  • 一次性触发:当数据改变的时候一个监听事件会被发送给客户端。比如说,如果一个客户端做了getData(“/znode1″, true)操作,然后 /znode1下的数据被改变或者删除了,客户端将得到/znode1的一个监听事件。如果/znode1节点再次发生改变,没有监听事件会被发送除非客户端做了别的设置了一个新的监听器。

  • 发送到客户端:这意味着事件正在发送给客户端的途中,但是在操作成功的返回码到达发起这个变更操作的客户端之前,事件可能还没到达监听的客户端。ZooKeeper提供了一个有序保证:在它第一次看到监听事件之前,它永远不会看到它设置的监听改变。网络延迟或别的因素可能会引起不同的客户端看见监听器和更新操作的返回码,在不同的时间。关键得一点是不同的客户端看见的每件事有一个一致的顺序。

  • 被设置监听的数据:这是指一个节点能变化的不同方式。可以认为ZooKeeper有两个监听器列表:数据监听和子节点监听。getData()和exists()设置数据监听器。 getChildren()设置子节点监听器。二选一,根据返回数据的类型来设置监听器。getData()和exists()返回节点的数据信息,然而getChildren()返回一个子节点列表。因此,setData()会触发数据监听器。一个成功的 create()会触发一个数据监听器。一个delete()会触发数据监听器和子节点监听器。

在ZooKeeper服务器中,当客户端连接的时候,监听器被保存在本地。这使得监听器轻量级的被设置、保存、分发。当一个客户端连接一个新的服务器,监听器会触发一些会话事件。当从服务器断开连接的时候,不会受到监听器。当一个客户端重新连接,如果需要的话,之前注册的监听器会被注册和触发。有一个监听器可能丢失的情况:如果在断开连接期间,一个节点被创建和删除,一个已存在的节点的监听器还没有创建,将丢失。

监听器的语义

我们能在三种调用读取ZooKeeper状态的情况下设置监听器:exists,getData和getChildren,下面的列表是一个监听器触发的事件的详细情况:

  • 创建事件:exists的调用

  • 删除事件:exists,getData和getChildren的调用

  • 改变事件:exists,getData的调用

  • 子节点事件:getChildren的调用

移除监听器

我们可以调用removeWatches来移除一个注册在节点上的监听器。同样的,一个ZooKeeper客户端在没有服务器连接的情况下能移除本地的监听器,通过设置本地的标记为true。下面是事件的详细列表监听器成功的被移除后触发:

  • 子节点移除事件:调用getChildren增加的监听器。

  • 数据移除事件:调用exists或getData增加的监听器。

ZooKeeper对监听器的保证

对于监听器,ZooKeeper有下列的保障:

  • 监听器和另外的事件,另外的监听器和异步的回复是有序的。ZooKeeper 客户端库确保每件事都有序分发。

  • 一个客户端看到这个节点的新的数据之前,会先看到他监听的节点的一个监听事件。

  • 从ZooKeeper 来的监听事件的顺序对应于ZooKeeper 服务看到的更新的顺序。

关于监听器要记住的事情
  • 监听器是一次触发的,如果你得到了一个监听事件并且想继续得到未来的事件通知,你必须设置一个另外的监听器。

  • 因为监听器是一次触发的,就会在得到事件和发送请求设置新的监听器之间有一个延迟,你不能看到ZooKeeper的节点上每次 改变。准备好处理在得到事件和设置监听器之间节点多次改变的情况(你或许不太关心,但至少要意识这会发生)。

  • 一个监听器对象或一个函数/上下文对,为一个事件只会被触发一次。比如说,如果相同的监听器在一次exists或getData调用中被注册到了相同的文件,并且文件被删除,对于该文件删除的通知,监听器对象只会被调用一次。

  • 当你从服务器断开连接,在恢复连接之前,你不会得到任何监听器。由于这个原因,会话事件会被发送给所有的未处理的监听器。使用会话事件进入一个安全模式:在断开期间,你不会收到事件,所以你的进程在这种模式下应该小心行事。

ZooKeeper使用ACLs控制访问

ZooKeeper使用ACLs来控制访问它的节点(ZooKeeper数据树上的数据节点)。ACL的实现和UNIX文件访问权限非常相似:它使用权限位来允许/拒绝对节点和位适用范围的各种操作。不像标准的UNIX权限,一个ZooKeeper节点没有限制在这三个标准的范围:user (文件拥有者)、group、world 。ZooKeeper没有节点拥有者的概念,取而代之的是,一个ACL指定ids和id相关的权限的集合。

还请注意一个ACL只适用于一个指定的节点,它也不适用于子节点。比如说,如果 /app节点只能被ip:172.16.16.1读取, /app/status是全部可读的,任何人都 可以读取/app/status。ACLs不是递归的。

ZooKeeper支持可插拔式的认证方案。Ids指定使用这个形式scheme:id,scheme是id对应的授权方案,比如说,ip:172.16.16.1是一个主机地址为172.16.16.1的id。

当一个客户端连接ZooKeeper并进行认证,ZooKeeper把符合这个客户端的所有ids联系起来。当客户端尝试存取一个节点的时候,这些ids用来检查一个节点的ACLs。ACLs由成对(scheme:expression, perms)的组成。expression的格式指定了权限,比如说,(ip:19.22.0.0/16, READ)给所有的以19.22开头的IP地址的客户端读的权限。

ACL权限:

Zookeeper支持如下权限: * CREATE:可以创建一个子节点 * READ:可以从一个节点读取数据并展示子节点 * WRITE:可以设置一个节点的数据 * DELETE:可以删除一个子节点 * ADMIN:可以设置权限

CREATEDELETE权限从WRITE权限中脱离是为了更细粒度的权限控制。CREATEDELETE的场景如下:

希望A能够设置节点的数据,但不能够CREATEDELETE该节点子节点。

CREATE没有DELETE:客户端通过在父目录创建节点来生成请求(这个客户端就是处理请求的客户端)。希望所有的客户端都能添加,只有request processor(创建父节点的客户端)能够删除(类似于文件系统的追加APPEND操作)。

ADMIN权限是因为zk没有文件所有者的概念。在某种意义上,ADMIN权限制定某个实体(entity,个体)为拥有者。zk不支持LOOKUP权限(目录的查询权限位支持LOOKUP操作,即使没有列出目录的权限)。所有的用户隐含都有LOOKUP的权限,这允许用户获取节点状态,仅仅只有这些(有一个问题是,在不存在的节点上执行zoo_exists(),这种操作不允许)。

内置ACL方案

zk有下面的内置方案: * world,有唯一的id,“anyone”,表示任何用户(客户端) * auth,不适用任何id,表示任何授权的用户 * digest,ca,感觉这块的官方文档说的不清楚。 * ip,使用客户端ip作为id,acl的表达式形式是addr/bits,addr的最高有效位bits个和客户端ip的最高有效位bits个匹配。 * x509,使用客户端X500 Principal作为ACL ID身份。 ACL表达式是客户端的确切X500主体名称。 使用安全端口时,客户端将自动进行身份验证,并设置其x509方案的身份验证信息。

可扩充的认证

zk使用不同的认证方案,运行在不同的环境中。因此他又一个完全可扩充的认证框架。即使内部的认证方案,也使用了可扩充的认证框架。

要理解认证框架如何工作,首先必须理解两个主要的认证操作。框架首先认证客户端。当客户端连接到服务器时就开始认证,包括客户端发送的信息和从客户端收集的信息,并且关联到连接。框架执行的第二个操作是找到ACL中和客户端对应的一个条目。ACL条目是<idspec, permissions>的形式。idspec可能是简单的字符串,直接可以和连接里面的认证信息对比,也可能是一个表达式,和连接里的信息进行计算。认证插件的实现负责匹配。 下面是认证插件必须实现的接口:

Code block
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判断哪个id适用于该scheme。 当客户端发送与连接关联的认证信息时,handleAuthentication被调用。客户端指定和认证信息相应的模式。zookeeper把信息传给认证插件,认证插件的getScheme匹配scheme。实现handleAuthentication的方法通常在判断信息错误后返回一个error,或者在确认连接后使用cnxn.getAuthInfo().add(new Id(getScheme(), data))

认证插件在设置和ACL中都有涉及。当对某个节点设置ACL时,zookeeper服务器会传那个条目的id给isValid(String id)方法。插件需要判断id的连接来源。例如,ip:172.16.0.0/16是有效id,ip:host.com是无效id。如果新的ACL包括一个"auth"条目,就用isAuthenticated判断该scheme的认证信息是否关联了连接,是否可以被添加到ACL中。一些scheme不会被包含到auth中。例如,如果auth已经指定,客户端的ip地址就不作为id添加到ACL中。 在检查ACL时zookeeper有一个matches(String id, String aclExpr)方法。ACL的条目需要和认证信息相匹配。为了找到和客户端对应的条目,zookeeper服务器寻找每个条目的scheme,如果对某个scheme有那个客户端的认证信息,matches(String id, String aclExpr)会被调用并传入两个值,分别是事先由handleAuthentication 加入连接信息中认证信息的id,和设置到ACL条目id的aclExpr。认证插件用自己的逻辑匹配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,只有一个会被使用。同样,所有服务器都必须统一插件定义,否则客户端用插件提供的认证schemes连接服务器时会出错。

一致性保证

ZooKeeper是一个高性能,可扩展的服务。读和写操作都非常快速。之所以如此,全因为zookeeper有数据一致性的保证:

  • 顺序一致性 客户端的更新会按照它们发送的次序排序。

  • 原子性 更新的失败或成功,都不会出现半个结果。

  • 单独系统镜像 不管客户端连哪个服务器,它看来都是同一个。

  • 可靠性 一旦更新生效,它就会一直保存到下一次客户端更新。这就有两个推论:

    1. 如果客户端得到成功的返回值,说明更新生效了。在一些错误情况下(连接错误,超时等)客户端不会知道更新是否生效。虽然我们使失败的几率最小化,但是也只能保证成功的返回值情况。(这就叫Paxos算法的单调性条件)

    2. 客户端能看到的更新,即使是渡请求或成功更新,在服务器失败时也不会回滚。

  • 时效性 客户端看到的系统状态在某个时间范围内是最新的(几十秒内),任何系统更改都会在该时间范围内被客户端发现。否则客户端会检测到断开服务。

用这些一致性保证可以在客户端中构造出更高级的程序如 leader election, barriers, queues, read/write revocable locks(无须在zookeeper中附加任何东西)。更多信息Recipes and Solutions

注意

zookeeper不存在的一致性保证: 多客户端同一时刻看到的内容相同 zookeeper不可能保证两台客户端在同一时间看到的内容总是一样,由于网络延迟等原因。假设这样一个场景,A和B是两个客户端,A设置节点/a下的 值从0变为1,然后让B读/a,B可能读到旧的数据0。如果想让A和B读到同样的内容,B必须在读之前调用zookeeper接口中的sync()方法。

绑定

ZooKeeper客户端库以两种方式提供:Java和C。下面几节描述这两种绑定。

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线程中进行。所有异步方法的回应,以及观察事件则在事件线程中处理。对于这个设计,有一些事情需要注意:

  • 所有同步调用和观察回调将按次序进行,一次一个。调用者可以进行任何想要的处理,但是在此期间不会处理其他回调。

  • 回调不会阻塞IO线程或者同步调用的处理。

  • 同步调用可能不会以正确的次序返回。比如说,假设客户端进行下述处理:提交一个watch设置为ture的、对节点/a的异步读取,然后在读取操作的完成回调中执行一个对/a的同步读取。(可能是不好的实现,但是是合法的,这只是一个简单的例子)

如果在异步读取和同步读取之间,对/a进行了修改,则客户端库将在同步读取返回之前接收到一个事件,表明/a已经被修改。但是因为完成回调阻塞了事件队列,同步读取将在观察事件被处理之前返回/a的新值。

最后,关于关闭的规则很直接:一旦被关闭或者接收到致命事件(SESSION_EXPIRED和AUTH_FAILED),ZooKeeper对象就变成无效的了。关闭后,两个线程被关闭,后续对zookeeper句柄的任何访问都将导致不确定的行为,应该避免。

C 绑定

C绑定有单线程和多线程库。多线程库易于使用,跟Java API非常相似。多线程库将创建用于处理连接维持和回调的IO线程与事件分发线程。通过暴露在多线程库中使用的事件循环,单线程库允许在事件驱动应用中使用ZooKeeper。

有两个共享库:zookeeper_st和zookeeper_mt。前者提供了异步API和回调,可集成到应用程序的事件循环中。这个库存在的目的仅仅是为了支持没有pthread可用,或者pthread不稳定的平台(如FreeBSD 4.x)。在其他场合,应用开发者应该链接zookeeper_mt,它同时支持同步和异步API。

安装

如果从Apache代码仓库检出的代码创建客户端库,执行下面的步骤。如果从apache下载的工程源代码包开始创建,则跳到步骤3。

  1. 在ZooKeeper顶级目录(…​/trunk)执行ant compile_jute。这将在../trunk/src/c目录中创建"generated"目录。

  2. 修改当前目录为../trunk/src/c,执行autoreconf -if,以启动autoconf、automake和libtool。请确认安装了2.59或者更高版本的autoconf。跳到步骤4。

  3. 如果从工程源代码包开始创建,解压缩源代码包,cd到zookeeper-x.x.x/src/c目录。

  4. 执行./configure <your-options>以生成makefile。对于这一步,configure工具支持下述有用的选项:

    • --enable-debug 启用优化和调试信息。(默认是禁用的)

    • --without-syncapi 禁止同步API支持,不创建zookeeper_mt库。(默认是启用的)

    • --disable-static 不创建静态库。(默认是启用的)

    • --disable-shared 不创建共享库。(默认是启用的)

      注意

      关于执行configure的一般信息,请看INSTALL文件。

  5. 执行make或者make install,创建并且安装库。

  6. 要生成ZooKeeper API的doxygen文档,可执行doxygen-doc。所有文档将放置到docs子目录中。默认情况下,这个命令只生成HTML。关于其他文档格式的信息,请执行./congiure --help。

绑定您自己的C客户端

在应用程序中使用ZooKeeper API时,应该记住:

  1. 包含ZooKeeper头文件:#include <zookeeper/zookeeper.h>

  2. 如果创建多线程客户端,请使用-DTHREADED编译器标志,以启用库的多线程版本,并且链接到zookeeper_mt库。如果创建单线程客户端,不要使用-DTHREADED,并且链接到zookeeper_st库。

常见问题和故障

下面是一些常见的陷阱:

  1. 如果你使用watch,你必须监控好已经连接的watch事件。当ZooKeeper客户端断开和服务器的连接,直到重新连接上这段时间你都收不到任何通知。如果你正在监视znode是否存在,那么你在断开连接期间收不到它创建和销毁的通知。

  2. 你必须测试ZooKeeper故障的情况。在大多数服务器都可用的情况下,ZooKeeper是可以维持工作的。关键问题是你的客户端程序是否能 察觉到。在实际情况下,客户端与ZooKeeper的连接有可能中断(多数时候是因为Zookeeper故障或网络中断)。Zookeeper的客户端库 关注于如何让你重新连接并且知道发生了什么。但是同时你也必须确保能够恢复你的状态和发送失败的请求。努力在测试库里测出这些问题,而不是在产品里——用 几台服务器组成的zookeeper集群测试这个问题,尝试让它们重启。

  3. 客户端维护的服务器列表必须和现有的服务器列表一致。如果客户端的列表是现有服务器列表的子集,还可以在非最佳状态工作,但是如果客户端列表里的服务器不在现有集群里你就悲剧了。

  4. 注意存放事务日志的位置。性能评测最重要的部分就是日志,ZooKeeper会在回复响应之前先把日志同步到磁盘上。为了达到最佳性能,首选专用 的磁盘来存日志。把日志放在繁忙的磁盘上会降低效率。如果你只有一个磁盘,就把记录文件放在NFS上然后增加SnapshotCount。这样虽然无法完 全解决问题,但能缓解一些。

  5. 正确地设置你java的堆空间大小。这是避免频繁交换的有效措施。无用的访问磁盘会让你的效率大打折扣。记住,在ZooKeeper中,一切都是有序的,如果一个服务器访问了磁盘,所有的服务器都会同步这个操作。