数据模型:
Zookeeper通过分层名称空间来管理数据,和文件系统类型,唯一的不同点是每个znode既可以创建children,同时也可以存储数据。每个znode的路径是通过\分割的字符串,每个路径都是唯一的,不存在相对路径。任意Unicode字符都可以用于构建路径,除了一下几个限制:
空字符串(\u0000)、\u0001 - \u001F和 \u007F、\u009F(不好展示)、 \ud800 - uF8FF,、\uFFF0 - uFFFF
.可以用于路径里name的一部分,但是不能代表一个独立的路径,因为znode不存在相对路径,举个例子: "/a/b/./c"是不合法的,但是/a/b/c./c是合法的
数据节点:ZNode
Zookeeper tree里每个数据节点都成为ZNode。Znode里保存了数据状态,包括版本号、acl访问权限、时间戳等。Zookeeper通过版本号和时间戳来验证缓存和协调更新。每次znode更新成功,他的版本号会递增,无论什么时候客户端检索znode,他都会收到znode的版本号,当客户端更新或者删除数据时,客户端必须提供版本号,如果znode当前的版本号和客户端提供的版本号不一致,则操作会失败。
注意:znode、server、quorum peers和ensemble的差别
In distributed application engineering, the word node can refer to a generic host machine, a server, a member of an ensemble, a client process, etc. In the ZooKeeper documentation, znodes refer to the data nodes. Servers refer to machines that make up the ZooKeeper service; quorum peers refer to the servers that make up an ensemble; client refers to any host or process which uses a ZooKeeper service.
监听器:watcher
客户端可以在znode上添加监听器。当znode发生变化的时候,添加在它上面的监听器会被触发,然后删除。当监听器被触发后,服务端会发送给客户端一个通知。更多详情请参考:http://zookeeper.apache.org/doc/current/zookeeperProgrammers.html#ch_zkWatches
数据访问权限:ACL
客户端通过原子的方式读写znode里存储的数据,读写操作都是对znode里所有数据生效,也就是说,无法对znode里数据的某个字段进行读写操作,每个znode都通过访问控制列表acl来进行权限控制
Zookeeper的目标不是为了提供传统的数据库服务,也不擅长存储长度太大的对象,它管理的是一些协调数据(分布式服务用于协调服务的数据),比如配置信息、集群状态信息等。这些数据通常来说都比较小,不超过kb级别。Zookeeper客户端和服务端都会通过一定的校验来确保存储在每个znode的数据大小不超过1Mb。实际上,平均数据大小应该远比1Mb小。如果数据过大,Zookeeper需要花费更多的时间来处理客户端请求,并且数据的网络传输时延和读写存储时延会增大,这在一定程度上会对某些操作产生影响。如果碰到大数据存储的场景,通常的做法是将数据存放到大数据存储系统例如NFS或者HDFS,然后再Zookeeper里存放指向存储地址的指针。
临时节点:
ZooKeeper还支持临时节点,当session失效后,临时节点会被删除,因此临时节点不允许存在子节点。
有序节点-唯一命名空间:
当创建znode的时候,你可以通过Zookeeper在数据节点路径的最后增加一个逐渐递增的计数器,对于父节点来说,这个计数器是唯一的,格式为%010d(计数器以这种方式格式化以简化排序),比如说 0000000001。计数器是一个带符号整形,由父节点维护。当计数器大于2147483647时会造成溢出。
容器节点(Container Nodes):
Zookeeper3.5.3版本增加了容器节点,这种类型的数据节点是为特定场景服务的,比如选主、锁等。当容器节点的最后一个子节点被删除后,服务端会随后会在某个时间点将容器删除。
限时节点(TTL Nodes):
Zookeeper3.5.3版本增加了限时节点,当创建一个永久节点或者永久有序节点时,可以给这个节点增加一个时间属性(毫秒为单位),当指定时间内节点没有被修改,并且不存在子节点,那么服务端会在随后的某个时间点将此节点删除。注意:必须通过System属性启用TTL节点,因为默认情况下它们被禁用。如果你没有在系统层面启用TTL属性,那么在添加的时候会抛出KeeperException.UnimplementedException异常
Zookeeper的时间机制:
Zookeeper通过多种机制来进行时间回溯
ZXid:每次状态变更的时候,Zookeeper都会收到一个Zxid形式的时间戳(Zookeeper的事物id),这个id记录了集群里所有的变更次数,每次变更的时候,集群就会生成一个新的唯一zxid。如果zxid1小于zxid2,就表示zxid1发生在zxid2之前。
版本号:znode有三种类型的版本号,数据节点变更的次数、子节点的变更次数、节点ACL配置的变更次数,每次变化都会导致某个版本号递增。
Ticks时钟:集群通过时钟来对多服务节点之间的比如session超时、连接超时等进行计时,最小会话超时时间是最小时钟的2倍。如果客户端设置的最小会话超时时间小于最小时钟的两倍,那么服务端会告知客户端会话超时实际上是最小会话超时。
真实时间:
除了在znode创建和znode修改时将时间戳放入状态数据之外,ZooKeeper根本不使用实时或时钟时间。
Zookeeper的状态数据结构:
数据节点的状态结构由一下字段构成:
czxid 创建此znode的zxid
mzxid 最后一次更改此znode的zxid
pzxid 最后一次更改此节点子节点的zxid
ctime 从创建纪元开始到创建此节点的毫秒数
mtime 从创建纪元开始到最后一次更改节点的毫秒数
version 数据节点变更的次数
cversion 子节点的变更次数
aversion 节点ACL配置的变更次数
ephemeralOwner 此节点owner的会话id,只对临时节点有效
dataLength 节点数据大小
numChildren 子节点数量
Zookeeper Sessions
客户端通过创建服务端句柄,并且绑定此句柄来建立会话连接。创建成功后服务端句柄进入connecting状态,此时客户端开始尝试和集群的某个server建立连接,一旦连接建立,服务端句柄进入connected状态,在整个正常请求过程中,句柄会保持在connected状态。但是当发生不会回复的异常比如会话超时、鉴权失败等情况时,句柄会进入closed状态。如下图所示
为了能够和服务端建立会话连接,客户端必须提供host:port形式的服务端地址。比如 ( "127.0.0.1:3000,127.0.0.1:3001")。客户端会随机的选一个地址进行连接。如果连接失败,或者客户端由于任何原因和服务端断开,客户端会从地址列表里挑选另一个地址进行连接,直到连接成功建立。
Zookeeper3.2版本支持在服务端地址增加chroot。比如当你使用"127.0.0.1:3000,127.0.0.1:3001,127.0.0.1:3002/app/a"作为连接地址的时候,客户端会进入到此路径,并且所有的操作都会基于此路径之下。当不同的服务需要使用不同的路径时,这个功能就显得非常实用。
当客户端创建好句柄后,ZooKeeper会给客户端创建一个64字节大小数字作为会话id。当客户端和不同的server节点通信的时候,它会将会话id写到握手信息里。出于安全考虑,服务端给会话id创建了个只有集群机器能识别的密码,在连接建立后,服务端会把这个密码和会话id一起发送给客户端。无论何时,客户端在发送会话id的时候,都会带着这个密码。
客户端通过方法调用创建session的时候,会传入timeout参数,服务端收到请求后,会返回给客户端可以接受的超时时间。目前服务端要求超时时间在2-20倍时钟时间之中。
会话过去机制是在server端维护的,而不是客户端,当客户端建立会话的时候,它会传入一个超时时间,server端用这个值来判断会话是否超时。如果服务端在超时时间内没有收到客户端的心跳,服务端就会判断此会话已过期。此时集群会将这个会话周期内创建的临时节点全部删除,并且触发注册在节点上的所有监听器。除非重新建立tcp连接,否则客户端会一直保持失联状态。重新建立连接后,客户端会收到回话超时提醒。
客户端和服务端建立连接的时候,还会传入一个默认的监听器。当客户端发送状态变化的时候,这个监听器就会被触发。比如当客户端和服务端断连、会话超时等,客户端都能感知到。在建立新连接的时候,监听器收到的第一个事件是会话建立事件,因此监听器需要将初始状态设置成disconnected。
客户端通过发送请求来保持会话,当会话快超时但是没有业务请求的话,客户端会发送PING请求,通过这种方式既让服务端知道客户端依旧存活,也让服务端感知此会话依旧有效。PING的时间足够保守,以确保有合理的时间来检测死连接并重新连接到新服务器。
连接建立以后,有两种情况会导致API抛出connectionloss异常,1是当会话失效后客户端发起请求,2是当客户端与server连接断开后的异步请求
Zookeeper3.2版本增加了SessionMovedException:当一个服务端又收到了新会话id的请求时,原server端就会把老的会话id丢弃,此时就会抛出此异常。最常见的场景是客户端像server1发送请求,由于网络延时导致客户端连接断开,然后连到了新的server2,当这个请求最终到达server1的时候,server1发现会话已经过期,因此会移除此会话,关闭客户端连接。此时由于客户端连接到了server2,因此它根本就不会感知到会话被移除。另一种情况是当两个客户端用同一个会话id和密码与服务端发起重连的时候,其中一个重连成功,生成了新的会话id,第二个客户端请求会被server端丢弃。这时候第二个客户端就能够捕获到此异常了。
服务端列表更新:
我们可以通过方法调用,用新的服务器列表来替换就的连接地址池。此方法可能会导致现有连接断开。这取决于重连算法。举个例子:当老的连接池有3个server,新的连接池在老的基础上加了两个server变成5个,那么该算法将使客户端以概率0.4丢弃与其连接的当前主机的连接,并且导致客户端随机选择一个新主机建立连接。再举一个例子,加上地址池里原有5台server,我们把它去掉2台,连接到原3台的客户端会保持连接,连接到这两台的客户端会断开连接,然后随机从3台中选一台重新建立连接。
在第一个例子中,如果客户端重连到所有新的server失败,那么它会继续尝试重连老的server。接下来客户端就进入正常模式,下次重连的时候会从所有server中随机选择服务端进行重连。
Zookeeper监听器(Zookeeper Watcher):
用户可以选择给所有的读请求添加监听器,我们来看下官方的定义:监听事件是一次性触发的,当数据发送变化时,监听事件会被发送给设置了监听器的客户端,三个重点:
1、触发一次:当一个客户端发送了getData("/znode1", true) 请求,然后znode1数据发生了改变,此时在znode1上添加了读事件的客户端会收到一个读事件,但是当后面znode1再次发生变化后,客户端不会再收到此事件了。
2、发送给客户端:这意味着在事件到达客户端时,有可能实际的操作结果还没有达到客户端。事件是通过异步的方式发送给客户端的,Zookeeper提供了一种机制保证顺序性:在事件到达客户端之前,客户端不会看到数据的变化。由于网络延时或者某些原因,Zookeeper无法保证不同的客户端在同一时刻获取到同一事件,但是它能保证事件变化和数据执行结果达到的时序性。
3、添加在数据上的监听器:指的是数据改变的方式。可以理解Zookeeper有两种类型的监听器,数据节点监听器和子节点监听器。getData() 和 exists() 用于设置数据监听器。getChildren() 用于设置子节点监听器。因此setData()会触发数据监听器,create()会触发数据节点监听器和父节点的子节点监听器。delete()会触发数据节点监听器、本节点的子节点监听器和父节点的子节点监听器
客户端在和某个server建立连接后,相关的znode监听器设置会保存在此server端。当客户端连接到新服务器时,将针对任何会话事件触发监听器。 从服务器断开连接时不会收到监听事件,所以当和新server建立连接后,如果有必要,需要将原来监听器全部重新注册一遍。举个事件丢失的例子:在断连期间,如果一个znode被创建然后又被删除了,那么这个客户端在重连后,会收不到znode的existence事件。
监听器的语义(Semantics of Watches):
我们可以通过3个读操作来添加监听器:exists, getData, 和 getChildren。以下例子详细的介绍了如何添加监听器以及触发监听器的事件:
Created event: 通过调用exists来添加
Deleted event: 通过调用exists, getData, 和 getChildren来添加
Changed event: 通过调用exists 和 getData来添加
Child event: 通过调用getChildren来添加
删除监听器:
我们可以通过removeWatches来删除监听器。以下列出了当监听器被成功删除后会触发的事件:
Child Remove event: 通过调用getChildren添加的监听器
Data Remove event: 通过电影exists 或者 getData添加的监听器
Zookeeper访问权限控制:
Zookeeper的权限访问控制和Unix文件访问控制差不多,它使用权限位来允许/禁止针对节点的各种操作以及权限的范围。不同的是他不是使用3位来控制权限,在Zookeeper里,znode没有owner的概念,ACL指定ID和这些ID对应的权限集。
注意,ACL权限通常情况下知识针对znode有效,而不是针对子节点。Ids通过scheme:expression来定义。schema代表了认证方式,比如 ip:172.16.16.1 代表使用ip为172.16.16.1的认证方式,而digest:bob:password代表使用用户名为bob和密码认证的方式。当客户端连接到ZooKeeper并对其自身进行身份验证时,ZooKeeper会将与客户端对应的所有ID与客户端连接相关联。 当客户端尝试访问节点时,将根据znode的ACL检查这些ID。ACLs由(scheme:expression, perms)构成。举个例子: (ip:19.22.0.0/16, READ) 代表给ip段添加读权限。
ACL权限:
Zookeeper支持以下几种权限:
CREATE: you can create a child node
READ: you can get data from a node and list its children.
WRITE: you can set data for a node
DELETE: you can delete a child node
ADMIN: you can set permissions
CREATE和DELETE权限已从WRITE权限中删除,以获得更精细的访问控制。
CREATE 但是不 DELETE:客户端通过在父目录中创建ZooKeeper节点来创建请求。 您希望所有客户端都能够添加,但只有请求处理器才能删除
内置ACL方案:
world 代表所有客户端.
auth是一种特殊方案,它忽略任何提供的表达式,而是使用当前用户,凭据和方案。 在持久化ACL时,ZooKeeper服务器会忽略所提供的任何表达式(无论是使用SASL身份验证还是用户:密码,如使用DIGEST身份验证)。 但是,仍必须在ACL中提供表达式,因为ACL必须与表单scheme:expression:perms匹配。 提供此方案是为了方便,因为它是用户创建znode然后将该znode的访问权限仅限于该用户的常见用例。 如果没有经过身份验证的用户,则使用auth方案设置ACL将失败。
digest 使用username:password字符串生成MD5哈希,然后将其用作ACL ID标识。 通过以明文形式发送用户名:密码来完成身份验证。 在ACL中使用时,表达式将是用户名:base64编码的SHA1密码摘要。
ip 使用客户端主机IP作为ACL ID标识。 ACL表达式的形式为addr / bits,其中addr的最高有效位与客户端主机IP的最高有效位匹配
x509 还不是很了解
参考文章:http://zookeeper.apache.org/doc/current/zookeeperProgrammers.html