来源于:
http://zookeeper.apache.org/doc/trunk/zookeeperProgrammers.html#ch_guideToZkOperations
讲对于 zookeeper 编程的简介:
简介:
本文讲了如何应用zk的优势创建分布式应用,包含了如下几个部分.
首先4个章节从一个比较抽象的层面讲了zookeeper的概念, 这使读者能够理解zk怎么工作和这样使用zk工作. 他不包括源代码,假像读者熟悉分布式计算的问题. 以下是四个部分:
后面的四个部分提供了具体的编程信息:
zk有一个像目录结构一样的分布式文件结构.不一样的是在一个命名空间的每一个节点可以有子节点.这就像在一个文件系统中,他允许一个文件可以是目录. 节点的路径一定要规范,绝对的,用斜杠分割的,没有相对路径. 任何unicode 字符可以用做一条路径除了以下限制:
\u0000(null) 不能做一个路径.
以下编码不能作为path 因为 表现不好: \u0001 - \u0019 \u007F - \u009F
以下编码不允许: \ud800 -uF8FFF, \uFFF0-uFFFF, \uXFFFE - \uXFFFF (where X is a digit 1 - E), \uF0000 - \uFFFFF
每一个节点在 zk中被称为 znode. Znodes 维护一个结构包括数据变化的版本号, acl变化.这种结构有个时间戳, 用这个时间戳去验证 cache和同步.每次数据变化, 数据的版本号增加.事实上, 当一个客户端检索数据的时候, 同时会收到数据的版本号. 当客户端更新或者删除数据的时候,也必须指明znode数据的版本,如果提供的版本号跟真实数据的版本号不一样, 更新失败.
Znodes 是一个程序员主要接触的.有几个特征值得关注.
Watches
客户端可以watch Znodes. 任何变化讲出发这个 watch.当一个watch触发了, zk会给客户端一个通知.
存在znode中的数据自动读写. 读的话是从一个znode中读出左右的数据流,而写的话会覆盖所有的数据.没一个node有一个权限控制列表(ACL) 限制谁可以做这个.
zk的设计初衷不是为了作为一个db或者大对象存储. 而是管理需要协作的数据.这种数据的形式可以是配置文件,状态信息,集结信息等等. 需要协作的数据的一个公共的特点是: 他们都很小,以kb 来衡量.zk客户端和服务端都会检测保证znodes是一个小于1M的数据,真实情况是 应该比1M小很多.操作大数据将导致一些操作话很多时间,有可能将一些操作延时, 需要将数据从网络上从一个地方移动到另外一个地方.如果存储打数据是必须的, 可以考虑大数据存储系统. 比如NFS或者 HDFS, 然后在zk上存储数据的指向.
zk有个 临时节点的概念, 当且仅当连在这个znode的 session 存在的时候, 临时节点存在.当这个session消失,znode删除.由于这种行为, 临时节点不允许有字节点.
当创建znode的时候, zk同时分配一个递增的计数器指向地址的最后. 这个计数器跟父节点的不一样.这个计数器用来帮助父节点存储存储队列中的下一个数字.当到2147483647 的时候,会从头开始.
Zxid
对于zk状态的改变用一个叫做 zxid(ZooKeeper Transaction Id)的形式存储. 用来对zk的改变做一个顺序的记录.每一个改变用一个不一样的zxid.如果 zxid1 比 zxid2 小,那zxid1 在zxid2 之前发生.
Version numbers
znode的每一次变化将会导致 znode的版本号增加.一个znode有如下三种版本号. version:znode 数据变化的次数,cversion: znode子节点变化的次数.aversion: ACL变化次数
Ticks
当我们在多台server上搭建 zk的时候, server用 ticks 来定义像 状态上传, session 超时, 在peers中连接超时等的时间.tick time 只是不直接显示了最小session 超时时间(tick time的两倍).
Real time
zk不用真实的时间,而把时间戳放到znode的 状态结构中,当这个znode创建或者修改的时候.
zk中每一个znode的状态结构由以下域组成:
czxid:
创建这个znode的 zxid.
mzxid:
最后一次修改这个znode的zxid.
ctime:
以毫秒为单位,这个znode创建的时间.
mtime:
以毫秒为单位, 最后一次修改这个znode的时间.
version:
这个znode被修改的次数.
cversion:
这个znode子节点被修改的次数.
aversion:
这个znode ACL被修改的次数.
ephemeralOwner:
如果这是一个临时节点, 存储的是session的id.如果不是临时的, 是0.
dataLength:
这个znode的数据长度.
numChildren:
这个znode的子节点的数量.
zk客户端通过一个叫做通道(handle)的东东来跟服务端创建一个session.一旦建立起连接,这个handle从CONNECTION 状态开始. 客户端尝试连上一个服务端的server,然后把状态改为 CONNECTED.一般情况下,handle只会有这两种状态.如果一个不可避免的错误发生,比如session超时, 验证失败,应用关闭handle, 这个handle将是CLOSED状态.
下图展示了可能的状态转换:
创建一个连接, 应用层必须提供的一个可以连接的以逗号分割的主机列表,zk server的地址:(e.g. "127.0.0.1:4545" or "127.0.0.1:3000,127.0.0.1:3001,127.0.0.1:3002"). 客户端将随机选择一个server 尝试连接. 如果连接失败,客户端将重新从list中自动选择下一个server,自动连接建立.
Added in 3.2.0:一个可选的"chroot"后缀可以加载一个连接请求后面.用起来像是这样: "127.0.0.1:4545/app/a" or "127.0.0.1:3000,127.0.0.1:3001,127.0.0.1:3002/app/a" 当一个客户端在 /app/a 是所有者.那请求的路径可以是相对路径,比如 getting "/foo/bar" 将导致一下命令被执行"/app/a/foo/bar".这种特性适用于多租户的环境下,每个租户用 zk的一部分.而编码的时候可以重用一部分代码.
当一个客户端得到 zk service 的handle的时候, zk创建一个 zk session, 表现为一个64 bit的数字, 分配给这个客户端.如果这个客户端连上不同的server, 他会把这个 session的id 作为握手协议的参数传过去, 作为一个安全措施, 这个server会创建这个session的 密码, 所以所有的server都可以验证.这个密码在创建的时候随着 id 一起传给客户端.客户端在重新建立这个session的时候把这个session和密码一起传给新的server.
当一个客户端如果被zk 集群划分(比如按照ip 做hash) , 客户端会到list中寻找特定的server 在session创建的时候.最终, 在客户端,服务端在重新建立连接后, session会过度到 "connected"状态(在 session 超时时间内)或者到"expired"状态(在 session 超时时间外). zk并不鼓励在断掉连接的时候创建一个新的对象.(new ZooKeeper.class or zookeeper handle). zk 客户端库中会处理重连. 仅当 你被通知 session 超时的时候再去创建新的session.
session超时被zk集群自己管理,而不是客户端.当session 建立的时候会提供一个"timeout"的属性.这个属性用在集群中决定什么时候客户端的session 超时.超时发生在集群在特定的时间内没有收到客户端的回应.在session 超时的时候,集群会删除这个session创建的所有的临时节点, 通知所有连着的客户端这个变化. 从这个意义上来讲, 超时的这个session 将不会收到session超时的通知,直到TCP 连接被再次建立.
以下是session超时时候的状态转换,被 watcher 看到的:
1. 'connected' session被创立,客户端 ,服务端无障碍交流.
2. ... 客户端被划分,重新选择服务器.
3. 'disconnected' : 客户端跟服务端失去连接.
4. ... 时间流逝,在'timeout'之后 集群将session 超时, 什么都不会被客户端看到.
5. ... 时间流失, 客户端恢复网络,跟集群建立连接.
6. 'expired': 最终客户端重连上 集群,被通知超时.
另外session的一个参数是 默认 watcher. 客户端watcher 在状态改变的时候被通知.比如说, 客户端跟集群失去连接会接到通知.在新连接这种情况下, 第一个给watcher的事件往往是 session 连接的时间.
这个session 会持续保持活跃. 如果一个session 在一段时间内是闲置的,将会导致这个session 超时,这个时候,客户端就会发送PING 来保持这个连接活跃.这个PING 不光是让服务端知道客户端是活着的, 也让客户端保证服务端的连接是好使的.PING的时间足够让各方来识别这条连接.
一旦这条连接建立起来,有两个原因让客户端代码抛出 connectionloss的异常.
1. 客户端想对已经无效的连接做操作.
2. 如果一些异步调用当连接已经断掉的时候.
Added in 3.2.0 -- SessionMovedException.
这是一种不被客户端发现的内部错误.这种异常在收到在不同server 已经建立连接的情况. 详细点说这种情况, 客户端发送一个请求给一个server , 由于网络堵了, 客户端超时, 连接一个新的server.网络又通了, 请求发送到老的server上,老的server发现session已经转移,关掉这个连接.客户端不会收到这种异常,因为他根本不会从老的连接读东西.一种情况会在客户端出现这种异常,就是两个客户端尝试用重新连接,用这个 session的id, 和 密码.一个将正确的建立连接.另外一个会超时.
ZooKeeper Watches
所有从zk读的操作 - getData(), getChildren(), and exists() 都有一个选项,设置一个watch 进去处理副作用.这里是zk定义的watch的定义: 一个监听的 watch是一个一次性的触发器,当客户端设置的数据变化的时候触发.以下三个关键点帮助理解watch:
One-time trigger
一个watch的时间在数据发生变化的时候发送到客户端.举个例子, 如果一个客户端执行 getData("/znode",true)的操作,然后/znode 变化或者被删除的时候,客户端会收到一个事件.如果/znode再次变化, 将不再有事件发送到客户端.除非客户端get了其他数据.
Sent to the client
假设一个事件在到客户端的路上,但是有可能没有达到客户端,在发起改变的客户端收到正确的返回码之前.到watchers 的都是异步的操作. zk保证了顺序: 一个客户端将永远不会在收到watch 事件之前看到变化.网络延迟或者i其他因素可能导致不同的客户端看到返回值和 watch 不一样.关键是被不同客户端看到的将有一致性的顺序.
The data for which the watch was set
一个节点有好几种改变的方式. zk维护两种watch: 数据 watch 和字节点watch. getData 和 exists 设置数据 watch.getChildren设置 自己点 watch. getData 和 exists返回节点的数据, getchildren 返回字节点的一个list.因此, setData将触发数据watch.成功的 create 将出发 数据 watch 和 字节点watch. 同理是delete操作.
watches 在zk server的本地进行管理.这样方便轻量级的set,维护和分发. 放一个客户端连到一个新的 server,这个watch将包括所有的session 事件.在连接断掉之后就收不到watch了.当一个客户端重连,所有之前注册的 watch将被重新注册和触发.一般来说,这对上层是透明的.
关于watches. zk做了如下保证:
watches的东西在其他事件,其他watches 或者异步的回复是有序的.zk 客户端库可以保证这一点.
在看到数据更新之后的新数据之前客户端可以收到watch事件.
客户端看到的 watch 事件的顺序 跟时间服务端改动的顺序是一样的.
Things to Remember about Watches
watches是一次性的.如果想看到数据未来的变化,设置另外一个watcher.
因为watch是一次性的,在得到事件和发送新的请求得到一个watch, 有可能看不到每一次的变化.准备好处理一种情况:znode改变了很多次,在得到事件和设置新的watch之间.
一个watch对象,只会被通知一次.比如说,一样的watch对象注册同一个exist事件.对于同一个文件.这个文件被删除,这个对象只会被调用一次.
当你从server失去连接.不会收到任何事件直到重连.可以使用 session 事件保证程序的严谨性.
zk用ACL做权限控制。ACL跟unix很像,每一个节点都有标识位。跟unix不一样的是,一个znode不仅仅局限于三种权限控制,owner,group,and other。zk没有owner的概念,取而代之的是有个id的set的集合, 权限跟这些id息息相关。
这些ACL的权限控制只针对一个znode,不包含子节点们,对于子节点不起作用。举个例子, /app 对于 10.3.24.13可读,而 /app/status 对于所有可读。也就是说ACL不具有递归性。
zk支持那种可插入的身份验证。id可以作为身份验证的一种方案,举个例子: ip:172.16.16.1 就是172.16.16.1 的id。
当客户端连接zk ,验证自己的时候,zk从id列表中查找这个客户端,同时检测这些znode的 ACL权限,比如说, (ip:19.22.0.0/16, READ) 给了任何 ip是 19.22 开头的机器读的权限。
zk支持如下几种权限:
CREATE: 创建子节点
READ:读取节点和子节点
WRITE:往节点写数据
DELETE:删除子节点
ADMIN:管理权限
对于CREATE 和 DELETE的权限需要更加细粒度的控制,这种情况如下:
如果一个客户端在一个znode中添加了一个节点,他想让所有的客户端都有加节点的权限,但是只有他自己有删除的权限。
。。。 翻译不下去了,此处省略几十行
zk是一个高性能, 高伸缩的服务。读写的新能都十分优越,尽管读比写更加高效。原因是在读的情况下,zk能够提供老数据,这主要归功于zk一致性的保证。
Sequential Consistency
客户端的更新能够严格按照他发出去的请求顺序。
Atomicity
更新要么成功要么失败,不会出现部分成功,部分失败的情况。
Single System Image
一个客户端只会看到一种情况不管他连接的是哪台server。
Reliability
当一个更新被执行的时候,效果会永久存在直到客户端覆盖这个更新。这个保证有两个推论:
1. 如果一个客户端得到更新成功的返回值,这个操作肯定被执行了。
2. 更新的变化,读或者一次成功的更新,将永远不可能回滚。
Timeliness
在一段时间内,客户端获取的东西总是实时的。
用这些一致性的保证,可以实现更加高级的功能。比如说leader选举,队列,可撤销的读写操作,更多用法请见:Recipes and Solutions
有两个包组成了java binding:org.apache.zookeeper and org.apache.zookeeper.data.
其他的包组成了server端实现的data包里是一些生成的class 作为容器使用的。
java中main class 是zookeeper ,他的两个不同的构造函数只是 session id 和password的区别,他支持session恢复通过一个process的实例,一个java程序能够吧session id 和 password存储起来,重启之后能用到早期程序中的 session id 和password。
当zookeeper 实例被创建之后,两个线程起来:一个IO线程,一个处理事件线程,所有IO操作都通过IO线程实现(用java nio),所有事件的回调都通过那个事件线程处理。session的维持,包括zk的重连和维持心跳,都通过IO线程完成,所有事件的监听都通过事件线程。这种设计有几个注意点:
所有完整的异步调用和watcher的回调都按照顺序进行,一次一个,但是当线程处理的时候是不会有任何反馈的。
回调不会阻塞IO线程或者其他的同步调用。
同步调用可能不会按照顺序返回。举个例子:如果一个客户端做了如下操作:假设一个异步调用读取 /a 节点,同时设置watch 为true,然后同步去读 /a 的内容。
如果这个时候 改变a的值,在异步调用和同步调用之间,客户端库会收到事件说 /a 已经被改变,时间实在同步调用之前,整个回调在阻塞event queue,这个同步的读将会先返回 a的最新的值。