系统模型
我们首先将从数据模型、节点特性、版本、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中,节点类型可以分为持久节点(Persistent)、临时节点(Ephemeral)和顺序节点(Sequence)三大类,在具体节点创建过程中,通过组合使用,可以生成以下四种组合型节点类型:
持久节点
持久节点是Zookeeper中最常见的一种节点类型。所谓持久节点,是指该数据节点被创建后,就会一直存在于Zookeeper服务器上,直到有删除操作来主动清除这个节点。
持久顺序节点
持久顺序节点的基本特性和持久节点是一致的,额外的特性表现在顺序性上。在Zookeeper中,每个父节点都会为它的第一级子节点维护的一份顺序,用于记录下每个子节点创建的先后顺序。基于这个顺序特性,在创建子节点的时候,可以设置这个标记,那么在创建节点过程中,Zookeeper会自动为给定节点名加上一个数字后缀,作为一个新的、完整的节点名。另外需要注意的是,这个数字后缀的上限是整型的最大值。
临时节点
和持久节点不同的是,临时节点的生命周期和客户端的会话绑定在一起,也就是说,如果客户端会话失效,那么这个节点就会被自动清理掉。注意,这里提到的是客户端会话失效,而非TCP连接断开。另外,Zookeeper规定了不能基于临时节点来创建子节点,即临时节点只能作为叶子节点。
状态信息
状态属性 | 说明 |
---|---|
czxid | 即Created ZXID,表示该数据节点被创建时的事务ID |
mzxid | 即Modified ZXID,表示该节点最后一次被更新时的事务ID |
ctime | 即Created Time,表示节点被创建的时间 |
mtime | 即Modified Time,表示该节点最后一次被更新的时间 |
version | 数据节点的版本号 |
cversion | 子节点的版本号 |
aversion | 节点的ACL版本号 |
ephemeralOwner | 创建该临时节点的会话的SessionID。如果该节点是持久节点,那么这个属性值为0 |
dataLength | 数据内容的长度 |
numChildren | 当前节点的子节点个数 |
pzxid | 表示该节点的子节点列表最后一次被修改时的事务ID。注意,只有子节点列表表更了才会变更pzxid,子节点内容变更不会影响pzxid |
版本-保证分布式数据原子性操作
Zookeeper中为数据节点引入了版本的概念,每个数据节点都具有三种类型的版本信息,对数据节点的任何更新操作都会引起版本号的变化。
版本类型 | 说明 |
---|---|
version | 当前数据节点数据内容的版本号 |
cversion | 当前数据节点子节点的版本号 |
aversion | 当前数据节点ACL变更版本号 |
Zookeeper中的版本概念和传统意义上的软件版本有很大区别,它表示的是对数据节点的数据内容、子节点列表,或是节点ACL信息的修改次数,我们以其中的version这种版本类型为例来说明。在一个数据节点/zk-book被创建完毕之后,节点的version值是0,表示的含义是当前节点自从创建之后,被更新过0次。如果现在对该节点的数据内容进行更新操作,那么随后,version的值就会变成1.同时需要注意的是,在上文中提到的关于version的说明,其表示的是对数据节点数据内容的变更次数,强调的是变更次数,因此即使前后两次变更并没有使得数据内容的值发生变化,version的值依然会变更。
一个多线程应用,尤其是分布式系统,在运行过程中往往需要保证数据访问的排他性。在数据库技术中,通常提到的悲观锁和乐观锁就是这种机制的典型实现。
悲观锁,又被称为悲观并发控制(Pessimistic Concurrency Control,PCC),是数据库中一种非常典型且非常严格的并发控制策略。悲观锁具有强烈的预占和排他特性,能够有效地避免了不同事务对同一数据并发更新而造成的数据一致性问题。在悲观锁的实现原理中,如果一个事务(假定事务A)正在对数据进行处理,那么在整个处理过程中,都会将数据处于锁定状态,在这期间,其他事务将无法对这个数据进行更新操作,直到事务A完成对该数据的处理,释放了对应的锁之后,其他事务才能够重新竞争来对数据进行更新操作。也就是说,对于一份独立的数据,系统只分配了一把唯一的钥匙,谁获得了这把钥匙,谁就有权力更新这份数据。一般我们认为,在实际生产应用中,悲观锁策略适合解决那些对于数据更新竞争十分激烈的场景-在这类场景中,通常采用简单粗暴的悲观锁机制来解决并发控制问题。
乐观锁,又被称为乐观并发控制(Optimistic Concurrent Control,OCC),也是一种常见的并发控制策略。相对于悲观锁而言,乐观锁机制显得更加宽松与友好。从上面的悲观锁的讲解中我们可以看到,悲观锁假定不同事务之间的处理一定会出现相互干扰,从而需要在一个事务从头到尾的过程中都会数据进行加锁处理。而乐观锁则正好相反,它假定多个事务在处理过程中不会彼此影响,因此在事务处理的绝大部分时间里不需要进行加锁处理。当然,既然有并发,就一定会存在数据更新冲突的可能。在乐观锁机制中,在更新请求提交之前,每个事务都会首先检查当前事务读取数据后,是否在其他事务对该数据进行了修改。如果其他事务有更新的话,那么正在提交的事务就需要回滚。乐观锁通常适合使用在数据并发竞争不大、事务冲突较少的应用场景中。
从上面的讲解中,我们其实可以把一个乐观锁控制的事务分成如下三个阶段:数据读取、写入校验和数据写入,其中写入检验阶段是整个乐观控制锁的关键所在。在写入校验阶段,事务会检查数据在读取阶段后是否有其他事务对数据进行过更新,以确保数据更新的一致性。简单地讲,对于值V,每次更新前都会比对其值是否是预期值A,只有符合预期,才会将V原子化地更新到新值B,其中是否符合预期便是乐观锁中的写入校验阶段。
Watcher-数据变更的通知
Zookeeper提供了分布式数据的发布/订阅功能。一个典型的发布/订阅模型系统定义了一种一对多的订阅关系,能够让多个订阅者同时监听某一个主题对象,当这个主题对象自身状态变化时,会通知所有订阅者,使他们能够做出相应的处理。在Zookeeper中,引入了Watcher机制来实现这种分布式的通知功能。Zookeeper允许客户端向服务端注册一个Watcher监听,当服务端的一些指定时间触发了这个Watcher,那么就会向指定客户端发送了一个事件通知来实现分布式通知功能。
我们可以看到,Zookeeper的Watcher机制主要包括客户端线程、客户端WatcherManager和Zookeeper服务器三部分。在具体工作流程上,简单地将,客户端在向Zookeeper服务器注册Watcher的同时,会将Watcher对象存储在客户端的WatchManager中。当Zookeeper服务器端触发Watcher事件后,会向客户端发送通知,客户端线程从WatchManager中取出对应的Watcher对象来执行回调逻辑。
Watcher接口
在Zookeeper中,接口类Watcher用于表示一个标准的事件处理器,其定义了事件通知相关逻辑,包含KeeperState和EventType两个枚举类,分别代表了通知状态和事件类型,同时定义了事件的回调方法:process(WatchedEvent event)。
Watcher事件
同一个事件类型在不同的通知状态中代表的含义有所不同。
即使使用相同的数据内容来更新,还是会触发这个事件通知,因为对于Zookeeper来说,无论数据内容是否变更,一旦有客户端调用了数据更新接口,且更新成功,就会更新dataVersion值。
回调方法process
process方法是Watcher接口中的一个回调方法,当Zookeeper向客户端发送一个Watcher事件通知时,客户端就会对相应的process方法进行回调,从而实现了对事件处理。process方法的定义如下:
abstract public void process(WatchedEvent event);
WatchedEvent包含了每一个事件的三个基本属性:通知状态(keeperState)、事件类型(eventType)和节点路径(path)。Zookeeper使用WatchedEvent对象来封装服务端事件并传递给Watcher,从而方便回调方法process对服务端事件进行处理。
工作机制
Zookeeper的Watcher机制,总的来说可以概括以下三个过程:客户端注册Watcher、服务端处理Watcher和客户端回调Watcher。
客户端注册Watcher
在前面,我们提到了创建一个Zookeeper客户端实例时,可以向构造方法中传入一个默认的Watcher:
public Zookeeper(String connectString, int sessionTimeout,Watcher watcher);
这个Watcher将作为整个Zookeeper会话期间的默认Watcher,会一直被保存在客户端ZKWatchManager的defaultWatcher中。另外,Zookeeper客户端也可以通过getData、getChildren和exist三个接口来向Zookeeper服务器注册Watcher,无论使用那种方式,注册Watcher的工作原理都是一致的。
Watcher特性总结
Zookeeper的Watcher具有以下几个特性。
一致性:从上面介绍中可以看到,无论是服务端还是客户端,一旦一个Watcher被触发,Zookeeper都会将其从相应的存储中移除。因此,开发人员在Watcher的使用上要记住的一点是需要反复注册。这样的设计有效地减轻了服务端的压力。
客户端串行执行:客户端Watcher回调的过程是一个串行同步的过程,这为我们保证了顺序,同时需要开发人员注意的一点是,千万不要因为一个Watcher的处理逻辑影响了整个客户端的Watcher回调。
轻量:WatchedEvent是Zookeeper整个Watcher通知机制的最小通知单元,这个数据结构中只包含了三部分内容:通知状态、事件类型和节点路径。
ACL-保障数据的安全
权限模式、授权对象和权限。
权限模式:Scheme
权限模式用来确定权限验证过程中使用的检验策略。在Zookeeper中,开发人员使用的多的就是以下四种权限模式。
IP:IP模式通过IP地址粒度来进行权限控制。
Gigest:Digest是最常用的权限控制模式,也更符合我们对于权限控制的认识,Zookeeper会对其先后进行两次编码处理,分别是SHA-1算法加密和BASE64编码,其具体实现由DigestAuthenticationProvider进行封装。
在 ZooKeeper 中已经定义好的权限有 5 种:
- 数据节点(create)创建权限,授予权限的对象可以在数据节点下创建子节点;
- 数据节点(wirte)更新权限,授予权限的对象可以更新该数据节点;
- 数据节点(read)读取权限,授予权限的对象可以读取该节点的内容以及子节点的信息;
- 数据节点(delete)删除权限,授予权限的对象可以删除该数据节点的子节点;
- 数据节点(admin)管理者权限,授予权限的对象可以对该数据节点体进行 ACL 权限设置。
需要注意的一点是,每个节点都有维护自身的 ACL 权限数据,即使是该节点的子节点也是有自己的 ACL 权限而不是直接继承其父节点的权限。
Leader选举
leader选举步骤如下:
1、初始化选举。
Leader选举可以说是集群和单机模式启动Zookeeper最大的不同点。Zookeeper首先会根据自身的SID、LastLoggedZXID(最新的ZXID)和当前的服务器epoch(currentEpoch)来生成一个初始化的投票-简单地讲,在初始化过程中,每个服务器都会给自己投票。
然后,Zookeeper会根据zoo.cfg中的配置,创建相应的Leader选举算法实现。在Zookeeper中,默认提供了三种Leader选举算法的实现,分别是LeaderElection、AuthFastLeaderElection和FastLeaderElection,可以通过在配置文件中使用electionAlg属性来指定,分别使用数字0-3来表示。
在初始化阶段,Zookeeper会首先创建Leader选举所需的网络IO层QuorumCnxManager,同时启动对Leader选举端口的监听,等待集群中其他服务器创建连接。
2、注册JMX服务。
3、检测当前服务器状态。
在正常情况下,Zookeeper服务器的状态在Looking、Leading和Following/Observing之间进行切换。而在启动阶段,初始状态时LOOKING,因此开始进行Leader选举。
4、Leader选举
Zookeeper的Leader选举过程,简单地讲,就是一个集群中所有的机器相互之间进行一系列投票,选举产生最合适的机器称为leader,同时其余机器成为Follower或是Observer的集群机器角色初始化过程。关于Leader选举算法,简而言之,就是集群中哪个机器处理的数据越新,其越有可能成为Leader。当然,如果集群中的所有机器处理的ZXID一致的话,那么SID最大的服务器成为Leader。
Leader和Follower启动交互过程:
Leader和Follower服务器启动交互过程包括如下几个步骤:
1、创建Leader服务器和Follower服务器。
完成Leader选举之后,每个服务器都会根据自己的服务器角色创建相应的服务器实例,并开始进入各自角色的主流程。
2、Leader服务器启动Follower接收器LearnerCnxAcceptor。
在Zookeeper集群运行期间,Leader服务器需要和所有其余的服务器保持连接以确定集群的机器存活情况。LearnerCnxAcceptor接收器用于负责接收所有非Leader服务器的连接请求。
3、Learner服务器开始和Leader建立连接。
所有的Learner服务器在启动完毕后,会从Leader选举的投票结果中找到当前集群中的Leader服务器,然后与其建立连接。
4、Leader服务器创建LearnerHandler。
Leader接收到来自其他机器的连接创建请求后,会创建一个LearnerHandler实例。每个LearnerHandler实例都对应了一个Leader与Learner服务器之间的连接,其负责Leader和Learner服务器之间几乎所有的消息通信和数据同步。
5、向Leader注册。
当和Leader建立起连接之后,Learner就会开始向Leader进行注册,所谓的注册,其实就是将Learner服务器自己的基本信息发送给Leader服务器,我们称之为LearnerInfo,包括当前服务器的SID和服务器处理的最新的ZXID。
6、Leader解析Learner信息,计算新的epoch。
Leader服务器在接收到Learner的基本信息后,会解析出该Learner的SID和ZXID,然后根据该Learner的ZXID解析出其对应的epoch_of_learner,和当前Learder服务器的epoch_of_leader进行比较,如果该Learner的epoch_of_learner更大的话,那么就更新Leader的epoch:
epoch_of_leader = epoch_of_learner + 1
然后,learnerHandler会进行等待,直到过半的Learner向Leader进行了注册,同时更新了epoch_of_leader之后,Leader就可以确定当前集群的epoch了。
7、发送Leader状态。
计算出新的epoch之后,Leader会将该信息以一个LEADERINFO消息的形式发送给Learner,同时等待Learner的响应。
8、Learner发送ACK消息。
Follower在收到来自Leader的LEADERINFO消息后,会解析出epoch和和ZXID,然后向Leader反馈一个ACKPOCH响应。
9、数据同步。
Leader服务器收到Learner的这个ACK消息后,就可以开始与其进行数据同步了。
10、启动Leader和Learner服务器。
当有过半的Learner已经完成了数据同步,那么Leader和Learner服务器实例就可以开始启动了。
Leader和Follower启动
Leader和Follower启动的步骤如下。
1、创建并启动会话管理器。
2、初始化Zookeeper的请求处理链。
3、注册JMX服务。
至此,集群版的Zookeeper服务器启动完毕。
Leader选举
Leader选举概述
Leader选举是Zookeeper中最重要的技术之一,也是保证分布式数据一致性的关键所在。
服务器启动期间的Leader选举
在我们讲解Leader选举的时候,需要注意的一点是,隐式条件便是Zookeeper的集群规模至少是2台机器,这里我们以3台机器组成的服务器集群为例。在服务器集群初始化阶段,当有一台服务器启动的时候,它是无法完成Leader选举的。当第二台机器也启动后,此时这两台机器已经能够相互通信,每台机器都试图找到一个Leader。于是便进入了Leader选举流程。
1、每个Server会发出一个投票。
由于是初始情况,因此对于Server1和Server2来说,都会将自己作为Leader服务器来进行投票,每次投票包含的最基本的元素包括:所推举的服务器的myid和ZXID,我们以(myid,ZXID)的形式来表示。因为是初始化阶段,因此无论是Server1还是Server2,都会投给自己,即Server1的投票为(1,0),Server2的投票为(2,0),然后各自将这个投票发送给集群中其他所有机器。
2、接收来自各个服务器的投票。
每个服务器都会接收来自其他服务器的投票。集群中的每个服务器在接受到投票后,首先会判断该投票的有效性,包括检查是否是本轮投票,是否来自Locking状态的服务器。
3、处理投票。
在接收到来自其他服务器的投票后,针对每个投票,服务器都需要将别人的投票和自己的投票进行PK,PK的规则如下。
- 优先检查ZXID。ZXID比较大的服务器优先作为Leader。
- 如果ZXID相同的话,那么就比较Myid.myid比较大的服务器作为Leader服务器。
现在我们来看Server1和Server2实际是如何进行投票处理的。对于Server1来说,它自己的投票是(1,0),而接收到的投票为(2,0)。首先会对比两者的ZXID,因为都是0,所以无法决定谁是Leader。接下来会对比两者的myid,很显然,Server1发现接收到的投票中的myid是2,大于自己,于是就会更新自己的投票为(2.0),然后重新将投票发出去。而对于Server2来说,不需要更新自己的投票信息,只是再一次向集群中所有机器发出上一次投票信息即可。
4、统计投票
每次投票后,服务器都会统计所有投票,判断是否已经有过半的机器接收到相同的投票信息。对于Server1和Server2服务器来说,都统计出集群中已经有两台机器接受了(2,0)。这里我们需要对过半的概念做一个简单的介绍,所谓过半就是指大于集群机器数量的一半,即大于或等于(n/2 + 1)。对于这里由3台机器构成的集群,大于等于2即为达到过半要求。
5、改变服务器状态
一旦确定了Leader,每个服务器就会更新自己的状态:如果是Follower,那么就变更为Following,如果是Leader,那么就变更为Leading。
服务器运行期间的Leader选举
在Zookeeper集群正常运行过程中,一旦选出了一个Leader,那么所有服务器的集群角色一般不会再发生变化-也就是说,Leader服务器将会一直作为集群的Leader,即使集群中有非Leader集群挂了或是有新机器加入集群也不会影响Leader。但是一旦Leader所在的机器管理,那么整个集群将暂时无法对外提供服务,而是进入新一轮的Leader选举。服务器运行期间的Leader选举和启动时期的Leader选举基本过程是一致的。
我们假设当前正在运行的Zookeeper服务器由3台机器组成,分别是Server1、Server2和Server3,当前的Leader是Server2.假设在某一个瞬间,Leader挂了,这个时候便开始了Leader选举了。
1、状态变更
当Leader挂了之后,余下的非Observer服务器都会将自己的服务器状态变更为LOOKING,然后开始进入Leader选举流程。
2、每个Server会发出一个投票。
在这个过程中,需要生成投票信息(myid,ZXID)。因为是运行期间,因此每个服务器上的ZXID可能不同,我们假定Server1和ZXID为123,而Server3的ZXID为122.在第一轮投票中,Server1和Server3都会投自己,即分别产生投票(1,123)和(3,122),然后将各自将这个投票发给集群中所有机器。
3、接受来自各个服务器的投票
4、处理投票。
对于投票的处理,和上面提到的服务器启动期间的处理规则是一样的。这里会把Server1选举为Leader。
5、统计投票。
6、改变服务器状态。
算法分析
进入Leader选举
当Zookeeper集群中的一台服务器出现以下两种情况之一时,就会开始进入Leader选举。
- 服务器初始化启动。
- 服务器运行期间无法和Leader保持连接。
而当一台机器进入Leader选举流程时,当前集群也可能会处于以下两种状态。
- 集群中本来就已经存在一个Leader。
- 集群中确实不存在Leader。
我们首先来看第一种已经存在Leader的情况。这种情况通常是集群中的某一台机器启动的比较晚,在它启动之前,集群已经可以正常工作,即已经存在了一台Leader服务器。针对这种情况,当该机器试图去选举Leader的时候,会被告知当前服务器的Leader信息,对于该机器来说,仅仅需要和Leader建立起连接,并进行状态同步即可。
下面我们重点来看在集群中Leader不存在的情况下,如何进行Leader选举。
开始第一次投票
通常有两种情况会导致集群中不存在Leader,一种情况是在整个服务器刚刚初始化启动时,此时尚未产生一台Leader服务器;另一种情况就是在运行期间当前Leader所在的服务器挂了。无论是那种情况,此时集群中的所有机器都处于一种试图选举出一个Leader的状态,我们把这个状态称为LOOKING,意思是说正在寻找Leader。当一台服务器处于LOOKING的时候,那么它就会向集群中所有其他的机器发送消息,我们称这个消息为投票。
在这个投票消息中包含了两个最基本的信息:所推举的服务器的SID和ZXID,分别表示了被推举服务器的唯一标识和事务ID。
我们假设Zookeeper由5台机器组成,SID分别为1,2,3,4,5,ZXID分别为9,9,9,8,8,并且此时SID为2的机器是Leader服务器。某一时刻,1和2所在的机器出现故障,因此集群开始进行Leader选举。
在第一次投票的时候,由于还无法检测到集群中其他机器的信息,因此每台机器都是将自己作为被推举的对象来进行投票。于是SID为3,4,5的机器分别投票为(3,9)(4,8)(5,8)。
变更投票
集群中的每台机器发出自己的投票后,也会接收到来自集群中其他机器的投票。每台机器都会根据一定的规则,来处理收到的其他机器的投票,并以此来决定是否需要变更自己的投票。这个规则也成为了整个Leader选举算法的核心所在。
每次对于收到的投票的处理,都是一个对(vote_sid,vote_zxid)和(self_sid,self_zxid)对比的过程:
- 规则1:如果vote_zxid大于self_zxid,就认可当前收到的选票,并再次将该投票发送出去。
- 规则2:如果vote_zxid小于self_zxid,那么就坚持自己的投票,不做变更。
- 规则3:如果vote_zxid等于self_zxid,那么就对比两者的SID。如果vote_sid大于self_sid,那么就认可当前接收到的投票,并再次将该投票发送出去。
- 规则4:如果vote_zxid等于self_zxid,并且vote_sid小于self_sid,那么同样坚持自己的投票,不作变更。
确定Leader
经过第二次投票后,集群中的每台机器都会再次收到其他机器的投票,然后开始统计投票,如果一台机器收到了超过半数的相同的投票,那么这个投票对应的SID机器即为Leader。
小结
简单地说,通常哪台服务器上的数据越新,那么就越有可能成为Leader,原因很简单,数据越新,那么它的ZXID也就越大,也就越能够保证数据的恢复。当然,如果集群中有几个服务器具有相同的ZXID,那么SID较大的那台机器成为Leader。
Leader选举的实现细节
算法核心
1、自增选举轮次。
在FastLeaderElection实现中,有一个logicalclock属性,用于标识当前Leader的选举轮次,Zookeeper规定了所有有效的投票必须在同一轮次中。Zookeeper在开始新一轮的投票时,会首先logicalclock进行自增操作。
2、初始化选票。
在开始进行新一轮的投票之前,每个服务器都会首先初始化自己的选票。
3、发送初始化选票。
在完成选票的初始化后,服务器就会发起第一次投票。Zookeeper会将刚刚初始化好的选票放入sendqueue队列中,由发送器WorkerSender负责发送出去。
4、接收外部投票。
每台服务器都会不断地从recqueue队列中获取外部投票。如果服务器发现无法获取到任何的外部投票,那么就会立即确认自己是否和集群中其他服务器保持着有效连接。如果发现没有建立连接,那么就会马上建立连接。如果已经建立了连接,那么就再次发送自己当前的内部投票。
5、判断选举轮次。
当发送完初始化投票之后,接下来就要开始处理外部投票了。在处理外部投票的时候,会根据选举轮次来进行不同的处理。
-
外部投票的选举轮次大于内部投票。
如果服务器发现自己的选举轮次已经落后于该外部投票对应的服务器的选举轮次,那么就立即更新自己的选举轮次(logicalclock),并且清空所有已经收到的投票,然后初始化的投票来进行PK已确定是否变更内部投票,最终在将内部的投票发送出去。 -
外部投票的选举轮次小于内部投票。
如果接收到的选票的选举轮次落后于服务器自身的,那么Zookeeper就会直接忽略该外部投票,不做任何处理。 -
外部投票的选举轮次和内部投票一致。
这也是绝大多数投票的场景,如果外部投票的选举轮次和内部投票一致的话,那么就开始进行选票PK。
总的来说,只有在同一选举轮次的投票才是有效的投票。
6、选票PK
选票PK的目的是为了确定当前服务器是否需要变更投票,主要从选举轮次、ZXID和SID三个因素来考虑,具体条件如下:在选票PK的时候依次判断,符合任意一个条件就需要进行投票变更。
- 如果外部投票中被推举的Leader服务器的选举轮次大于内部投票,那么就需要进行投票变更。
- 如果选举轮次一致的话,那么就对比两者的ZXID。如果外部投票的ZXID大于内部投票,那么就需要进行投票变更。
- 如果两者的ZXID一致,那么就对比两者的SID。如果外部投票的SID大于内部投票,那么就需要进行投票变更。
7、变更投票。
通过选票PK后,如果确定了外部投票优于内部投票,那么就进行投票变更-使用外部投票的选票信息来覆盖内部投票。变更完成后,再次将这个变更后的内部投票发送出去。
8、选票归档。
无论是否进行了投票变更,都会将刚刚收到的那份外部投票放入选票集合中进行归档。
9、统计投票。
完成了选票归档之后,就可以开始统计投票了。统计投票的过程就是为了统计集群中是否已经有过半的服务认可了当前的内部投票。如果已经确定了有过半的服务器认可了该内部投票,则中止投票。
10、更新服务器状态
统计投票后,如果已经确定了可以终止投票,那么就开始更新服务器的状态。服务器会首先判断当前过半服务器认可的投票所对应的Leader服务器是否是自己,如果是自己的话,那么就会将自己的服务器状态更新为LEADING。如果自己不是被选举产生的Leader的话,那么就会根据具体情况来确定自己是FOLLOWING或是OBSERVING。
Leader
Leader服务器时整个Zookeeper集群工作机制中的核心,其主要工作有以下两个:
- 事务请求的唯一调度者和处理者,保证集群事务处理的顺序性。
- 集群内部各服务器的调度者。
Follower
Follower服务器是Zookeeper集群状态的跟随者,其主要工作有以下三个。
- 处理客户端非事务请求,转发事务请求给Leader服务器。
- 参与事务请求Proposal的投票。
- 参与Leader选举投票。
Observer
Observer服务器在工作原理上和Follower基本是一致的,对于非事务请求,都可以进行独立的处理,而对于事务请求,则会转发给Leader服务器进行处理。和Follower唯一的区别在于,Observer不参与任何形式的投票,包括事务请求Proposal的投票和Leader选举投票。简单地讲,Observer服务器只提供非事务服务,通常用于在不影响集群事务处理能力的前提下提升集群的非事务处理能力。
请求处理
请求接收
1、IO层接收来自客户端的请求。
在Zookeeper中,NIOServerCnxn实例维护每一个客户端连接,客户端与服务端的所有通信都是由NIOServerCnxn负责的-其负责统一接收来自客户端的所有请求,并将请求内容从底层网络IO中完整地读取出来。
2、判断是否是客户端会话创建的请求。
NIOServerCnxn在负责网络通信的同时,自然也承担了客户端会话的载体-每个会话都会对应一个NIOServerCnxn实体。因此,对于每个请求,Zookeeper都会检查当前NIOServerCnxn实体是否已经被初始化。如果尚未被初始化,那么就可以确定该客户端请求一定是会话创建的请求。很显然,在会话创建初期,NIOServerCnxn尚未得到初始化,因此此时的第一个请求必定是会话创建请求。
3、反序列化ConnectRequest请求。
一旦确定了当前客户端请求时会话创建请求,那么服务端就可以对其进行反序列化,并生成一个ConnectRequest请求实体。
4、判断是否是ReadOnly客户端。
在Zookeeper的设计实现中,如果当前Zookeeper服务器是以ReadOnly模式启动的,那么所有来自非ReadOnly型客户端的请求将无法处理。因此,针对ConnectRequest,服务端会首先检查其是否是ReadOnly客户端,并以此来决定是否接收该会话创建请求。
5、检查客户端ZXID。
在正常情况下,同一个Zookeeper集群中,服务端的ZXID必定大于客户端的ZXID,因此如果发现客户端的ZXID值大于服务端的ZXID值,那么服务端将不接受该客户端的会话创建请求。
6、协商sessionTimeout。
客户端在构造Zookeeper实例的时候,会有一个sessionTimeout参数用于指定会话的超时时间。客户端可以向服务器发送这个超时时间后,服务器会根据自己的超时时间限制最终确定该会话的超时时间-这个过程就是sessionTimeout协商过程。
7、判断是否需要重新创建会话。
服务端根据客户端请求中是否包含sessionID来判断该客户端是否需要重新创建会话。如果客户端请求中已经包含了sessionID,那么就认为该客户端正在进行会话重连。在这种情况下,服务端只需要重新打开这个会话,否则需要重新创建。
会话创建
8、为客户端生成sessionID
9、注册会话
10、激活会话
11、生成会话码
预处理
12、将请求交给Zookeeper的PreRequestProcessor处理器进行处理
13、创建请求事务头。
14、创建请求事务体。
15、注册与激活会话。
事务处理
16、将请求交给ProposalRequestProcessor处理器
Proposal流程
在Zookeeper的实现中,每一个事务请求都需要集群中过半机器投票认可才能被真正应用到Zookeeper的内存数据库中去,这个投票与统计过程被称为Proposal流程。
1、发起投票。
如果当前请求时事务请求,那么Leader服务器就会发起一轮事务投票。在发起事务投票之前,首先会检查当前服务端的ZXID是否可用。
2、生成提议Proposal。
如果当前服务端的ZXID可用,那么就可以开始事务投票了。Zookeeper会将之前创建的请求头和事务体,以及ZXID的请求本身序列化到Proposal对象中-此处生成的Proposal对象就是一个提议,即针对Zookeeper服务器状态的一次变更申请。
3、广播提议。
生成提议后,Leader服务器会以ZXID作为标识,将该提议放入投票箱中,同时将该提议广播给所有的Follower服务器。
4、收集投票
Follower服务器在接收到Leader发来的这个提议后,会进入SYNC流程来进行事务日志的记录,一旦日志记录后,就会发送ACK消息给Leader服务器,Leader服务器根据这些ACK消息来统计每个提议的投票情况。当一个提议获得了集群中过半机器的投票,那么就认为这个提议通过,接下去就可以进入提议的Commit阶段了。
5、将请求放入toBeApplied队列。
6、广播commit消息。
一旦Zookeeper确认一个提议已经可以被提交了,那么Leader服务器就会向Follower和Observer服务器发送commit消息,以便所有服务器能够提交该提议。
数据与存储
在Zookeeper中,数据存储分为两部分:内存数据存储与磁盘数据存储。
ZKDatabase是Zookeeper的内存数据库,负责管理Zookeeper的所有会话、DataTree存储和事务日志。ZKDatabase会定时向磁盘dump快照数据,同时在Zookeeper服务器启动的时候,会通过磁盘上的事务日志和快照数据文件恢复成一个完整的内存数据库。