ZooKeeper程序员指南

前言

基于 3.6.2 版本 的 官方文档, 主要是翻译,可能会自己根据意思表达. 部分有删减

ZooKeeper 数据模型

ZooKeeper(以下称 ZK) 使用了层级的命名空间, 跟我们的文件系统非常类似. 唯一不同的是每个命名空间内的节点可以同时拥有数据 和 子节点. 有点像文件系统, 但是其中的文件既可以是普通文件也同时可以是目录. 节点的路径通常就是规范的, 斜杆分隔的绝对路径; 没有相对路径的形式. 任何unicode字符只要符合以下的形式就可以使用:

  • null 字符(\u0000) 不能用于路径名 (会导致C语言的绑定有问题)
  • 以下的字符不能使用因为它们不可见, 或者是会渲染异常: \u001-\u001F 和 \u007F
  • \ud800 - uF8FF, \uFFF0 - uFFFF, 之间的字符不允许
  • ‘.’ 可以被用来作为路径的一部分, 但是不能单独出现, 因为ZK并不使用相对路径, 比如 “/a/b/./c” 或者 “/a/b/…/c” 就是不合法的
  • “zookepper” 是保留单词的, 不能用

ZNodes

每个 ZK 树中的节点被称为 znode. znode 维护了一个统计结构(stat structure), 其中包含了数据变化的版本号, acl(Access Control List) 变化的信息. 统计结构 同时还拥有时间戳. 版本号时间戳 允许 ZK 验证缓存以及协调更新操作. 每次 znode 数据发生变化, 版本号 会增加, 时间戳 也会变化. 比如, 无论什么时候客服端拿到数据, 它也会同时接收到数据的版本. 当客户端发起更新或者删除的时候, 它必须提供它正在修改的znode的数据的版本号. 如果它提供的版本号和实际的数据版本号不一致, 更新会失败.(这个行为可以重写)

注意:
在分布式应用工程, 节点(node)可以用来指代通用的主机, 服务器或者一个整体的成员, 一个客户端进程等. 在 本文档, znodes 表示 数据节点. 服务器 表示 组成 ZK 服务的机器; quorum 节点表示组成整体的服务器; 客户端表示任何使用ZK服务器的进程或者主机

Znode 是程序员主要操作的对象, 它们拥有一些值得讨论的特性.

监听(Watch)

客户端可以监听znode. 变化会触发那个znode的监听器并且清除监听. 当一个监听器被触发, ZK 会给客户端发送一个消息. 更多信息可以查看 ZK监听器

数据访问

保存在每个命名空间中的每个znode中的数据都是自动读取和写入的. 读取操作会读取znode关联的数据的字节码 而 写操作会替换所有的数据. 每个节点都有一个 访问控制列表(ACL) ,限制了谁能干什么.

ZK 不是设计为通用数据库或者存储大对象的. 相反, 它负责协调数据. 这个数据可以是配置, 状态信息, 集合等. 无论是什么形式,它们的共同点是数据量相对小: 使用 KB 来衡量. ZK客户端和服务端都会进行检查以保证znode 的数据小于 1M , 但是数据应该平均远小于这个水平. 对相对大的数据的操作是缓慢的并会导致某些操作的延迟,因为 网络传输 和 储存更大的数据需要更多的时间. 如果有存储这种大的数据的需求, 建议使用块存储系统, 比如 NFS 或者 HDFS, 并把存储位置的指针放到 ZK 中.

临时节点(Ephemeral Nodes)

ZK 也有临时节点的概念. 这种 znode 只有创建它的 session 存活的时候, 它才存活.当 session 关闭的时候, 它就会被删除. 由此临时节点不允许拥有子节点. session 的临时节点 列表可以通过 getEphemerals() api 获取到.

getEphemerals()

获取 session 创建特定路径的临时节点列表. 如果路径为空, 则会列出session的全部临时节点. 用例 - 可以用临时节点来检查是否创建了重复节点, 这些节点的创建是序号化的所以不知道路径的情况.这种情况下, 这个api可以用来获取该session的node列表. 这可能是一种典型的服务发现的用法.

序列化节点 – 唯一名称

当创建节点的时候, 你可以同时要求ZK添加一个被监控的自增计数器到路径末尾. 计数器对于父znode是唯一的. 计数器的格式是 %010d – 一个长度为10的用0填充的数字(为了简化排序). 比如说 “0000000001”. 看 队列清单 有使用的例子

注意: 计数器使用有符号整数来保存下一个序列号, 由父节点来维护, 如果 超过 2147483647 会溢出, 下一个数变成 -2147483648

