1. Zookeeper技术内幕
1.1. 系统模型
1.1.1. 数据模型
树
zookeeper名字空间由节点znode构成,其组织方式类似文件系统,其中各个节点相当于目录和文件,通过路径作为唯一标识。
事务ID
在传统数据库中事务具有所谓的ACID特性:即原子性、一致性、隔离性、和持久性。
在Zookeeper中,事务是指能够改变Zookeeper服务器状态的操作,称为事务操作或者更新操作。一般包括数据节点的创建与删除,数据节点内容更新,和客户端会话创建与失效等操作。对于每一个事务的请求,Zookeeper都会为其分配一个全局唯一的事务ID(ZXID),通常是一个64位的数字。每一个ZXID对应一次更新操作,Zookeeper根据这些全局唯一的ZXID请求来处理更新请求的全局顺序。
1.1.2. 节点特性
节点类型
节点类型可以分为持久节点(PERSISTENT )、临时节点(EPHEMERAL),以及时序节点(SEQUENTIAL ),具体在节点创建过程中,一般是组合使用,可以生成以下 4 种节点类型。
持久节点(PERSISTENT)
所谓持久节点,是指在节点创建后,就一直存在,直到有删除操作来主动清除这个节点——不会因为创建该节点的客户端会话失效而消失。
持久顺序节点(PERSISTENT_SEQUENTIAL)
在ZK中,每个父节点会为他的第一级子节点维护一份时序,会记录每个子节点创建的先后顺序。基于这个特性,在创建子节点的时候,可以设置这个属性,那么在创建节点过程中,ZK会自动为给定节点名加上一个数字后缀,作为新的节点名。这个数字后缀的范围是整型的最大值。
临时节点(EPHEMERAL)
和持久节点不同的是,临时节点的生命周期和客户端会话绑定。也就是说,如果客户端会话失效,那么这个节点就会自动被清除掉。注意,这里提到的是会话失效,而非连接断开。另外,在临时节点下面不能创建子节点,注意是根据Session会话的失效时间来设定的。
临时顺序节点(EPHEMERAL_SEQUENTIAL)
临时顺序节点的特性和临时节点一致,同时是在临时节点的基础上,添加了顺序的特性。
状态信息
在Zookeeper维护数据节点的同时,每个节点除了存储数据内容之外,还存储了数据节点本身一些状态信息,
[zk: 127.0.0.1:2181(CONNECTED) 1] get /zookeeper/test
/zookeeper/test
cZxid = 0x600000014
ctime = Sat Nov 05 16:56:53 CST 2016
mZxid = 0x600000014
mtime = Sat Nov 05 16:56:53 CST 2016
pZxid = 0x600000014
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 15
numChildren = 0
状态信息解释:
czxid:节点创建时的事物ID
mzxid:节点最新一次更新时的事物ID
ctime:节点创建时的时间戳
mtime:节点最新一次更新发生时的时间戳
pZxid:该节点的子节点列表最后一次被修改时的事物ID,如果没有子节点,则为当前节点的cZxid
cversion:其子节点的更新次数
dataVersion:节点数据的更新次数
aclVersion:节点ACL(授权信息)的更新次数
ephemeralOwner:创建临时节点的会话的sessionID. 如果该节点是持久节点,该值为0
dataLength:节点数据的字节数
numChildren:当前节点子节点个数
1.1.3. 版本
每个数据节点都具有三种类型的版本信息, 对数据节点的任何更新操作都会引起版本号的变化。
版本类型 | 说明 |
dataVersion | 当前数据节点数据内容的版本号 |
cversion | 当前数据节点子节点的版本号 |
aversion | 当前数据节点ACL变更版本号 |
dataVersion是表示对数据节点数据内容的变更次数,强调的是变更次数,因此就算数据内容的值没有发生变化,version的值也会递增。
版本用来干什么?
乐观锁控制的事务分成如下三个阶段:数据读取、写入校验和数裾写入。在写入校验阶段.事务会检查数据在读取阶段后是否有其他事物对数据进行过更新,以确保数裾更新的一致性,那么,如何来进行写人校验呢?我们首先可以来看JDK中最典型的乐观锁实现——CAS。简单地讲就是“对于值V,毎次更新前都会比对其值是否是预期值A,只有符合预期,才会将V原子化地更 新到新值B“,其中是否符合预期便是乐观锁中的“写入校验”阶段。
在ZooKeeper中, version属性正是用来实现乐观锁机制中的“写人校验”的。在这里我们看看ZooKeeper的内部实现,在ZooKeeper服务器的PrepRequestProcessor处理器类中,在处理毎一个数椐更新<setDataRequest>请求时,会进行如下所示的版本检查。
private static int checkAndIncVersion(int currentVersion, int expectedVersion, String path)
throws KeeperException.BadVersionException {
if (expectedVersion != -1 && expectedVersion != currentVersion) {
throw new KeeperException.BadVersionException(path);
}
return currentVersion + 1;
}
在进行一次修改数据的请求处理时,首先进行了版本检查:Zookeeper会从请求中获取到当前请求的版本expectedVersion,同时从内存的数据记录中获取到当前服务器上该数据的最新版本currentVersion,如果expectedVersion为“-1”,那么说明客户端并不要求使用乐观锁,可以忽略版本比对,如果 version不是“-1”,那么就比对expectedVersion和currentVersion,如果两个版本不匹配,那么将会抛出BadVersionException异常。
1.1.4. Watcher机制
Zookeeper的Watcher机制主要包括客户端线程、客户端WatchManager和Zookeeper服务器三部分。
在具体的流程上,客户端向Zookeeper服务器注册Watcher事件监听的同时,会将Watcher对象存储在 客户端WatchManager中。当Zookeeper服务器触发Watcher事件后,会向客户端发送通知,客户端线程从WatchManager中取出对应的Watcher对象执行回调逻辑。
Watcher接口
Watcher接口类用于表示一个标准的事件处理器,定义事件通知的相关逻辑,包含KeeperState和EventType两个枚举,分别代表通知状态和事件类型,同时定义事件的回调方法:process方法。
public interface Watcher {
public interface Event {
public enum KeeperState {
Unknown (-1),
NoSyncConnected (1),
SyncConnected (3),
AuthFailed (4),
ConnectedReadOnly (5),
SaslAuthenticated(6),
Expired (-112);
private final int intValue;
KeeperState(int intValue) {
this.intValue = intValue;
}
public int getIntValue() {
return intValue;
}
}
public enum EventType {
None (-1),
NodeCreated (1),
NodeDeleted (2),
NodeDataChanged (3),
NodeChildrenChanged (4),
DataWatchRemoved (5),
ChildWatchRemoved (6);
private final int intValue; // Integer representation of value
EventType(int intValue) {
this.intValue = intValue;
}
public int getIntValue() {
return intValue;
}
}
}
abstract public void process(WatchedEvent event);
}
Watcher事件
同一个事件类型在不同的通知状态中代表的含义有所不同。如下常见的通知状态和事件类型。
回调方法process()
process方法是 Watcher 接口中的一个回调方法,当 ZooKeeper 向客户端发送一个 Watcher 事件通知时,客户端就会对相应的 process 方法进行回调,从而实现对事件的处理。
process 方法包含 WatcherEvent 类型的参数,WatcherEvent包含每一事件的三种基本属性:通知状态、事件类型、节点路径。Zookeeper使用Watcher对象来封装服务器端事件,并传递给Watcher,从而方便回调方法process对服务器事件进行处理。
public class WatchedEvent {
final private KeeperState keeperState;
final private EventType eventType;
private String path;
}
注:ZookeeperWatcher的一个重要特性:客户端无法直接从WatchedEvent事件中获取到对应数据节点的原始数据内容,以及变更后的数据内容,而是客户端再次主动去重新获取数据。
事件注册
事件响应
一次性Watcher几个特性
无论是服务端还是客户端,一旦一个 Watcher 被触发,ZooKeeper 都会将其从相应的存储中移除。因此,开发人员在 Watcher 的使用上要记住的一点是需要反复注册。这样的设计有效地减轻了服务端的压力。如果注册一个 Watcher 之后一直有效,那么针对那些更新非常频繁的节点,服务端会不断地向客户端发送事件通知,这无论对于网络还是服务端性能的影响都非常大。
客户端串行执行
客户端Watcher 回调的过程是一个串行同步的过程,这为我们保证了顺序,同时,需要开发人员注意的一点是,千万不要因为一个 Watcher 的处理逻辑影响了整个客户端的 Watcher 回调。
轻量
WatchedEvent 是 ZooKeeper 整个 Watcher 通知机制的最小通知单元,这个数据结构中只包含三部分的内容:通知状态、事件类型和节点路径。也就是说,Watcher 通知非常简单,只会告诉客户端发生了事件,而不会说明事件的具体内容。另外,客户端向服务端注册 Watcher 的时候,并不会把客户端真实的 Watcher 对象传递到服务端,仅仅只是在客户端请求中使用 boolean 类型属性进行了标记,同时服务端也仅仅只是保存了当前连接的ServerCnxn 对象。这样轻量级的 Watcher 机制设计,在网络开销和服务端内存开销上都是非常廉价的。
1.1.5. ACL权限控制
Zookeeper提供一套ACL权限控制机制来保证数据的安全。
从三个方面来理解ACL机制,分别是:权限模式(Scheme)、授权对象(ID)和权限 (Permission),通常使用 “scheme:id:permission”来标识一个有效的 ACL信息。
权限模式:Scheme
权限模式用来确定权限验证过程中使用的检验策略。在ZooKeeper中,开发人员使用最多的就是以下四种权限模式。
IP
通过IP地址粒度来进行权限控制,例如配置了“ip:192.168.0.110”,即表示权限控制都针对这IP地址的。同时IP模式也支持按照网段的方式进行配置, 例如“ip:192.168.0.1/24”表示针对192.168.0.*这个网段进行权限控制。
Digest
Digest是最常用的权限控制模式,也史符合我们对于权限控制的认识,其以类似于“username:password”形式的权限标识来进行权限配置,便于区分不同应用来进行权限控制。
World
World是一种最开放的权限控制模式,从其名字中也以看出,事实上这种权限控制方式几乎没有任何作用,数据节点的访问权限对所存用户开放,即所有用户都可以在不进行任何权限校验的情况下操作ZooKeeper的数据。另外,World模式也可以看作一种特殊的Digest模式,它只有一个权限标识,即“world:anyone”。
Super
Super模式,顾名思义就是超级用户的意思,也是—种特殊的Digest模式。在Super模式下,超级用户可以对任意ZooKeeper上的数据节点进行任何操作。
授权对象:ID
权限赋予的用户或一个指定的实体,例如IP地址或是机器等。在不同的权限模式下,授权对象是不同的。
权限模式 | 授权对象 |
IP | 通常是一个IP地址或是IP段,例如“192.168.0.123”或“192.168.0.1/24” |
Digest | 自定义,通常是 username:BASE64(SHA-l(usemame:password))”, 例如”foo: kWN6aNSbjcKWPqjiV7cgON24raU=” |
World | 只有一个ID:“anyone” |
Super | 与Digest模式一致 |
权限
权限指的是那些通过权限检查后可以被允许执行的操作。数据权限分为五大类:
CREATE (C):数据节点的创建权限,允许授权对象在该数据节点下创建子节点。
DELETE (D):子节点的刪除权限,允许授权对象刪除该数椐节点的子节点。
READ (K):数据节点的读取权限,允许授权对象访问该数据节点并读取其数据内容或子节点列表等。
WRITE (W):数据节点的更新权限,允许授权对象对该数据节点进行更新操作。
ADMIN (A):数椐节点的管理权限,允许授权对象对该数据节点进行ACL相关的设置操作,
权限扩展
启动Zookeeper的时候,通过-Dzookeeper.authProvider.$n=$classname的方式,$n只要保证不重复即可,没有实际的意义,通常用数字1,2,3等
ACL管理
略过
数据结构
DataTree中的数据节点
DataNode{
byte data[];
Long acl;
public StatPersisted stat;
private Set<String> children = null;
}
DataTree{
Map<Long, List<ACL>> longKeyMap;
ConcurrentHashMap<String, DataNode> nodes
}
例子
public class ZookeeperTest {
public static void main(String[] args) {
try {
String lockPath = "/zookeeper/test5";
ZookeeperTest test = new ZookeeperTest();
test.create(lockPath);
test.setData(lockPath);
} catch (Exception e) {
e.printStackTrace();
}
}
private void setData(String lockPath) throws IOException, KeeperException, InterruptedException {
ZooKeeper zk1 = new ZooKeeper("192.168.99.171:2181", 100000, new Watcher() {
// 监控所有被触发的事件
public void process(WatchedEvent event) {
System.out.println("已经触发了" + event.getType() + "事件!事件名字:" + event.getState().name());
}
});
zk1.addAuthInfo("digest", "lihyRead:passRead".getBytes());
System.out.println("修改前,读取数据:"+new String(zk1.getData(lockPath,false,null)));
zk1.addAuthInfo("digest", "lihyWrite:passWrite".getBytes());
zk1.setData(lockPath,"createContent_1".getBytes(),-1);
System.out.println("修改后,读取数据:"+new String(zk1.getData(lockPath,false,null)));
}
private void create(String path) throws IOException, KeeperException, InterruptedException, NoSuchAlgorithmException {
ZooKeeper zk = new ZooKeeper("192.168.99.171:2181", 100000, new Watcher() {
// 监控所有被触发的事件
public void process(WatchedEvent event) {
System.out.println("已经触发了" + event.getType() + "事件!事件名字:" + event.getState().name());
}
});
ArrayList<ACL> acls = new ArrayList<ACL>();
ACL writeAcl = new ACL(ZooDefs.Perms.WRITE,new Id("digest",DigestAuthenticationProvider.generateDigest("lihyWrite:passWrite")));
ACL readAcl = new ACL(ZooDefs.Perms.READ,new Id("digest",DigestAuthenticationProvider.generateDigest("lihyRead:passRead")));
acls.add(writeAcl);
acls.add(readAcl);
String seq = zk.create(path, "createContent".getBytes(), acls, CreateMode.PERSISTENT);
}
}