从系统模型、序列化与协议、客户端工作原理、会话、服务端工作原理以及数据存储等方面来向揭示ZooKeeper的技术内幕,更深入地了解ZooKeeper这一分布式协调框架。
将从数据模型、节点特性、版本、Watcher和ACL五方面来讲述ZooKeeper的系统模型。
ZooKeeper的视图结构和标准的Unix文件系统非常类似,但没有引入传统文件系统中目录和文件等相关概念,而是使用了其特有的“数据节点”概念,我们称之为ZNode。ZNode是ZooKeeper中数据的最小单元,每个ZNode上都可以保存数据,同时还可以挂载子节点,因此构成了一个层次化的命名空间,我们称之为树。
树
首先我们来看下图所示的ZooKeeper数据节点示意图,从而对ZooKeeper上的数据节点有一个大体上的认识。在ZooKeeper中,每一个数据节点都被称为一个ZNode,所有ZNode按层次化结构进行组织,形成一棵树。ZNode的节点路径标识方式和Unix文件系统路径非常相似,都是由一系列使用斜杠(/)进行分割的路径表示,开发人员可以向这个节点中写入数据,也可以在节点下面创建子节点。
事务ID
在《事务处理:概念与技术》一书中提到,事务是对物理和抽象的应用状态上的操作集合。在现在的计算机科学中,狭义上的事务通常指的是数据库事务,一般包含了一系列对数据库有序的读写操作,这些数据库事务具有所谓的ACID特性,即原子性(Atomic)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability)。
在ZooKeeper中,事务是指能够改变ZooKeeper服务器状态的操作,我们也称之为事务操作或更新操作,一般包括数据节点创建与删除、数据节点内容更新和客户端会话创建与失效等操作。对于每一个事务请求,ZooKeeper都会为其分配一个全局唯一的事务ID,用ZXID来表示,通常是一个64位的数字。每一个ZXID对应一次更新操作,从这些ZXID中可以间接地识别出ZooKeeper处理这些更新操作请求的全局顺序。
ZooKeeper的命名空间是由一系列数据节点组成的,在本节中,将对数据节点做详细讲解。
在ZooKeeper中,每个数据节点都是有生命周期的,其生命周期的长短取决于数据节点的节点类型。在ZooKeeper中,节点类型可以分为持久节点(PERSISTENT)、临时节点(EPHEMERAL)和顺序节点(SEQUENTIAL)三大类,具体在节点创建过程中,通过组合使用,可以生成以下四种组合型节点类型:
持久节点(PERSISTENT)
持久节点是ZooKeeper中最常见的一种节点类型。所谓持久节点,是指该数据节点被创建后,就会一直存在于ZooKeeper服务器上,直到有删除操作来主动清除这个节点。
持久顺序节点(PERSISTENT_SEQUENTIAL)
持久顺序节点的基本特性和持久节点是一致的,额外的特性表现在顺序性上。在ZooKeeper中,每个父节点都会为它的第一级子节点维护一份顺序,用于记录下每个子节点创建的先后顺序。基于这个顺序特性,在创建子节点的时候,可以设置这个标记,那么在创建节点过程中,ZooKeeper会自动为给定节点名加上一个数字后缀,作为一个新的、完整的节点名。另外需要注意的是,这个数字后缀的上限是整型的最大值。
临时节点(EPHEMERAL)
和持久节点不同的是,临时节点的生命周期和客户端的会话绑定在一起,也就是说,如果客户端会话失效,那么这个节点就会被自动清理掉。注意,这里提到的是客户端会话失效,而非TCP连接断开。另外,ZooKeeper规定了不能基于临时节点来创建子节点,即临时节点只能作为叶子节点。
临时顺序节点(EPHEMERAL_SEQUENTIAL)
临时顺序节点的基本特性和临时节点也是一致的,同样是在临时节点的基础上,添加了顺序的特性。
状态信息
可以针对ZooKeeper上的数据节点进行数据的写入和子节点的创建。事实上,每个数据节点除了存储了数据内容之外,还存储了数据节点本身的一些状态信息。
ZooKeeper中为数据节点引入了版本的概念,每个数据节点都具有三种类型的版本信息,对数据节点的任何更新操作都会引起版本号的变化,下表中对这三类版本信息分别进行了说明。
ZooKeeper中的版本概念和传统意义上的软件版本有很大的区别,它表示的是对数据节点的数据内容、子节点列表,或是节点ACL信息的修改次数,我们以其中的version这种版本类型为例来说明。在一个数据节点/zk-book被创建完毕之后,节点的version值是0,表示的含义是“当前节点自从创建之后,被更新过0次”。如果现在对该节点的数据内容进行更新操作,那么随后,version的值就会变成1。同时需要注意的是,在上文中提到的关于version的说明,其表示的是对数据节点数据内容的变更次数,强调的是变更次数,因此即使前后两次变更并没有使得数据内容的值发生变化,version的值依然会变更。
在上面的介绍中,我们基本了解了ZooKeeper中的版本概念。那么版本究竟用来干嘛呢?在讲解版本的作用之前,我们首先来看下分布式领域中最常见的一个概念——锁。
一个多线程应用,尤其是分布式系统,在运行过程中往往需要保证数据访问的排他性。
悲观锁,又被称作悲观并发控制(Pessimistic Concurrency Control,PCC),是数据库中一种非常典型且非常严格的并发控制策略。悲观锁具有强烈的独占和排他特性,能够有效地避免不同事务对同一数据并发更新而造成的数据一致性问题。在悲观锁的实现原理中,如果一个事务(假定事务A)正在对数据进行处理,那么在整个处理过程中,都会将数据处于锁定状态,在这期间,其他事务将无法对这个数据进行更新操作,直到事务A完成对该数据的处理,释放了对应的锁之后,其他事务才能够重新竞争来对数据进行更新操作。也就是说,对于一份独立的数据,系统只分配了一把唯一的钥匙,谁获得了这把钥匙,谁就有权力更新这份数据。一般我们认为,在实际生产应用中,悲观锁策略适合解决那些对于数据更新竞争十分激烈的场景——在这类场景中,通常采用简单粗暴的悲观锁机制来解决并发控制问题。
乐观锁,又被称作乐观并发控制(Optimistic Concurrency Control,OCC),也是一种常见的并发控制策略。相对于悲观锁而言,乐观锁机制显得更加宽松与友好。从上面对悲观锁的讲解中我们可以看到,悲观锁假定不同事务之间的处理一定会出现互相干扰,从而需要在一个事务从头到尾的过程中都对数据进行加锁处理。而乐观锁则正好相反,它假定多个事务在处理过程中不会彼此影响,因此在事务处理的绝大部分时间里不需要进行加锁处理。当然,既然有并发,就一定会存在数据更新冲突的可能。在乐观锁机制中,在更新请求提交之前,每个事务都会首先检查当前事务读取数据后,是否有其他事务对该数据进行了修改。如果其他事务有更新的话,那么正在提交的事务就需要回滚。乐观锁通常适合使用在数据并发竞争不大、事务冲突较少的应用场景中。
从上面的讲解中,我们其实可以把一个乐观锁控制的事务分成如下三个阶段:数据读取、写入校验和数据写入,其中写入校验阶段是整个乐观锁控制的关键所在。在写入校验阶段,事务会检查数据在读取阶段后是否有其他事务对数据进行过更新,以确保数据更新的一致性。那么,如何来进行写入校验呢?我们首先可以来看下JDK中最典型的乐观锁实现——CAS。简单地讲就是“对于值V,每次更新前都会比对其值是否是预期值A,只有符合预期,才会将V原子化地更新到新值B”,其中是否符合预期便是乐观锁中的“写入校验”阶段。
好了,现在我们再回过头来看看ZooKeeper中版本的作用。事实上,在ZooKeeper中,version属性正是用来实现乐观锁机制中的“写入校验”的。在ZooKeeper服务器的PrepRequestProcessor处理器类中,在处理每一个数据更新(setDataRequest)请求时,会进行如下清单所示的版本检查。
从上面的执行逻辑中,我们可以看出,在进行一次setDataRequest请求处理时,首先进行了版本检查:ZooKeeper会从setDataRequest请求中获取到当前请求的版本version,同时从数据记录nodeRecord中获取到当前服务器上该数据的最新版本currentVersion。如果version为“-1”,那么说明客户端并不要求使用乐观锁,可以忽略版本比对;如果version不是“-1”,那么就比对version和currentVersion,如果两个版本不匹配,那么将会抛出BadVersionException异常。
ZooKeeper提供了分布式数据的发布/订阅功能。一个典型的发布/订阅模型系统定义了一种一对多的订阅关系,能够让多个订阅者同时监听某一个主题对象,当这个主题对象自身状态变化时,会通知所有订阅者,使它们能够做出相应的处理。在ZooKeeper中,引入了Watcher机制来实现这种分布式的通知功能。ZooKeeper允许客户端向服务端注册一个Watcher监听,当服务端的一些指定事件触发了这个Watcher,那么就会向指定客户端发送一个事件通知来实现分布式的通知功能。整个Watcher注册与通知过程如下图所示:
从下图中,我们可以看到,ZooKeeper的Watcher机制主要包括客户端线程、客户端WatchManager和ZooKeeper服务器三部分。在具体工作流程上,简单地讲,客户端在向ZooKeeper服务器注册Watcher的同时,会将Watcher对象存储在客户端的WatchManager中。当ZooKeeper服务器端触发Watcher事件后,会向客户端发送通知,客户端线程从WatchManager中取出对应的Watcher对象来执行回调逻辑。
Watcher接口
在ZooKeeper中,接口类Watcher用于表示一个标准的事件处理器,其定义了事件通知相关的逻辑,包含KeeperState和EventType两个枚举类,分别代表了通知状态和事件类型,同时定义了事件的回调方法:process(WatchedEvent event)。