容器节点

3.6 版本新加的

ZK 有容器znode 的概念. 容器Znode 是特殊的节点用来实现 leader 和 锁 等等. 当 容器 的最后一个子节点被删除的时候, 该容器会在未来的某个时间点被服务器删除.

因为这个属性,当创建容器节点的子节点的时候, 代码里要捕获 KeeperException.NoNodeException 异常. 比如, 当创建子节点的时候,如果抛了KeeperException.NoNodeException , 就需要重新创建父节点然后再添加子节点.

TTL 节点

3.6 版本新加的
当创建 PERSISTENT 或者 PERSISTENT_SEQUENTIAL znode 的时候, 可以指定一个 TTL 的毫秒数. 如果 znode 在TTL时间内没有被修改而且没有子节点的话, 它就会成为候选移除的节点, 未来某个时间点会被ZK自动删除.

注意: TTL 必须通过系统属性开启, 默认是关闭的, 见 管理员指南. 如果尝试创建 TTL 节点但是没有正确的设置系统属性, 则会抛出 KeeperException.UnimplementedException 异常.

ZK 的时间

ZK有多种方式来跟踪时间:

  • Zxid :每次这ZK状态变化的时候都会收到一个戳(stamp), 用 zxid 来表示(ZooKeeper Transaction Id). 这样显示了ZK所有变化的顺序. 每个变化都会有一个唯一的 zxid 并且如果 zxid1 小于 zxid2 ,那么 zxid1 发生在 zxid2 之前.
  • 版本号: 每次一个节点发生变化,都会增加该节点其中一个版本号。三种版本号分别是,version (节点数据变化的数目), cversion(节点子节点变化的数目), 和 aversion (节点ACL变化的数目.
  • Ticks: 当使用多服务器 ZK 的时候, 服务器使用 ticks 来定义时间的时间, 比如说 状态上传, 会话超时, 节点间的连接超时等等. 时钟时间只会通过最小会话超时时间(2倍tick 时间).如果一个客户端请求的会话超时时间小于最小会话超时时间, 那么服务器会提示客户端实际的超时时间是最小会话超时时间.
  • 实际时间: ZK并不适用实际的时间, 或者时钟时间 除非是在创建znode或者修改的时候会添加时间戳.

ZK 统计结构 (Stat Structure)

统计结构由下面这些字段组成:

  • czxid: 导致了当前节点创建的 修改 的 zxid
  • mzxid: 最近修改了这个节点的 修改 的 zxid
  • pzxi: 最后修改了当前节点子节点的 修改 的 zxid
  • ctime: 创建了这个节点的起始时间到现在的毫秒数
  • mtime: 上一次修改到现在的毫秒数
  • version: znode 数据被修改的次数
  • cversion: znode 子节点被修改的次数
  • aversion: znode ACL 被修改的次数
  • ephemeralOwner: 当前临时节点的拥有者的 session 的 id. 如果当前节点不是临时节点, 则为0
  • dataLength: data字段的数据长度
  • numChildren: 子节点的数目

ZK 会话

ZK 客户端通过 语言绑定(language banding) 创建的句柄来和 ZK 服务建立会话关系. 一旦创建, 句柄会开始于 CONNECTING 状态并且客户端库会开始尝试连接到其中一个ZK服务然后转换成 CONNECTED 状态. 正常情况下, 客户端句柄会处于这两种状态之一. 如果一个无法恢复的错误出现, 比如说 会话过期 或者 认证时标, 或者应用显示地关闭了句柄, 该句柄会转换成 CLOSED 状态.

调用close
认证失败
会话过期
已连接事件
连接断开事件
调用close
CONNECTING
CLOSED
CONNECTED

在这里插入图片描述
为了创建一个会话, 需要提供一个逗号分隔的ip:port 的连接字符串(比如: “127.0.0.1:4545” 或者"127.0.0.1:3000,127.0.0.1:3001,127.0.0.1:3002"), 每个客户端表示一个服务器. 客户端会随机使用其中一个尝试连接, 如果连不上或者连接被断开, 客户端就会尝试下一个 ip, 直到成功.

3.2.0版本 以后, 连接字符串后面可以加 chroot, 用来指定该用户的根目录, 比如: “127.0.0.1:4545/app/a”, 那它后面的一些命令都是相对于这个路径了.

当客户端获取到服务的句柄之后, ZK会创建一个 会话, 分配一个64位的数字给客户端.如果客户端连接到了一个不同的 ZK 节点, 那么它会把 session id 作为连接握手的一部分发送给服务端.从安全性来考量, 服务端会为该session id 创建密码,并且任何ZK服务节点都能验证. 当创建会话的时候, 服务端会把密码和session id 一并发送到客户端. 当客户端需要和其他服务器建立连接的时候, 就要将这两个参数发送过去.

客户端创建会话的参数之一就是会话超时(单位毫秒). 客户端发送一个被请求的超时时间, 服务端响应它能提供的超时时间. 目前的实现要求超时时间最小是 2倍 tick 时间(服务端配置的), 最大是20倍 tick 时间. ZK 客户端API允许协商超时时间.

当客户端(会话)和ZK服务集群分隔(断开连接)的时候, 它会开始查找创建会话时候指定的服务器列表.最终当 客户端 和 至少一个服务器连接上的时候, session要么转化成 "已连接"状态(如果在会话超时时间内重新连接上了) 要么转换成 “超时"状态(如果在会话超时时间过后重新连接). 不建议在断开连接之后创建一个新的session对象(新的 ZooKeeper.class 或者 C绑定中的ZooKeeper 句柄). ZK客户端库会自动处理重新连接. 特别是我们拥有启发式的内置方法来处理"羊群效应”, 等待. 只有你被通知会话过期的时候才应该创建一个新的session(强制要求).

会话超时是由集群自己管理的, 而不是客户端. 当ZK客户端创建一个会话的时候, 集群会给它一个超时时间. 这个用被集群用来判断客户端是否过期. 过期会发生在集群在某段时间内都没有接收到来自客户端的消息(比如,心跳包). 当会话过期的时候, 集群会删除该会话拥有的所有临时节点 并同时所有已连接的客户端该变化(任何监听这些节点的客户端).这个时候客户端的过期会话还是和集群断开的, 直到它能够重新和集群建立连接的时候, 它才会被通知会话过期. 客户端会一直处disconnected 状态直到和集群建立TCP连接, 那时过期会话的监听器会收到 “session expired” 的通知.

被监听的过期会话的转换例子:

  1. ‘connected’ : 建立会话,客户端与集群通信(客户端/服务器通信正常运行)
  2. … 客户端从集群中分区
  3. ‘disconnected’ : 客户端失去了与集群的连接
  4. … 时间流逝, 超时时间后, 集群将会话过期掉,但是客户端看不到任何东西,因为它已经从集群断开了连接
  5. … 时间流逝, 客户端和集群恢复了网络层面的连接.
  6. ‘expired’ : 最后客户端重新连接上了集群, 它会被通知其会话过期.

创建会话的另一个参数是默认监听器. 客户端发生变化的时候, 监听器会被通知. 比如客户端连接丢失,或者会话过期等. 该监听器的初始状态是 disconnected(在触发任何事件之前). 当创建连接的时候, 第一个事件通常是 会话建立 事件 (session connection event).

session通过客户端的请求来维持不过期. 如果会话空闲超过一定时间就会超时. 客户端会发送PING 请求来保持会话不过期. PING 请求 既可以让服务端知道客户端还活着, 也可以让客户端知道服务端活着. PING 的时间必须足够保守来保证能检测到死连接并重新连接到新的服务器.

只要建立了连接, 只有两种情况会导致连接丢失:

  1. 应用在一个失效的 session 上调用操作
  2. ZK 客户端在已有正在处理中的请求的时候断开, 比如有一个异步的调用.

Added in 3.2.0 – SessionMovedException 服务器内部的异常, 对客户端是不可见的. 抛出的时候表示客户端已经在别的服务器重新建了连接了. 通常导致这个异常的原因是, 客户端向A服务器发送了请求,但是请求包有网络延迟, 所以客户端超时了并重新连接到了一个新的服务器. 当延迟的网络包到达服务器A之后,它发现 会话已经被移走了,然后就移除这个连接. 客户端一般不会看到这个错误应该它不会再从旧的连接读取数据(旧连接通常被关闭了). 一种能看到这个条件的情况是 当两个客户端使用已保存的 session id 和 密码来重新建立相同的连接. 其中一个客户端会成功, 另一个会失败(然后导致两个客户端不断的重连并断开)

更新服务器列表 允许客户端提供新的 连接字符串 . 这个功能会触发一个 随机负载均衡算法 并可能会导致客户端断开原来的连接并连接新服务器 来均衡新服务器连接的数量. 如果新的 连接字符串 中不包含当前连接的服务器, 这个调用就会导致客户端断开当前的连接; 否则, 客户端就会依据服务器增加或者减少的数量来做决定

比如, 如果之前配了 3个服务器, 现在新配的 在之前3个的基础上, 增加了2个, 那么 40% 的服务器已连接到原来3个服务器的客户端 会断开原来的连接并连接新的服务器 来实现负载均衡. 连接新服务器的选择是随机的.

另一个例子, 假设旧的服务器配了5个, 现在移除了2个. 已经连接到剩下的3个服务器的客户端就保持不变, 而连接到那2个被移除服务器的客户端就会随机切换到剩下的那3个服务器上. 如果连接删除了, 客户端会转换成特殊状态, 在选择新服务器的时候会使用随机算法而不是 轮转算法.

在第一个例子中, 每个客户端有0.4的概率会选择去连接新的服务器, 但是只要做了决定之后就会随机选择新的服务器连接, 除非是连接失败, 客户端才会去连接旧的服务器. 当找到一个服务器之后, 或者尝试了所有新列表中的服务器而且都失败之后, 客户端会返回正常模式, 即随机选择连接随机字符串中的服务器去连接. 如果失败了, 它就会继续轮流尝试使用不同的随机服务器.

本地Session: 3.5.0 版本加入的特性

  • 背景: 创建和删除会话在ZK中开销很大因为它们需要成员(quorum)的确认,当需要处理成千上万的连接的时候, 这会称为整体的瓶颈.所以 3.5.0 版本之后, 引入了新类型的 会话: 本地会话 , 正常(全局)会话的阉割版本. 可以通过开启 localSessionEnabled 使用这个特性.

localSessionsUpgradingEnabled 关闭了之后:

  • 本地session不能创建临时节点
  • 本地会话丢失之后, 用户无法使用 session id 和 密码重新建立, 该会话和它的监控器都会被删除. 注意: 丢失tcp连接不代表该会话已经丢失了.如果客户端可以在会话超时之前在相同的服务器上重新建立连接, 那么会话可以继续. (这种会话无法迁移到别的服务器)
  • 当本地会话连接的时候, 会话的信息仅保持在连接上的服务器上.leader 不会感知到这种session的创建, 也不会向磁盘写入状态
  • ping, 过期 和 其他 会话状态 的维护 由 客户端连接上的 服务器处理.

localSessionsUpgradingEnabled 开启了之后:

  • 本地会话可以被升级为正常(全局)会话

  • 当一个新会话创建的时候它会被保存在在一个 封装的 LocalSessionTracker 中. 它随后能够根据请求升级成为一个全局会话.(比如创建临时节点). 升级之后, 该session id不变但是会从 local 集合 中删除.

  • 目前, 只有创建临时节点时候才需要升级会话. 这是因为临时节点对全局会话的依赖很强。如果不升级的话,会导致不同节点的数据不一致。Leader节点也需要知道会话的生命周期以便以在其关闭或者过期的时候清理临时节点. 这就需要一个全局节点, 因为临时节点只会绑定到特定的服务器。

  • 一个会话在升级的过程中可以同时是本地和全局节点,但是升级的操作没有办法被两个线程同步地调用。

  • ZooKeeperServer(独立) 使用 SessionTrackerImpl; LeaderZooKeeper使用 LeaderSessionTracker. LeaderSessionTracker持有了 SessionTrackerImpl(全局) and LocalSessionTracker(如果开启的话). FollowerZooKeeperServerObserverZooKeeperServer 使用 LearnerSessionTracker,LearnerSessionTracker 持有 LocalSessionTracker

  • Q&A

  • 为什么需要单独的配置来关闭本地会话升级?

  • 在一个需要处理大量客户机的大型部署中,我们知道客户机通过观察者进行连接,观察者应该是本地会话。因此,这更像是一种防范意外创建大量临时节点和全局会话的防护措施。

  • 本地会话会在什么时候创建?

  • 当前的实现中会在处理 ConnectRequest 并且当 *createSession * 请求达到 FinalRequestProcessor 的时候创建

ZooKeeper 监听器

读操作 getData(), getChildren(), 和 exists() 都可以设置监听器, 定义如下: 一个监听器事件是一个 一次性触发 , 发送到客户端的 , 在其监听的数据发生变化的时候 产生的事件:

  • 一次性触发: 设置后只触发一次. 监听一次触发一次, 要重新监听才有下一次
  • 发送到客户端: 事件是异步发送的, 可能在收到返回码之前都不会收到事件.ZK只能保证 : 收到变化事件之前都不会看到数据的变化 这个顺序
  • 所监听的数据发生变化: 数据监听器 和 子节点监听器是不同的事件. getData( ) 和 exists() 触发的是数据监听器, 而 getChild() 触发的是 子节点监听器. 删除的节点的时候会同时触发

监听器主要是由其所连接的服务器维护. 这样比较轻量级允许监听器的设置, 维护和分发. 当 客户端连接到一个新的服务器的时候, 会触发监听 session 的监听器. 监听器在连接断开的时候不会触发, 但是当客户端重新连接的时候, 监听器会重新注册并在必要的时候触发. 通常这些操作都是透明的. 唯一会丢失监听器的情况是, 一个监听器监听了一个未创建节点的存在, 并在连接断开的时候, 那个节点被创建并且删除, 这个时候这个监听器就会丢失.

3.6.0 版本之后, 客户端可以创建永久, 递归监听器. 它们在触发之后不会被移除,并且在子节点递归变化的时候也能触发事件.

监听器的语义

可以在三个读取ZK状态的调用上注册监听器 : exists, getData 和 getChildren, 下面的列表描述了监听器能触发的事件和 启用它们的调用:

  • 已创建事件(Created Event): 使用 exists 调用来开启
  • 已删除事件(Deleted Event): 使用 exists, getData, 和 getChildren 来开启
  • 已变更事件(Changed Event): 使用 exists , getData 来开启
  • 子节点事件(Child Event): 使用 getChildren 来开启

持久化, 递归监听器

3.6.0 版本以后有: 有变量来控制监听器触发后是否就移除. 除此之外还能监听 NodeCreated, NodeDeleted, 和 NodeDataChanged 在子节点上触发(递归, 也是可选的). 不过不会触发 NodeChildrenChanged 因为没必要.

通用 addWatch() 方法来添加一个持久化的监听器. 触发语义和保证和标准监听器是一样的, 唯一的区别就是上面说的不会触发NodeChildrenChanged . 要移除持久化监听器的话, 使用 removeWatches() 并指定 监听类型为 WatcherType.Any.

移除监听器

调用 removeWatches 方法来移除已经在znode上注册的监听器.同时,即使在没有连接上服务器的时候, ZK客户端也可以通过设置本地的flag为true来移除本地的监听器.

移除成功会触发的事件:

  • Child Remove event: getChildren 添加的监听器
  • Data Remove event : exists 或者 getData 添加的监听器
  • Persistent Remove event: 持久化监听器

ZooKeeper对于监听器的保证

  • 监听器是按照事件, 其他监听器 和 异步响应来排序的. ZK客户端库保证所有东西都是按照顺序分发的
  • 客户端会在看到znode新数据之前接收到的事件, 接收事件先于发现变化
  • 来自ZooKeeper的监听器事件的顺序对于ZooKeeper服务的更新顺序

关于监听器的重要事项

  • 一设一触发: 标准监听器只会触发一次. 触发后还想监听则要重设.
  • 丢失的可能: 因为标准监听器只会触发一次, 触发完重设监听器的请求到服务器之间会有个延迟, 也就意味着这期间的多个变化可能会丢失.
  • 一个监视器对象 或者 函数/长下文对, 对于给定的通知只会触发一次. 比如如果相同的监视器对象同时注册了相同文件的 existsgetData 调用, 然后该文件被删除的时候, 监视器对象只会被触发1次 而不是 2次.
  • 当你和服务器断开连接的时候,无法监听事件直到连接恢复.因为这个原因, 会话事件会被发送到所有外部的监控器处理器. 使用 会话事件 来进入"安全模式": 断开连接的时候将不会接收到事件, 所以你的程序在这种情况下应该小心行事.(意思是当接受到会话事件被通知断开的时候, 程序就应该要进入一种允许丢失事件的处理逻辑,即是容错处理)

使用ACLs进行访问控制

ZK使用ACL来进行访问控制, 控制节点的数据节点. ACL的实现有点类似于 UNIX 文件的访问控制, 使用的是 权限位 来表示 是否允许进行某个操作. 不过不像 UNIX 的权限系统,ZK 的权限不只有三个域(拥有者, 组 和 其他). ZK 没有拥有者的概念, 相反, ACL指定id 的集合 和 其关联的权限.

注意, ACL 只属于某个 znode , 不会递归地传递到子节点.

ZK 使用 插件式认证模式. Id 使用 模式:表达式 的形式来指定, 其中的模式就是和那个 id 关联的认证模式. 比如说: ip:172.16.16.1是一个 主机地址为 172.16.16.1 , 模式为 ip 的 id; 而 digest:bob:password 是 用户名为 bob 的使用 digest 模式的 id.

当一个客户端连接到ZK 并且认证之后,ZK会将该客户端关联的所有id 和 这个连接关联起来. 当客户端试图访问节点时,将根据znodes的acl检查这些id. ACL由 (模式:表达式, 权限) 组成. 表达式的格式是根据 模式来的. 比如 (ip:19.22.0.0/16, READ) 赋予了 ip 为 19.22 开头的客户端 读取的权限.

ACL 权限

权限:

  • CREATE: 允许创建子节点
  • READ: 允许 读取节点的数据 和 子节点列表
  • WRITE: 设置节点的数据
  • DELETE: 删除子节点
  • ADMIN: 设置权限

CREATE 和 DETETE 从 WRITE 中分离出来了, 这是为了更好的权限控制体验.举例如下:
你需要 A 能够设置ZK节点的值, 但是不能创建或者删除子节点.

CREATE 但不能 DELETE: 客户端的创建了节点, 然后又想要其他节点能够添加, 但只有创建该节点的客户端能删除.

同时, AMDIN 权限存在的原因是 ZK 没有 文件拥有者的 概念. ZK 不支持 LOOKUP 权限 ( 允许列出目录 ). 每个人都已经默认允许 LOOKUP 权限了. 这允许你来查看 node, 但也仅仅如此.(问题是, 如果你想调用 zoo_exists() 在一个不存在的节点上, 就不会检查权限.)

ADMIN 权限在 ACL中扮演一个特殊的角色: 为了拿到znode的ACL, 用户必须 拥有 READ 或者 ADMIN 权限, 当时如果没有 ADMIN 权限, hash值会被隐藏.

ACL的模式:

  • world: 有一个单独的id,anyone, 表示任何人
  • auth: 会忽略任何提供的表达式,并使用当前用户(user), 凭证(credentials) 和 模式(scheme). 任何表达式在ZK持久化ACL的时候将会被忽略.然而 表达式 还是必须提供因为要符号格式.这个模式适用于 用户想要创建 znode 并且 限制为只能自己使用. 如果没有已认证的用户, 那么设置 auth 模式将会失败.
  • digest : 使用 用户名:密码 串来生成 MD5 哈希 用于 ACL ID 标识. 只要传 用户名:密码 的明文就能完成认证.当在ACL中使用的时候, 表达式是 用户名:密码 SHA1 哈希的base64编码.
  • ip: 使用客户端的注解ip作为 ACL ID 标识. ACL 表达式. 表达式的格式是 ip地址/掩码 , addr 需要匹配尽可能多的ip地址
  • X509 : 客户端使用 X500 规则作为 ACL ID标识. ACL 表达式是客户端的 X500 的规则名, 当使用安全端口的时候, 客户端会自动的认证并且它们的X509认证信息会被设置.

插件式ZK认证

认证不仅可以使用内置的上面介绍的模式, 还可以自定义. 首先看一下认证过程:

  1. 认证客户端: 根据发送过来的 或者是收集到的关于连接的信息进行认证
  2. 在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();
}

