zk官方文档部分译文

refer:官方文档

zk数据模型

zk的命名空间中的每个节点既能关联数据也能有子节点.节点的路径总是被表示为斜杠分割的绝对路径, 并且不允许使用相对路径. 路径的以及节点的命名遵守以下规则:

  • 不能包含空字符(\u0000).
  • 不能包含无法显示字符:\u0001 - \u0019 and \u007F - \u009F.
  • 不包含\ud800 -uF8FFF, \uFFF0 - uFFFF范围的字符.
  • “.”可以作为命名空间的组成, 但是不能单独使用(zk没有相对路径).
  • “zookeeper”被认为是保留字.

ZNodes

zk中的每个节点即是znode. znode维护着包含版本号, 权限控制列表, 时间戳等的状态结构. 版本号与时间戳让zk保证了数据的更新. 每一次znode的数据有变化, 版本号会增加, 每当客户端接收到变更数据时也会接收到对应的版本号. 当客户端执行更新或者删除操作时也必须提供对应的znode的版本号, 如果版本号不匹配当前的数据, 变更将会失败.

zk不适合作为存储系统而是用于管理协调数据. 存储的数据可以是配置信息, 状态等, 它们的共性是数据量较小(KB级别). 存储体积较大的数据会由于replica导致存储以及网络的开销造成延迟.

znode是程序员需要关注的主要对象. znode有以下几个指的被关注的特性:

  • Watches: clients能够在znode上设置watches. znode发生变更时能够触发并清除watch. 当watch被触发时, zk向client发送一个通知.
  • Data Access: 存储在znode中的数据能够自动地被读写. 读能够获取znode中的所有数据, 对应地, 写也能替换znode中的所有数据. 每个znode有一个Access Control List (ACL)来做权限控制.
  • Ephemeral Nodes: zk允许创建临时节点. 这种znode的生命周期与创建它的session连接相同, session结束znode即被删除.临时节点因为这种特性不允许拥有子节点.
  • Sequence Nodes: 创建znode时, zk可以在路径的末尾添加唯一自增序号, 这种znode可以被用来实现分布式锁或者分布式队列.

Time in ZooKeeper

  • Zxid: 每次zk状态的变更会接收到唯一且自增的zxid(ZooKeeper Transaction Id)形式的时间戳. 这代表着zk的所有变更的时序.
  • Version numbers: znode的每次变更会导致该znode的version number加1. version代表znode的变更次数, cversion代表该znode的子节点的变更次数, aversion代表该znode的ACL的变更次数.
  • Ticks: 当使用zk集群时, zk-server之间使用ticks来定义事件的事件状态. 例如状态上传, session超时, 链接超时等等. tick time间接地暴露了session的最小超时时间(往返tick时间).
  • Real time: zk不使用real time或者clock time, 而是将时间戳保存在znode的状态结构中, 包括生成时间, 修改时间.

ZooKeeper Stat Structure

zk中的znode的状态结构由以下构成:

  • czxid: 该znode创建时的zxid.
  • mzxid: 该znode最近修改时的zxid.
  • pzxid: 该znode最近修改子节点时的zxid.
  • ctime: 该znode创建时的毫秒时间戳.
  • mtime: 该znode最近一次修改时的毫秒时间戳.
  • version: 该znode的data修改次数.
  • cversion: 该znode的子节点的修改次数.
  • aversion: 该znode的ACL的修改次数.
  • ephemeralOwner: 倘若该znode是临时节点, 代表创建者的sessionId. 若不是临时节点, 这个值为0.
  • dataLength: 该znode的data的数据大小.
  • numChildren: 该znode的子节点数量.

ZooKeeper Sessions

zk-client通过其语言绑定的实现连接到zk-service便创建一个session. session的状态图如下:

为了创建一个client-session, 应用需提供逗号分隔的host:port pairs. zk客户端会随机选中某个server地址并试图连接上去.如果这个连接失败了或者由于其他原因断开了, 客户端会自动连接下一个server直到连接被建立.

Added in 3.2.0: 在连接的字符串后面可以添加chroot后缀, client的所有命令将会变为相对于该root的操作. 如:”127.0.0.1:3000,127.0.0.1:3001,127.0.0.1:3002/app/a”. 这样做的好处是便于了隔离.

