1、zookeeper数据模型
zk的节点类似于文件系统,和文件系统的区别在于,文件系统的目录是不能存储数据的,但zk的路径及其子路径都可以存储数据,且数据的读写在节点上是原子的;zk并不是数据库或大型文件存储系统,你只能用zk来存储协调数据(coordination data),一般在kb大小,小于1m,如果需要存储大型数据,建议将数据存放在hdfs等文件存储系统,而zk只存指针或偏移量;另外,zk并没有相对路径存在。zk对路径的命名有一些要求,几乎所有的unicode编码都适用,除了以下几点:
- \u0000 空字符不允许使用,因为会造成c语义绑定失败
- \u0001 - \u001F 和 \u007F - \u009F范围内字符不允许使用,因为显示不友好且会confuse
- \ud800 - uF8FF, \uFFF0 - uFFFF范围内字符也不允许使用
- 字符串“zookeeper”不允许使用,这是保留字
- .和..不允许单独使用,zk不允许相对路径,比如”/a/b/./c”、”/a/b/../c”是非法的,但”/a/b/.a/c”、”/a/b/..a/c”是合法的
2、节点
节点类型有四种,分别是PERSISTENT、PERSISTENT_SEQUENTIAL、EPHEMERAL、EPHEMERAL_SEQUENTIAL,可参看CreateMode.java,使用zk java客户端时,必须指定ACL 列表和CreateMode指定节点类型。
2.1、znode节点
zk中每个节点就是一个znode,znode中保存有“状态结构数据(stat structure,该结构数据很丰富,后面会说)”,包括当前数据版本及历史版本、当前acl版本数据、修订的时间戳,每次节点更新数据都会自动增加版本号。客户端如果需要修订或删除节点,必须提供对应数据version信息,如果与当前的版本不匹配,更新失败。
2.2、Ephemeral Nodes会话节点
会话期存在,会话结束自动删除。该类型节点不允许有子节点。
2.3、Sequence Nodes有序节点
形如0000000001的节点地址,在创建节点的时候,可以要求zk在path的后面增加唯一的自增序列号,格式为%010d,在zk中是用4个字节的有符号整数存储,因此存在溢出的可能,比如-2147483647。
2.4、Container Nodes容器节点
从zk3.6.0开始新增的节点,对于leader和lock十分有用,当容器节点中的最后一个节点被删除,该容器节点也将被标记为即将删除。当你向一个不存在的容器节点添加子节点,会报KeeperException.NoNodeException异常。
3、stat structure状态数据结构
每个节点都包含以下数据:
- zxid:zookeeper transaction id,每次节点的变更都会有一个自增票戳(stamp),例如0x200000009、0x20000000a、0x20000000b。包括三个:
- cZxid:节点创建票戳
- mzxid:节点最后修改票戳
- pzxid:子节点最后修改票戳
- time:zk并没有使用真正的时间,而是存储相对与起始(epoch)的毫秒数,包括两个:—有疑问?
- ctime:创建时的毫秒数
- mtime:距离上次修改的毫秒数
- version:对节点的任何变更,都会触发版本的升级,包括三个:
- version:当前节点变更版本
- cversion:子节点变更版本
- aversion:acl变更版本
- ephemeralOwner:如果是ephemeral node节点,将会存储session id,否则存储为0;zk服务器会依据这个session id判断是否删除该临时节点。
- dataLength:当前节点存储的数据长度;
- numChildren:当前节点的子节点个数;
示例:
cZxid = 0x200000005
ctime = Tue Nov 21 15:17:49 CST 2017
mZxid = 0x20000000a
mtime = Tue Nov 21 15:20:17 CST 2017
pZxid = 0x200000005
cversion = 0
dataVersion = 3
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 5
numChildren = 0
5、watches哨兵
每个znode上都可以放置哨兵,当节点数据发生变更,znode会通知哨兵回调函数,并清除znode上的哨兵。在zk客户端的getData()、getChildren()、exists()方法上都有设置哨兵的入参,具体如下:
- 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.
可以使用removeWatches方法移除掉哨兵,当日移除时,getData()、getChildren()、exists()方法也会收到移除事件。
哨兵具有以下几个特征:
- One-time trigger:哨兵是一次性的,使用完即丢弃!,例如getData(“/test”, true),则获取的同时,会在/test节点上设置一个哨兵,如果节点删除或修改,则会收到通知,如果再删除或修改,则不会收到通知。
- Sent to the client:事件的发送是异步的,这里说的是延时问题,虽然在网络延时的因素的作用下,不同的客户端可能会收到不同时间的更新事件,但对于多次修改,zk客户端可以保证一致有序。
- The data for which the watch was set:接口的设计是有针对性的,getData()和exists()使用与监听znode节点, getChildren()使用与监听znode的孩子节点,setData()会触发znode节点的事件,create()和 delete() 则会触发znode节点及其孩子节点的事件。(A client will see a watch event for a znode it is watching before seeing the new data that corresponds to that znode.)客户端可能会在收到事件时,节点的数据并没有更新的情况。
哨兵的创建时在zk服务器本地维护的,当client重新连接时,client会重新注册哨兵到新的zk服务器,这一切都是透明的。但要注意的是,对于判断节点存在的哨兵,如果失去连接前,节点还未创建,而在失去连接中创建或删除了,则重新连接后,将会丢失该哨兵。哨兵是不会收到connect-loss事件的。
6、acl访问控制列表
access control list存在于每个znode节点,用于控制谁能读写。类似于unix的文件权限管理(使用权限位(permission bits)来控制读写权限和范围的),只是与unix不同的是,zk并不是用user、group和word三个标准范围来进行约束的,而是用ids和permissions,即 (scheme:expression, perms),这个被称为ACL entries。
- zk的权限不是继承的,即子节点并不继承父节点的权限,例如/app只能被172.16.16.1读取,/app/test却可以被所有客户端读取。
- zk使用的是可插拔的验证方案(pluggable authentication schemes),支持的ids的格式为scheme: id,其中scheme是该方案的具体说明,比如“ip: 172.16.16.1”,表明该验证是用ip地址验证,且其ip地址为172.16.16.1,当前支持的scheme包括:
- world:目前只有一个词,即anyone,表示所有客户端
- auth:doesn’t use any id, represents any authenticated user.
- digest:信息摘要,使用“username:password”字符串作为acl的id,在zk使用时,采用的是“digest:base64encoded(SHA1(username:password))”模式获取信息摘要验证。
- ip:客户端ip地址视为acl的id。
- x509:使用x500 Principal作为acl的id
- perms表明支持的权限列表:
- CREATE:可以创建子节点
- READ:可以读取节点值及子节点列表
- WRITE:可以set节点值
- DELETE:允许删除子节点
- ADMIN:表明你能设置权限,类似于文件的owner
关于以上的定义,可以查看ZooDefs.java类,perms举例如下:
public interface Perms {
int READ = 1 << 0;
int WRITE = 1 << 1;
int CREATE = 1 << 2;
int DELETE = 1 << 3;
int ADMIN = 1 << 4;
int ALL = READ | WRITE | CREATE | DELETE | ADMIN;
}
服务端如何验证
客户端在发送连接请求时,会发送验证信息给服务端,服务端收到验证信息的同时,也会寻找到与之对应的ACL entries,并调用对应scheme实现验证。
如何自定义scheme
- 实现AuthenticationProvider接口即可(IPAuthenticationProvider还不支持v6地址),接口如下:
public interface AuthenticationProvider {
//简单返回当前支持的scheme
String getScheme();
//cnxn代表客户端到服务端的连接,包括两种实现NIOServerCnxn和NettyServerCnxn,authData是从客户端收到的验证数据,包括ids
//该类需要解析客户端的ids,并存入cnxn.addAuthInfo
KeeperException.Code handleAuthentication(ServerCnxn cnxn, byte authData[]);
//验证客户端的id是否格式合法
boolean isValid(String id);
//这个是真正的验证方法,aclExpr允许使用通配符
boolean matches(String id, String aclExpr);
//是否要识别node的创立者,ip验证方式不需要,所以返回false,如果返回为true,则会将其添加到节点的ACL列表
boolean isAuthenticated();
}
- 通过配置或属性指定新的验证器
可以使用属性指定(zookeeper.authProvider.),列入Dzookeeeper.authProvider.X=com.f.MyAuth;也可以在配置文件中指定,例如:
authProvider.1=com.f.MyAuth
authProvider.2=com.f.MyAuth2
7、session会话
客户端session状态的状态图如下:
一个session id的大小为64-bit,当client连接上一台zk服务器时,将会收到一个session id和一个用于验证的password,下次client连接另一台服务器时,会把这两个参数都发给新的zk服务器进行验证。如果客户端与服务端失去连接再重连时,服务端会判断当前客户端的session是否过期,如果过期会将状态设置为SESSION_EXPIRED,否则设置为CONNECTED.SESSION的管理是在zk服务端。
如何判断过期呢?
客户端和服务端会维护一个心跳连接(heartbeat, 发送PING request),如果服务端没有在规定的session timeout时间内收到心跳连接,则服务端会判断为会话失效,会话失效后,zk服务器会删除由该session id创建的临时节点,并通知watch了该节点的客户端,而对于已经失效的客户端,一旦其重新连接后,其不但会收到状态变更为SESSION_EXPIRED通知,也会收到节点删除通知。一个示例:
Example state transitions for an expired session as seen by the expired session's watcher:
1. 'connected' : session is established and client is communicating with cluster (client/server communication is operating properly)
2. .... client is partitioned from the cluster
3. 'disconnected' : client has lost connectivity with the cluster
4. .... time elapses, after 'timeout' period the cluster expires the session, nothing is seen by client as it is disconnected from cluster
5. .... time elapses, the client regains network level connectivity with the cluster
6. 'expired' : eventually the client reconnects to the cluster, it is then notified of the expiration
超时时间的协商
服务端默认配置的会话超时时间要求是2~20个tick,客户端创建时会指定一个毫秒时间作为超时时间,服务端收到后会比较这个配置,如果在这个区间,则返回用户指定的,不在这个区间,则会使用2或者20返回。
更新zk服务器地址列表
zk允许客户端在已经链接成功的情况下改变zk集群地址列表,zk客户端将会使用probabilistic load-balancing algorithm,将连接进行重新分配,例如:
- 原先有3个服务器地址,增加2个变为5个服务器地址,则每个zk客户端将会按照40%的概率,选择是否切换新地址,如果确认需要切换,则会随机选择2台新地址中的一个进行连接,如果新地址全部尝试失败,则会回到刚创建的zk客户端时的方式,随机从所有节点选择一台进行连接,如果还是失败,则会采用round robin算法,一台一台尝试连接。
- 假设原先有5台服务器地址,减少为3台,则原先连接未减少的3台服务器的客户端将不会重连,连接被丢弃的2台服务器的客户端,将会随机选择当前3台中的一台进行连接(注意,并不是使用round robin算法)。
8、zk客户端介绍
zk的客户端有java版本和c语言版本两种。对于java包而言主要是org.apache.zookeeper和org.apache.zookeeper.data,其中后者是用hadoop record compiler生成的。ZooKeeper类是客户端类,可以通过传入sessionid和password恢复上一次会话。
zk启动后会启动两个线程:IO线程和事件线程;所有的IO操作都由IO线程执行,包括heartbeat、reconnected、同步方法的响应,其使用的是NIO技术;异步方法的响应及事件回调发生在事件线程。
java客户端配置参数可以参考:http://zookeeper.apache.org/doc/trunk/zookeeperAdmin.html#sc_configuration
9、参考
[1] http://zookeeper.apache.org/doc/trunk/zookeeperProgrammers.html#sc_zkDataModel_znodes