getSchem 返回标识该插件的字符串.因为我们支持多个方法的认证, 所以一个认证凭据或者 idspec 总是带有 模式 (scheme: )作为前缀. ZK服务器使用 插件返回的这个模式 来决定模式应用到哪些 id.

handleAuthentication 是在客户端发送验证信息的时候调用的.ZK服务器会根据客户端发送过来的模式和 getScheme 比较来获取对应的实现类. 验证成功的话就会调用 cnxn.getAuthInfo().add(new Id(getScheme(), data)) 来关联连接 和 认证信息; 验证失败会返回错误.

认证插件涵盖了设置和使用ACL的任务. 当ACL设置到了znode上之后, ZK服务器会传条目的 id 部分来调用 isValid(String id) 方法. 如何验证 id 是否正确由插件来实现. 比如, ip:172.16.0.0/16 是合法的, 但是 ip:host.com 不合法. 如果新的 ACL中包含了 “auth” 条目, isAuthenticated 会被用来决定这个模式下连接已经关联的认证信息是否应该添加到ACL中. 有些模式不应该包含, 比如: 客户端的IP地址就不应该被添加.

检查ACL的时候会调用 matches(String id, String aclExpr). 它需要匹配客户端的认证信息 和 相关的 ACL 条目.为了找到应用到客户端的 条目, ZK 服务器会会遍历每个条目的模式, 如果有该模式对应的认证信息, 就会调用matches(String id, String aclExpr)方法,并传入之前通过 handleAuthentication 设置到连接上的认证信息给id参数; 给 aclExpr 参数 传入 ACL条目的 id. 认证插件使用它自己的逻辑和匹配模式来决定 id 是否包含在 aclExpr 中.

