Luckylau's Blog

Zookeeper的原理(2)

​ 本章开始讲解Zookeeper重要的技术实现。在分布式系统基础理论(3)中,我们讲到了Zookeeper是采用ZAB协议保持一致性的。

系统模型

树形结构

​ Zookeeper的数据节点称为ZNode,ZNode是Zookeeper中数据的最小单元,每个ZNode都可以保存数据,同时还可以挂载子节点,因此构成了一个层次化的命名空间,称为树。ZNode的节点路径标识方式和Unix文件系统路径非常相似,都是由一系列使用斜杠(/)进行分割的路径表示。

​ 在Zookeeper中,事务是指能够改变Zookeeper服务器状态的操作,一般包括节点创建与删除,数据节点内容更新和客户端会话创建与失效,对于每个事务请求,Zookeeper都会为其分配一个全局唯一的事务ID,用ZXID表示,通常是64位的数字,每个ZXID对应一次更新操作,从这些ZXID中可以间接地识别出Zookeeper处理这些更新操作请求的全局顺序。

节点特性

​ 在Zookeeper中,每个数据节点都是由生命周期的,类型不同则会不同的生命周期,节点类型可以分为持久节点(PERSISTENT)、临时节点(EPHEMERAL)、顺序节点(SEQUENTIAL)三大类,可以通过组合生成如下四种类型节点:

持久节点(PERSISTENT):节点创建后便一直存在于Zookeeper服务器上,直到有删除操作来主动清楚该节点。

持久顺序节点(PERSISTENT_SEQUENTIAL):相比持久节点,其新增了顺序特性,每个父节点都会为它的第一级子节点维护一份顺序,用于记录每个子节点创建的先后顺序。在创建节点时,会自动添加一个数字后缀,作为新的节点名,该数字后缀的上限是整形的最大值。

临时节点(EPEMERAL)。临时节点的生命周期与客户端会话绑定,客户端失效,节点会被自动清理。同时,Zookeeper规定不能基于临时节点来创建子节点,即临时节点只能作为叶子节点。

临时顺序节点(EPEMERAL_SEQUENTIAL)。在临时节点的基础添加了顺序特性。

另外就是查看节点的状态,用get操作

1
2
3
4
5
6
7
8
9
10
11
12
13
[zk: localhost:2181(CONNECTED) 5] get /zk-book
123
cZxid = 0x3
ctime = Wed Mar 14 16:04:56 CST 2018
mZxid = 0x3
mtime = Wed Mar 14 16:04:56 CST 2018
pZxid = 0x3
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 3
numChildren = 0

版本

版本是用来保证分布式数据原子性操作,每个数据节点都具有三种类型的版本信息,对数据节点的任何更新操作都会引起版本号的变化。

version– 当前数据节点数据内容的版本号

cversion– 当前数据子节点的版本号

aversion– 当前数据节点ACL变更版本号

Watcher

watcher用于数据变更通知,可以用于订阅和通知功能。

​ Zookeeper的Watcher机制主要包括客户端线程、客户端WatcherManager、Zookeeper服务器三部分。客户端在向Zookeeper服务器注册的同时,会将Watcher对象存储在客户端的WatcherManager当中。当Zookeeper服务器触发Watcher事件后,会向客户端发送通知,客户端线程从WatcherManager中取出对应的Watcher对象来执行回调逻辑。

Watcher接口

​ Watcher接口类用于表示一个标准的事件处理器,定义事件通知的相关逻辑,包含KeeperState和EventType两个枚举,分别代表通知状态和事件类型,同时定义事件的回调方法:process(WatchedEvent event)方法。其中WatchedEvent 包含通知状态(keeperState),事件类型(eventType)和和节点路径(path)。要注意与WatcherEvent区别,它们本身都是对服务端事件的封装,只是WatcherEvent实现序列化用于网络传输,到达客户端后又会还原成为WatchedEvent 。

工作机制

客户端注册watcher、服务端处理watcher和客户端回调watcher事件。

客户端注册Watcher

客户端注册watcher的方式有很多,因为Watcher是一次性的,可以通过创建一个 ZooKeeper 客户端对象实例,传入Watcher;可以通过 getData、exists 和 getChildren 三个接口来向 ZooKeeper 服务器注册 Watcher等。

应用程序客户端调用Zookeeper的API例如getData操作,实际是构建了ClientCnxn 类,在这个类里面新建了2个线程SendThread和EventThread。SendThread 使用的nio操作,负责将ZooKeeper的请求信息封装成一个Packet,发送给 Server ,并维持同Server的心跳;EventThread循环处理watch事件的线程。

SendThread将请求转化为Packet包,放入OutgoingQueue, 然后在run中循坏处理,在这个过程中,如果OutgoingQueue里面有数据需要发送,则发送数据包并把数据包从Outgoing Queue移至Pending Queue,意思是数据我已经发出去了,但还要等待Server端的回复,所以这个请求现在是Pending 的状态。当Server端响应的时候,SendThread通过readResonse进行处理,如果是之前请求的响应,Pending Queue里面一定会有,这时候会从Pending Queue移除, 又通过finishPacket方法从packet中提取Watcher存到ZKWatchManger的dataWatches。

EventThread用来处理Finish Event和Watcher Event。收到Finish Event的时候会把相对应的Package置成Finish状态,这样等待结果的Client函数就能得以返回。收到Watcher Event的时候会联系WatcherManager找到相对应的Watcher,从WatcherManager里面移除这个Watcher(因为每个Watcher只会被通知一次) 并回调Watcher的process函数。所以所有Watcher的process函数是运行在EventThread里面的。

