ZooKeeper程序员指南

本文档是针对希望创建利用ZooKeeper协调服务的分布式应用程序的开发人员的指南。它包含概念和实践信息。

认真阅读本文档有助于你开发高可用,质量稳定的软件服务。

数据模型

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

  • null字符串(u0000)不能是一个路径名称。
  • 下列字符不能被使用,因为不能很好的被展示:u0001 – u001F 和 u007F – u009F。
  • 下列字符是不允许的:ud800 – uF8FF, uFFF0 – uFFFF。
  • “.”字符可以作为另一个名字被使用,但是“.”和“..”不能单独使用来表示一个节点路径,因为ZooKeeper不使用相对路径,下列是无效的:”/a/b/./c”或者 “/a/b/../c”。
  • “zookeeper”标记被保留。

znode 节点

ZooKeeper树中的每个节点都称为znode。Znodes维护一个统计信息结构,其中包括用于数据更改和acl更改的版本号。统计信息结构还带有时间戳。版本号和时间戳允许ZooKeeper验证缓存并协调更新。znode的数据每次更改时,版本号都会增加。例如,每当客户端检索数据时,它也会接收数据的版本。并且,当客户端执行更新或删除时,它必须提供其更改的znode数据的版本。如果它提供的版本与数据的实际版本不匹配,则更新将失败。(可以忽略此行为)

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

Watches 监听

客户端可以在znodes上设置监听器,znode的改变触发这个监听器然后清空这个监听器。当一个监听器被触发,ZooKeeper发送给客户端一个通知。

Data Access 数据访问

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

ZooKeeper没有被设计成一个一般的数据库或一个大型对象存储。它管理协调数据,数据可以是配置、状态信息、集合点等的形式。各种各样的数据有一个共同的属性就是他们都很小:以千字节为标准。

ZooKeeper客户端和服务器有一个健康检查来确保znodes的数据少于1M,但是数据平均应该更小。操作较大的数据将导致一些操作花费更多的时间,并且会影响一些操作的延迟,因为在网络和存储媒介中移动更多的数据将需要额外的时间。如果需要存储大数据,通常的处理是把数据存储在一个大容量存储系统中,并把存储位置的指针存储到ZooKeeper上。

Ephemeral Nodes 临时节点

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

Sequence Nodes -- Unique Naming 顺序节点,唯一名称

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

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

Container Nodes 容器节点 v3.6新增

ZooKeeper 有容器节点的概念
容器znode是特殊用途的znode,可用于诸如领导者,锁等。
当删除容器的最后一个子容器时,该容器将成为服务器将来某个时候要删除的候选对象。
给定此属性,您应该准备在容器znodes内创建子级时获取KeeperException.NoNodeException。
在容器znode内创建子znode时,请始终检查KeeperException.NoNodeException并在发生时重新创建容器znode

TTL Nodes 过期时间节点

创建PERSISTENT或PERSISTENT_SEQUENTIAL znode时,可以选择为znode设置以毫秒为单位的TTL。 如果znode在TTL内未修改且没有子节点,它将成为服务器将来某个时候要删除的候选者。

注意:TTL节点必须通过“系统”属性启用,因为默认情况下它们是禁用的。 有关详细信息,请参见《管理员指南》。 如果尝试创建没有设置适当的System属性的TTL节点,则服务器将抛出KeeperException.UnimplementedException。

Time in 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 Structure

ZooKeeper中每个znode的Stat结构由以下字段组成:

  • czxid 创建此znode的zxid
  • mzxid 最后一次更改此znode的zxid
  • pzxid 最后一次更改此节点子节点的zxid
  • ctime 从创建纪元开始到创建此节点的毫秒数
  • mtime 从创建纪元开始到最后一次更改节点的毫秒数
  • version 数据节点变更的次数
  • cversion 子节点的变更次数
  • aversion 节点ACL配置的变更次数
  • ephemeralOwner 此节点owner的会话id,只对临时节点有效 如果它不是临时节点,那么它将为零。
  • dataLength 此znode的数据字段的长度
  • numChildren 此znode的子级数

会话 session

客户端连接服务端后变成CONNECTING状态,断开的时候移至CLOSED状态

客户端指定连接字符串(例如,“ 127.0.0.1:4545”或“ 127.0.0.1:3000,127.0.0.1” :3001,127.0.0.1:3002“)。如果此连接失败,或者客户端由于某种原因与服务器断开连接,则客户端将自动尝试列表中的下一个服务器,直到(重新)建立连接为止。

ZooKeeper watch

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是一个高性能,可扩展的服务。读和写操作都非常快速。之所以如此,全因为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()方法。

在Java开发中,设置监听事件,并不会给你带来最新的值,需要再去查一次,这是为了保证数据最新的一致性

构建:ZooKeeper操作指南

具体查看项目Demo案例

Java绑定

陷阱:常见问题和故障排除

所以现在您知道了ZooKeeper。快速,简单,您的应用程序可以运行,但是请稍候...出了点问题。以下是ZooKeeper用户的一些陷阱:

  1. 如果您使用watch,则必须寻找已连接的watch事件。当ZooKeeper客户端与服务器断开连接时,除非重新连接,否则您不会收到更改通知。如果您正在监视znode的存在,那么当断开连接时创建并删除znode时,您将错过该事件。
  2. 您必须测试ZooKeeper服务器故障。只要大多数服务器处于活动状态,ZooKeeper服务都可以经受住故障。要问的问题是:您的应用程序可以处理吗?在现实世界中,客户端与ZooKeeper的连接可能会断开。(ZooKeeper服务器故障和网络分区是造成连接丢失的常见原因。)ZooKeeper客户端库负责恢复连接并让您知道发生了什么事情,但是必须确保恢复状态和所有失败的未完成请求。在测试实验室而不是生产环境中查找是否正确无误-使用由多个服务器组成的ZooKeeper服务进行测试,然后使它们重新启动。
  3. 客户端使用的ZooKeeper服务器列表必须与每个ZooKeeper服务器具有的ZooKeeper服务器列表匹配。如果客户端列表是ZooKeeper服务器的真实列表的子集,则事情可以进行,尽管不是最佳的,但是如果客户端列出的ZooKeeper服务器不在ZooKeeper集群中,则事情就没有效果。
  4. 请注意将事务日志放在何处。ZooKeeper最关键的性能部分是事务日志。ZooKeeper必须先将事务同步到媒体,然后才能返回响应。专用的事务日志设备是保持良好性能的关键。将日志放在繁忙的设备上会对性能产生不利影响。如果只有一个存储设备,则将跟踪文件放在NFS上并增加snapshotCount;它不能消除问题,但可以缓解问题。
  5. 正确设置Java最大堆大小。避免交换非常重要不必要地进入磁盘几乎肯定会降低您的性能。请记住,在ZooKeeper中,所有内容都是有序的,因此,如果一个请求命中磁盘,则所有其他排队的请求都命中磁盘。为避免交换,请尝试将堆大小设置为拥有的物理内存量,减去操作系统和缓存所需的数量。确定配置最佳堆大小的最佳方法是运行负载测试。如果由于某些原因您不能这样做,请保守估计,并选择一个远低于会导致机器交换的限制的数字。例如,在4G机器上,3G堆是一个保守的估计。
 Protobuf Java 使用案例 入门
ZooKeeper入门指南(初次体验) 
上一篇:Protobuf Java 使用案例 入门
下一篇:ZooKeeper入门指南(初次体验)
评论

如果我的文章对你有帮助,或许可以打赏一下呀!

支付宝
微信
QQ