有两个内置的认证插件: ipdigest. 额外的插件可以通过系统属性添加.ZK启动的时候会检查 zookeeper.authProvider. 开头的系统属性,将其值作为插件的类名. 可以通过 -Dzookeeeper.authProvider.X=com.f.MyAuth 设置或者直接到配置文件:

authProvider.1=com.f.MyAuth
authProvider.2=com.f.MyAuth2

主要保证属性的后缀是唯一. 如果冲突了, 只有其中1个会被使用.同时所有服务器的插件定义都是相同的, 要不然的话客户端使用的认证方式就无法通用.

3.6.0版本之后: 方便的抽象类

public abstract class ServerAuthenticationProvider implements AuthenticationProvider {
    public abstract KeeperException.Code handleAuthentication(ServerObjs serverObjs, byte authData[]);
    public abstract boolean matches(ServerObjs serverObjs, MatchValues matchValues);
}

这个类相比接口增加了新的参数:

  • ZooKeeperServer : ZK服务器实例
  • ServerCnxn: 当前的连接
  • path:ZNode的路径, 如果没有的话则为null
  • perm: 操作值或者为 0
  • setAcls : 当 setAcl() 方法正在被操作的时候, ACL列表会被设置.

一致性保证

ZK 是一个高性能的, 可扩展的服务. 读写操作都被设计成高效的, 但是读还是比写要快的. 原因为在读取的时候,ZK 可以使用旧的数据, 这要得益于ZK的一致性保证:

  • 序列一致性: 客户端的更新会按照发送到服务器端的顺序执行
  • 原子性: 更新要么成功要么失败, 不会有部分结果
  • 单系统镜像: 客户端无论连接到那一台服务器, 都会看到相同的服务器视图. 比如, 一个客户端即使断开了连接并重新连接到了另一台服务器, 也不会看到系统的旧视图.
  • 可靠性: 只要应用了更新, 它就会在那个时间点被持久化直到客户端修改了它. 这个保证有两个推论:
    1. 如果客户端收到了成功的返回码, 说明更新操作已经执行了.有一些情况下(通信异常, 超时等等) 客户端无法知道操作是否被执行了. 我们尽可能地减少这种错误了, 但是只有成功码才是会有保证的.(在 Paxos中称为 单调条件)
    2. 任何被客户端观察到的更新操作(通过读取或者是成功更新),将永远不会被回滚.
  • 时间线: 系统的客户端视图保证在一定的时间范围内是最新的(大约几十秒)。客户机可以在此范围内看到系统更改,也可以检测到服务中断。