当client连接到zk-service后, zk创建一个64-bit的number代表zk的session并分配给client.如果client连接到另一个zk-server, 它将会把sessionId作为连接握手请求的一部分. 为了安全起见, server会给sessionId生成密码并在client创建session时返回给client. client在重连session到一个新的server时会发送密码与sessionId.

client创建zk-session时有一个超时时间的参数. 目前的实现要求超时时间至少是tickTime的两倍, 最大是tickTime的20倍.

当一个client(session)成为zk-cluster的一部分时, 它将在创建session时指定的server列表中搜索, 最终client与其中一个server的连接被建立后, session将会进入”connected”状态或者”expired”状态(取决于是否超时). 连接断开后我们不推荐你新建一个session对象, zk-client会为你处理重连. 只有接收到session过期事件后才会强制创建一个新的session.

session的过期是由zk-cluster自己管理的, 不是客户端. 当client建立与zk-cluster的session时提供了”timeout”参数. 这个值被zk-cluster用来决定何时client的session会过期. 当zk-cluster在timeout时间内接收不到消息时, 过期事件会发生. 当session过期后, zk-cluster会立即删除掉该session拥有的所有临时节点, 并且触发这些节点的watches. 在这期间, 这个client依然与zk-cluster是失联状态, 它只有重连到集群上时才知道自己的session过期了.

过期的session的watcher观察到的state变化:

  1. connected: session建立了, client与zk-cluster通信中.
  2. … client与zk-cluster发生了网络分区.
  3. disconnected: client与zk-cluster断开连接了.
  4. … 过了一段时间, 超时后, zk-cluster将该会话过期了, client还没意识到什么事情.
  5. … 过了一段时间, client与zk-cluster网络恢复了.
  6. expired: 最后client重连了zk-cluster, 它被通知超时了.

session创建调用的另一个参数是默认的watcher. 当client的state发生任何变更时, watchers会被通知. 例如, 如果client与server失去了连接或者session过期了等, client会被通知. 该watcher应该考虑到断开连接的初始状态(如在client发送state变更事件给watcher之前). 比如新建连接时, 第一个发送给watcher的事件应该是session connection事件.

session通过client发送的心跳来保持存活状态. 心跳请求不仅让zk-server知道client是否活跃, 也让client知道server是否还正常. 心跳请求的间隔应该既能保证正常的使用, 也能及时检测到连接的断开从而重连到一个新的server.

一旦与server的连接被建立好, 基本上有两种情况会导致client发生connectionloss.

  1. 该session已经挂掉了或者有问题.
  2. client在向server发送请求期间connection断开了.

Added in 3.2.0 – SessionMovedException: 有一个clients通常看不见的exception叫做SessionMovedException. 当一个session重连到另一个server上后, 旧session继续发送请求时发生这个异常.这个异常发生的原因是客户端发送给server的请求由于网络延迟原因delay了, client重连到新的server上后这个包才到旧的server上. 旧server收到这个包后会主动关闭这些connection. clients通常看不到这些error是因为它们的旧connection已经关闭了. 一种情况下, clients能够看到这个错误是在两个client使用了相同的sessionId与密码去重连session并建立了相同的连接. 其中一个client会建立成功, 另一个会断开.

ZooKeeper Watches

zk中所有的读操作: getData(), getChildren(), exists() 有设置watch的选项. 这里是zk对于watch的定义: 一个watch event是一个一次性触发器, 当watch设置到的data发生变更时会被触发. 有三点需要注意的地方:

  • One-time trigger: 当data变更时, 一个watch event会被发送给client. 例如如果一个客户端操作getData(“/znode1”, true) 过了一会儿 /znode1 的data被改变或者删除了, 客户端会收到/znode1的watch event. 如果/znode1再一次改变了, 除非client再一次在读操作中设置了watch否则不会给client发送watch event.
  • Sent to the client: 这个强调了一个event是单向发送给client的, 但是有可能在变更操作返回succ之后到达client. watches被异步地发送给watchers. zk提供了以下时序的保证: 一个client永远不会在watch event到达之前看到数据的变更. 网络延迟或者其他因素可能导致不同的client接收watch event与update succ的时间不同. 但是关键之处在于不同的client看到的everything的顺序是不变的.
  • The data for which the watch was set: 这个是指一个znode的变化会有不同的方式. 你可以认为zk维护着两组watches的list: data watches与child watches. getData()与exists()方法可以设置data watches, getChildren()方法可以设置child watches. 考虑到以上方法的返回数据, setData()将会触发data watches, create()将会触发生成的znode的data watch与其父节点的child watch, delete()将同样触发data watch与child watch.

