本文主要讲述ZooKeeper中的权限管理机制、Watch以及Zookeeper的客户端框架使用。
ACL
默认情况下,所以客户端能都可以读写任何节点,但有时我们需要控制节点的访问权限,ZK通过ACL机制来解决节点的访问权限问题。
- ZooKeeper的权限控制是基于单个znode节点的,需要对每个节点设置权限
- 每个znode支持设置多种权限控制方案和多个权限
- 子节点不会继承父节点的权限,客户端无权访问某节点,但可以访问它的子节点。
ACL权限控制,使用schema:id:permission
来标识,主要涵盖3个方面:
-
schema: 权限模式,又称鉴权策略
- world: 只有一个用户:anyone,代表所有人(默认)
- ip: 使用IP地址认证
- auth: 使用已添加认证的用户认证
- digest: 使用“用户名:密码”方式认证
-
id: 授权对象,指权限赋予的用户或者一个实体。sheema与id的对应关系:
- world: 只有一个id,即anyone
- ip: 通常是一个ip地址或地址段,比如
192.168.0.1
或192.168.0.1/2
- auth: 用户名
- digest: 自定义:通常是“username:BASE64(SHA-1(username:password))”
-
permission: 权限
- CREATE,简写为c,可以创建子节点
- DELETE,简写为d,可以删除子节点
- READ,简写为r,可以读取节点数据及显示子节点列表
- WRITE,简写为w,可以设置节点数据
- ADMIN,简写为a,可以设置节点访问控制列表
这5种权限中,delete是指对子节点的删除权限,其它4种权限指对自身节点的操作权限
查看ACL
查看/zk_test
节点权限
默认创建的节点的权限是最开放的,也就是world:anyone:cdrwa
。
设置ACL
设置节点对所有人都有写和管理权限
在上下文中添加一个用户:
add auth user1:12345
设置/acl_node只有user1可以操作
setAcl /acl_node auth:user1:12345:rdwca
其他用户再访问/acl_node会报权限不可用
Watch
一个Zookeeper的节点可以被监控,包括这个目录中存储的数据的修改,子节点目录的变化,一旦变化就会通知设置监控的客户端,通过这个特性可以实现配置的集中管理、集群管理、分布式锁等。
一个Watch事件是一个一次性的触发器,当被设置了Watch的数据发生改变的时候,则服务器会将这个改变发送给设置了Watch的客户端。
一个Watcher实例是一个回调函数,被回调一次就被移除了。如果还需要关注数据的变化,需要再次注册Watcher。New Zookeeper()时注册的watcher叫Default Watcher
,事件类型为 EventType.None
,它不是一次性的,只对client连接状态变化做出反应。
可以注册watcher的方法:getData
、exists
、getChildren
可以触发watcher的方法:create
、delete
、setData
,连接断开的情况下触发的watcher会丢失。
event For “/path” | event For “/path/child” | |
---|---|---|
create("/path") | EventType.NodeCreated | 无 |
delete("/path") | EventType.NodeDeleted | 无 |
setData("/path") | EventType.NodeDataChanged | 无 |
create("/path/child") | EventType.NodeChildrenChanged(getChild) | EventType.NodeCreated |
delete("/path/child") | EventType.NodeChildrenChanged(getChild) | EventType.NodeDeleted |
setData("/path/child") | 无 | EventType.NodeDataChanged |
ZooKeeper客户端框架
为了作比较,我们先来演示下原生客户端的使用
原生客户端
public class ZookeeperClientTest {
public static void main(String[] args) throws IOException, KeeperException, InterruptedException {
// zk连接
ZooKeeper client = new ZooKeeper("localhost:2181", 5000, new Watcher() {
@Override
public void process(WatchedEvent event) {
System.out.println("Zookeeper连接: " + event);
}
});
// 创建节点
client.create("/data", "1".getBytes(), Collections.singletonList(new ACL(ZooDefs.Perms.ALL, new Id("world", "anyone"))),
CreateMode.PERSISTENT);
// 获取节点数据,并监控数据改变
// 传入stat用户获取节点完整信息,比如 cZxid,ctime,cversion等
Stat stat = new Stat();
client.getData("/data", new Watcher() {
@Override
public void process(WatchedEvent event) {
if (Event.EventType.NodeDataChanged.equals(event.getType())) {
System.out.println("数据改变");
}
}
}, stat);
// 修改节点 需要指定修改的的节点版本
client.setData("/data", "4".getBytes(), -1);
// 使用回调函数,用于异步方式获取结果数据
// client.getData("/data", false, new AsyncCallback.DataCallback() {
// @Override
// public void processResult(int rc, String path, Object ctx, byte[] data, Stat stat) {
// System.out.println("回调函数" + new String(data));
// }
// }, null);
// client.delete("/data", -1);
// 让进程等待,不会立刻退出
System.in.read();
}
}
这里只演示基础操作,其它操作请读者自行尝试,API清晰可读。
原生客户端存在的问题:
- watcher是一次性的
watch事件是一个一次性触发器,想继续使用的话需要编写额外代码,重新注册。 - session超时问题
客户端在连接服务端时会设置一个sessionTimeout,用来标识session过期时间,客户端会定时向服务端发送心跳以刷新服务端的session过期时间。一旦客户端和服务端之间断开连接,session就会进行倒计时,超过了过期时间就会发送Session过期,即使客户端后面又重新连接上服务端,也只能接受session过期的事件,从而删除临时节点和watcher等等。 - API接口,节点数据需要转换为二进制数据保存。
zkclient客户端
zkclient是github上的开源项目
源码地址
项目中需要引入MAVEN依赖
<dependency>
<groupId>com.101tec</groupId>
<artifactId>zkclient</artifactId>
<version>0.11</version>
</dependency>
zkClient对于原生客户端存在的几个问题,都提供了自己的解决方案:
- 解决了watcher的一次性注册问题,将znode的事件重新定义为子节点的变化、数据的变化、连接及状态的变化这三类。由zkClient统一将watcher的WatchedEvent转换到以上三种情况去处理。watcher执行后重新读取数据的同时,将再注册相同的watcher。
- zkClient在发生session expire异常时会自动创建新的ZooKeeper实例重连。这时所有的watcher和EPHEMERAL节点都将失效,可以在zkClient定义的连接状态变化的接口 IzkStateListener里面的handleNewSession方法中进行相应的处理。
- zkClient提供了ZKSerializer接口,可进行序列化和反序列化操作。
public class ZkClientTest {
public static void main(String[] args) throws IOException {
// 连接ZK
ZkClient zk = new ZkClient("10.40.231.95:2181", 5000, 5000, new SerializableSerializer());
// 创建节点
zk.createPersistent("/zkclientdata", "1");
// 创建子节点
zk.create("/zkclientdata/children", "1", CreateMode.PERSISTENT);
// 订阅数据变化
zk.subscribeDataChanges("/zkclientdata", new IZkDataListener() {
@Override
public void handleDataChange(String s, Object o) throws Exception {
System.out.println("数据被修改了");
}
@Override
public void handleDataDeleted(String s) throws Exception {
System.out.println("数据被删除了");
}
});
// 订阅子节点变化
zk.subscribeChildChanges("/zkclientdata", new IZkChildListener() {
@Override
public void handleChildChange(String s, List<String> list) throws Exception {
System.out.println("子节点数据变化了");
}
});
// 订阅状态变化
zk.subscribeStateChanges(new IZkStateListener() {
@Override
public void handleStateChanged(Watcher.Event.KeeperState keeperState) throws Exception {
System.out.println("状态变化");
}
@Override
public void handleNewSession() throws Exception {
// session失效,重建session时触发
// 可以在这里恢复数据,比如session失效后导致的临时节点被删除
System.out.println("session重建");
zk.create("/zkclientdata-ls", "4", CreateMode.EPHEMERAL);
}
});
@Override
public void handleSessionEstablishmentError(Throwable throwable) throws Exception {
}
// 修改节点
zk.writeData("/zkclientdata", "2");
// 读取节点
String data = zk.readData("/zkclientdata");
System.out.println(data);
// 获取子节点列表
List<String> childList = zk.getChildren("/zkclientdata");
// 统计子节点个数
System.out.println(zk.countChildren("/zkclientdata"));
// 删除节点
// zk.delete("/zkclientdata");
System.in.read();
}
}
这里只演示基础操作,其它操作请读者自行尝试,API清晰可读。
在zkclient中,节点有三种状态可供订阅,分别用于监控数据的变化、子节点的变化、zk状态的变化:
- org.I0Itec.zkclient.IZkDataListener
定义了两种事件,一种是节点数据的变化,另一种是节点被删除。 - org.I0Itec.zkclient.IZkChildListener
定义了一种事件,用于监控子节点的变化,这时候获取到的是新的子节点列表。如果此节点被删除,那么获取到的值为null。 - org.I0Itec.zkclient.IZkStateListener
定义了三种事件,一种是连接状态的改变,一种是创建一个新的session, 通常是由于session失效后新的session被建立时触发,此时可以恢复数据,比如重新创建临时节点;最后一种是session重建失败。
zkclient强大之处就在于,当发送session过期时能够自动重新订阅这些事件,而不需要开发者重新订阅。