使用这些一致性保证, 就能在单独(无需ZK上的附加组件)的客户端上更简单地构建高层次的功能比如说领导选举, 屏障, 队列和读/写可撤销锁. 见 清单和解决

注意: 有时候程序员误认为 “各个客户端对于服务的视图是同时一致的”. ZK无法保证每个客户端在每个时间点都有相同的视图. 可能是因为网络延迟, 一个客户端可能会在接收到变化通知之前就执行一个update操作. 考虑两个客户端, A和 B. 如果 客户端A 更新 znode 的 /a 值从 0 变成1 ,然后客户端 B 执行读取 /a 的值, 则 客户端 B 可能会读取到 0 而不是 1, 取决于它连接到的服务器. 如果 客户端 A 和 客户端B 读取相同的值非常重要, 则 客户端 B 应该在读取 /a 值之前调用 sync() 方法. 所以 ZK 本身不保证变化 同步地更新到所有的服务器, 但是 ZK 原语(原始命令) 可以用来构建高层级功能来提供丰富的客户端同步.

语言绑定

支持两种语言的库: Java 和 C.

Java绑定

主要集中在这两个包 org.apache.zookeeper 或者 org.apache.zookeeper.data , 其它包都是ZK内部使用的 或者是 由服务器部分实现的. org.apache.zookeeper.data 包是有生成的类组成的, 供容器使用.