watches被client连接的zk-server本地维护. 这样设计使watches的维护,分发与触发变得轻量级. 当一个client连接到一个新的server上时, watch的session event将被触发. 当与server断开连接后, watches不会被接收到, 当client重新连接后, 如果需要, 之前注册的watches都将被重新注册并且触发. 有一种情况可能会导致watch丢失: 在watch的节点还未生成的情况下, 如果在断开连接期间,该节点被创建又被删除,那么watch事件会丢失.

Semantics of Watches

我们能够通过获取zk状态的三个读方法设置watches: exists(), getData(), getChildren(). 下方详细地列出了watch能够触发的事件与启用他们的调用:

  • Created event: Enabled with a call to exists.
  • Deleted event: Enabled with a call to exists, getData, and getChildren.
  • Changed event: Enabled with a call to exists and getData.
  • Child event: Enabled with a call to getChildren.

What ZooKeeper Guarantees about Watches

  • watches相对于其他时间, 其他watches, 异步相应来说是有序的. zk-client保证了所有分发的顺序.
  • 对于一个znode, client收到的watch event总是在其看到新的数据之前.
  • 来自zk的watch event的顺序取决于zk-service接收到update的顺序.

Things to Remember about Watches

  • watches是一次性的触发器. 如果你得到了一个watch event并且还想获得未来的变更通知, 你必须再设置另一个watch.
  • 因为watches是一次性触发器, 并且在获得event与发送新请求去获得一个watch之间有延迟, 你不能可靠地看到发生在zk中的znode的每个变更. 你要准备好去处理获取event与重设watch之间znode可能的多次变更的情况(你可能不关心这种情况, 但是至少你要知道它可能会发生).
  • 一个watch对象, 或者方法, 仅会被触发一次. 例如, 同一个watch对象既通过exists()操作也通过getData()注册了, 然后节点被删除了, watch对象将被触发一次.
  • 当你与一个server断开连接时(例如server挂了), 你再重连之前不会收到任何watches. 因为这个原因, session events会被发送到所有的watch handlers. 使用session events来进入一个安全模式: 你可能在断开期间收不到任何events, 所以在这个模式下你应该谨慎地搞事情.

Java Binding

zk的java客户端由两个包组成: org.apache.zookeeper, org.apache.zookeeper.data, 剩下的包是zk内部使用的或者是zk-server的部分实现.

zk的java客户端使用的主要的类是ZooKeeper. 它有两个构造器, 区别仅仅是可选的sessionId与password. zk支持session恢复, 程序可以保存它的sessionId与password用来重启或恢复之前使用的session.

当一个zk对象被创建时, 同时会启动两个线程: 一个IO线程与一个event线程. 所有的IO由IO线程来处理(java-NIO). 所有的event回调由event线程来处理. session的重连或者心跳维护, 同步方法的相应也是在IO线程上. 异步方法的响应与watch event由event线程处理. 下面是这样来设计的特点:

  • 所有的异步调用与watcher回调会单线程按顺序执行. 调用者可以在回调用搞任何事情, 但是与此同时没有其他的回调会执行.
  • 回调不会阻塞IO线程, 也不会阻塞同步调用的处理.
  • 同步调用的返回可能是乱序的. 例如, 假定一个客户端进行以下处理: 在对节点/a异步读操作时设置了watch, 然后在读回调中对/a节点进行同步读. 在这种情况下, 如果在异步读与同步读中/a节点的数据发生了变更, 客户端将会在同步读相应前收到watch event, 因为异步回调是通过一个队列实现的.

最后, 关闭操作很简单: 一旦ZooKeeper对象被关闭了或者接收到了session过期或者auth_failed事件, 这个ZooKeeper对象就是非法的. 在关闭的时候, 两个线程也会被关闭, 这之后的任何操作都不行啦.