ZooKeeper的名字空间节点(有关znode的一切)

    上一篇有人跟我说比较深奥和抽象,确实,我写这个不是按照循序渐进的写法写的,而是先写那本OREILLY书最不清楚的部分,然后再写次不清楚的……到最后会覆盖zk的绝大多数特性,这时候会再给出一个阅读次序的建议。
    这篇来说说有关znode的一切,比较容易理解,也很容易通过zkCli.sh来实验。



分层名字空间
ZooKeeper的名字空间节点(有关znode的一切)_第1张图片

    上图是ZooKeeper的分层名字空间的示意图,可见这种结构很像典型的文件系统结构:以/分割各元素的一种路径,ZooKeeper名字空间的每个节点都是以这样一个路径来标识的。这样的节点统一称为znode。


Znode

    然而,ZooKeeper的名字空间和文件系统仍有着显著的差别。文件系统典型地有目录和文件,目录可包含其他目录或文件,文件则实际存储数据。而znode既可以有其他子znode,又可以存放数据(严格说是必须存放数据,真没有的话得给个“”),因此znode可以看做是目录和文件的合体。znode被用来存储Byte级或KB级的数据,如状态信息、配置信息、位置信息等,因此znode可存放数据量的最大值默认被设为1MB,请注意,该值不仅计算数据的Byte数,所有子节点的名字也要折算成Byte数计入,因此znode的子节点数也不是无限的。改大这个参数可以存储更多数据或包含更多子znode,但我非常不建议这样做,因为这与ZooKeeper的设计目的相悖,只有数据足够小,才能保证ZooKeeper的高读取性能。如果要存储大量数据,有的是其他解决方案。
    除了数据外,znode还管理了其他一些元数据,存储在stat对象中:
  • 版本号:znode的数据每次更新时,该版本号递增。当客户端请求该znode时,数据和版本号会一起发回。另外,当znode重建时版本号会被重置,这似乎很自然,但很多时候这是巨坑的来源,比如:客户端执行了’set /test “testdata” 1’,即指定版本1写/test节点,之后该znode被删除重建,数据默认置为“”,版本号还是1,此时客户端请求时虽然该版本号仍然存在,但已经是错误的数据了。
  • ACL:即Access Control List,用来限定哪些账号可以操作该znode。ZooKeeper内置了4种ACL模式,第1种是any,即不鉴权;第2种是super,即不受任何ACL约束的管理员模式;第3种是digest,使用用户名和密码进行认证;第4种是SASL,通常使用Kerberos协议来认证,但Kerberos也是个大坑,并且对性能也有一定影响。
  • 时间戳:其实就是zxid,存储该znode创建或修改时的时间戳,用于缓存验证或协调更新等。

    理解了这些就可以做点操作了,所有zkCli.sh的命令如下,主要掌握ls,get,set,create,delete几个就够玩了(应该明白为何zkCli下没有cd这样的命令):
ZooKeeper -server host:port cmd args 
        connect host:port 
        get path [watch] 
        ls path [watch] 
        set path data [version] 
        rmr path 
        delquota [-n|-b] path 
        quit  
        printwatches on|off 
        create [-s] [-e] path data acl 
        stat path [watch] 
        close  
        ls2 path [watch] 
        history  
        listquota path 
        setAcl path acl 
        getAcl path 
        sync path 
        redo cmdno 
        addauth scheme auth 
        delete path [version] 
        setquota -n|-b val path 


Znode的四种类型

    持久节点/临时节点:持久节点只能用delete来删除,通常用来保存应用的持久化数据,比如主从模式下的任务分派情况,这种数据不应该由于主节点的崩溃而丢失。临时节点则顾名思义,除了客户端主动删除该节点时会消失外,创建该znode的客户端断开连接时该节点也会消失,因此它常用来检测会话的有效性,例如主从模式下的主节点注册一个znode,当该主节点崩溃或断开时,该znode会立即消失,于是应用可以检测到该故障并重新选举主节点,这种用法极为常见比如kafka。另外,临时节点不允许有子节点。
    有序节点:znode还可以被设置为有序节点,当这样的节点被创建时,一个会自动递增的序号会加到路径之后,比如客户端创建一个有序znode并指定路径为/tasks/task,则第一个创建的节点是/tasks/task-0000000001,该节点消亡后再创建这样的节点会产生/tasks/task-0000000002,以此类推。有序znode提供了创建具有唯一命名的znode的方式。
    这两种类型互相结合后就会产生znode的四种类型:持久的,临时的,持久有序的,临时有序的。


监视和通知

    我们知道,访问ZooKeeper的基本方式是使用API,很多场景我们需要访问一个znode,如果使用轮询的方式,不仅不能实时知晓变更(取决于轮询间隔),更恶劣的是会大大增加ZooKeeper的负载。比较好的方式是,对该znode设置一个监视(watch),当某些事件发生时触发一个通知(notification)。注意监视点是一次性的,如果通知被触发后还需要继续监视该znode,则需要设置一个新的watch。
    ZooKeeper异步通知设置watch的客户端。但是ZooKeeper能够保证在znode的变更生效之后才会异步地通知客户端,然后客户端才能够看到znode的数据变更。由于网络延迟,多个客户端可能会在不同的时间看到znode数据的变更,但是看到变更的顺序是保证有序的。
    Znode可以设置两类watch,一个是Data Watches(该znode的数据变更导致触发watch事件),另一个是Child Watches(该znode的孩子节点发生变更导致触发watch事件)。API和watch间的关系如下表:
Java API
Data Watches
Child Watches
getData
设置

exists
设置

setData
触发

create
触发

getChildren

设置
create某znode的子节点

触发
delete
触发
触发
如删除的znode有父节点,其父节点上也触发

    当某个znode的变化可触发监视点时,ZooKeeeper会触发其上可触发的所有监视点,这在某些情况下可能会产生问题,如1000个客户端都在监视某znode的Data时,当该znode的数据发生变化,会触发1000个通知,这会造成性能的波动,比如同一时刻的提交操作可能延迟。解决这一问题,OREILLY书上给出了一个利用有序节点的办法(这块书上写的不错,但翻译依旧很烂,现实中很常用所以整理下):例如n个客户端都抢一把锁。第一种方法是抢到的客户端创建/lock节点,其余客户端便监视该节点的删除事件,当占用锁的客户端完成操作断开时,/lock节点删除,其他客户端获得通知,这种方法就有性能问题。另一种方法是,所有客户端均创建有序节点/lock/lock-xxx,其中xxx为序号,xxx最小的客户端获得锁,其余客户端监视前一个节点,例如有/lock/lock-001,/lock/lock-002,/lock/lock-003三个节点时,001的客户端获得锁,002的客户端监视001的znode,003的客户端监视002的znode,当001的锁释放时,002即获得锁,以此类推,这种方式下每个znode上监视的客户端只有一个,尽管总watches数是一样的,却不会造成性能的波动。
    监视点也是有存储开销的,一个监视点对应服务端的一个Watcher对象,大约消耗300字节内存,因此管理大量的监视点也有不小的开销,运维人员应该注意连接数和产生的监视点数,以免造成OOM。
    另外,如果客户端与ZooKeeper断开连接,客户端就无法触发watches,除非再次与ZooKeeper建立连接。


    关于znode暂时能想到的就这些,如有遗漏会在后续的章节中进行补充。



你可能感兴趣的:(ZooKeeper)