Java里面主要使用 ZooKeeper类, 它的两个构造类区别只在于可选的 session id 和密码而已. ZK 支持不同实例之间的进程的会话恢复. Java程序可能会保存它的 session id 和 密码, 用于 重启 或者 恢复之前使用过的session.

当 ZK 对象创建的时候, 会同时创建两个线程:IO 线程事件线程. 所有 IO 都由 IO线程负责(使用 Java NIO).所有事件的回调由事件线程负责. 会话维护比如说重连ZK服务器和维护心跳由 IO线程 完成. 所有异步方法的响应和 监听事件 由 事件线程 处理. 这种设计有几点需要注意:

  • 异步调用 和 监听器回调 是 每次一个依次执行的. 调用者可以执行任何操作但是其他的回调在那个时间是不会处理的.
  • 回调不会阻塞 IO线程 或者 异步调用
  • 同步调用 可能不会以正确的顺序返回. 比如, 假如客户端按照下面的操作进行: 对 /a 触发 异步的处理 设置其为 true, 然后在完成的回调中同步读取 /a 的值.(也许不是好实践, 但是也不禁止,只是用来做例子而已) 注意如果那个时候 /a 的值在 异步 和 同步 读取之间被修改了, 客户端库在收到同步的读取结果之前就接收到监听事件说 /a 变更了, 但是因为完成回调阻塞了事件队列, 在监听事件处理之前, 同步读取还是读取到新的值而不是期间变化的值.