服务端处理Watcher

服务端收到客户端的请求时候,首先判断是否需要注册Watcher。当需要注册的时候,就将ServerCnxn对象和数据节点路径传入getData方法中去。ServerCnxn是一个Zookeeper客户端和服务器之间的连接接口,代表了一个客户端和服务器的连接,默认实现是NIOServerCnxn,最终会被存储在WatchManager的watchTable和watch2Paths中。watchTable是从数据节点路径的粒度来托管Watcher。watch2Paths是从Watcher的粒度来控制事件触发需要触发的数据节点路径。

客户端回调Watcher

WatchManager除了保存了Watcher,同时还负责Watcher事件的触发,并移除那些已经触发的Watcher。WatchManager只是一个统称,在服务端,DataTree中会托管2个叫做dataWatches和childWatches的WatchManager,分别用于对应数据变更Watcher和子节点变更Watcher。上述的getData接口会被存储在叫dataWatches的WatchManager中。

当数据出现变化,(创建、删除、设置节点数据)时,会触发watch:

1
public Set<Watcher> triggerWatch(String path, EventType type, Set<Watcher> supress)

封装WatchedEvent: 首先将通知状态(KeeperState)、事件类型(EventType)以及节点路径(Path)封装成一个WatchedEvent对象。

查询Watcher:根据数据节点的节点路径从watchTable中取出对应的Watcher。如果没有找到Watcher,说明没有任何客户端在该数据节点上注册过Watcher,直接退出。而如果找到了这个Watcher,会将其提取出来,同时会直接从watchTable和watch2Paths中将其删除——从这里我们也可以看出,Watcher在服务器是一次性的,及触发一次就失效了。

调用process方法来触发Watcher:会逐个依次的调用从步骤2中找出的所有Watcher的process方法:在请求头中标记“-1”,表明当前是一个通知;将WatchedEvent包装成WatcherEvent,以便进行网络传输序列化;向客户端发送该通知。

​ 客户端收到服务端响应后,对于一个来自服务端的响应,客户端都是由SendThread.readResponse(ByteBuffer incomingBuffer)方法来统一进行处理的,如果响应头replyHdr中标识了XID为-1,表明这是一个通知类型的响应,对其的处理大体上分为以下4个主要步骤:反序列化:将字节流转换成WatcherEvent对象;处理chrootPath:如果客户端设置了chrootPath属性,那么需要对服务端传过来的完整的节点路径进行chrootPath处理,生成客户端的一个相对节点路径;还原WatchedEvent:将WatcherEvent对象转换成WatchedEvent;回调Watcher:客户端识别出是EventType后,会会从相应的Watcher存储(即dataWatches、existWatches或childWatches中的一个或多个,本例中就是从dataWatches和existWatches两个存储中获取)中去除相应的Watcher。注意,此处使用的是remove接口,因此也表明了客户端的Watcher机制同样也是一次性的,即一旦被触发后,该Watcher就失效了。获取到相关的所有Watcher之后,会将其放入waitingEvents这个队列中去。WaitingEvents是一个待处理Watcher的队列,EventThread的run方法会不断对该队列进行处理,在下一个轮询周期中进行Watcher回调。

Watcher的特性

一次性
无论是服务端还是客户端,一旦一个 Watcher 被触发,ZooKeeper 都会将其从相应的存储中移除。因此,在 Watcher 的使用上,需要反复注册。这样的设计有效地减轻了服务端的压力。

客户端串行执行
客户端 Watcher 回调的过程是一个串行同步的过程,这为我们保证了顺序,同时,需要注意的一点是,一定不能因为一个 Watcher 的处理逻辑影响了整个客户端的 Watcher 回调,所以,客户端 Watcher 的实现类要另开一个线程进行处理业务逻辑,以便给其他的 Watcher 调用让出时间。

轻量
WatcherEvent 是 ZooKeeper 整个 Watcher 通知机制的最小通知单元,这个数据结构中只包含三部分内容:通知状态、事件类型和节点路径。也就是说,Watcher 通知非常简单,只会告诉客户端发生了事件,而不会说明事件的具体内容。例如针对 NodeDataChanged 事件,ZooKeeper 的Watcher 只会通知客户端指定数据节点的数据内容发生了变更,而对于原始数据以及变更后的新数据都无法从这个事件中直接获取到,而是需要客户端主动重新去获取数据——这也是 ZooKeeper 的 Watcher 机制的一个非常重要的特性。

ACL

Zookeeper提供一套ACL权限控制机制来保证数据的安全。分别是:权限模式(Scheme)、授权对象(ID)和权限 (Permission),通常使用 “scheme:id :permission”来标识一个有效的 ACL信息。

权限包括: CREATE (C):数据节点的创建权限,允许授权对象在该数据节点下创建子节点;DELETE (D):子节点的刪除权限,允许授权对象刪除该数椐节点的子节点;READ (K):数据节点的读取权限,允许授权对象访问该数据节点并读取其数据内容或子节点列表;WRITE (W):数据节点的更新权限,允许授权对象对该数据节点进行更新操作;ADMIN (A):数椐节点的管理权限,允许授权对象对该数据节点进行ACL相关的设置操作。

参考

http://blog.csdn.net/abountwinter/article/details/55188783

http://blog.csdn.net/yinwenjie/article/details/47685077

Luckylau wechat
如果对您有价值,看官可以打赏的!