最后, 关闭的规则比较直接: 只要 ZK 对象被关闭了 或者 接收到了 严重错误事件 (SESSION_EXPIRED 和 AUTH_FAILED), ZK 对象 将不可用. 在关闭的时候, 两个线程将会停止 , 任何将要访问 zk 句柄的行为都是未定义的, 需要避免.

客户端配置参数

以下参数的设置可以通过系统属性, 对于服务端配置, 查看 管理手册 的服务器配置一节.

C 绑定

构建基石: Zk操作指南

这一节考察了程序员可能会遇到的操作, 相比于概念介绍更加底层, 但相比于api更加高层.

处理错误

Java通过抛异常, 调用 code() 拿错误码
C通过状态码,枚举在ZOO_ERRORS类
回调均会传递错误码,具体考察api

连接ZK

略, 主要介绍C语言如何连接ZK, 有兴趣可看

常见错误和解决方案

ZK 的一些陷阱要避开:

  1. 如果你使用监控器, 那么你必须得监听 已连接事件. 当客户端从服务器断开连接的时候, 是无法接收到事件通知的,直到你重新连上. 断开连接期间如果有节点创建了又删除了, 这些事件是会丢失的.
  2. 必须测试服务器失败的情况, ZK服务只要大多数的服务器还存活就没问题, 但问题是你的应用是否能处理这种情况. 现实世界中客户端到ZK的连接可能会丢失.(ZK服务器异常 和 网络分区是连接丢失的常见情况). ZK客户端库会帮你重新建立连接并且让你知道发生了什么, 但是你必须确保你 恢复了 你的状态 以及 任何未完成的请求. 在测试环境中测试而不是等待生产, 用几个服务器构建一个ZK服务器然后重启他们测试一下.
  3. 客户端使用的ZK服务器连接字符串必须和ZK服务器匹配. 如果只是指定了子集, 尽管不是最优,还是可以工作; 但是如果都不匹配就连不上了.
  4. 谨慎地选择事务日志放置的位置. 事务日志是影响ZK性能的关键部分. ZK必须同步日志到媒体之后才能返回响应. 一个专门的事务日志设备是维持高性能的关键. 把日志放在一个繁忙的设备上会影响性能. 如果你只有1个存储设备, 把跟踪文件放在NFS并且添加 snapshotCount; 尽管不能避免问题, 但是能够减轻问题.
  5. 正确地设置 Java 最大堆大小. 避免内存交换是非常重要的. 内存交换到磁盘会降低性能. 记住, 在ZK里面所有的东西都是有序的, 所以如果1个请求交换到了磁盘上, 那么所有其他队列中的请求也被磁盘影响. 为了避免内存交换, 尝试把堆大小设置为物理内存的大小, 减少操作系统和缓存需要的数量. 负载测试是找到最优堆大小的最佳方式. 实在没办法就估个大一点的值, 比如 4G内存的机器, 3G起步估.

其他信息

API Reference : The complete reference to the ZooKeeper API

ZooKeeper Talk at the Hadoop Summit 2008 : A video introduction to ZooKeeper, by Benjamin Reed of Yahoo! Research

Barrier and Queue Tutorial : The excellent Java tutorial by Flavio Junqueira, implementing simple barriers and producer-consumer queues using ZooKeeper.

ZooKeeper - A Reliable, Scalable Distributed Coordination System : An article by Todd Hoff (07/15/2008)

ZooKeeper Recipes : Pseudo-level discussion of the implementation of various synchronization solutions with ZooKeeper: Event Handles, Queues, Locks, and Two-phase Commits.

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值