ZooKeeper系统模型之会话创建请求。

        ZooKeeper服务端对于会话创建的处理,大体可以分为请求接收、会话创建、预处理、事务处理、事务应用和会话响应6大环节,其大体流程如下图所示。

其中事务处理部分的流程详见下图所示。

请求接收

I/O层接收来自客户端的请求

        在ZooKeeper中,NIOServerCnxn实例维护每一个客户端连接,客户端与服务端的所有通信都是由NIOServerCnxn负责的——其负责统一接受来自客户端的所有请求,并将请求内容从底层I/O中完整的读取出来。

判断是否是客户端“会话创建”请求

        NIOServerCnxn在负责网络通信的同时,自然也承担了客户端会话的载体——每个会话都会对应一个NIOServerCnxn实体。因此,对于每个请求ZooKeeper都会检查当前NIOServerCnxn实体是否已经被初始化。如果尚未被初始化,那么就可以确定该客户单请求一定是“会话创建”请求。很显然,在会话创建初期,NIOServerCnxn尚未得到初始化,因此此时的第一个请求必定是“会话创建”请求。

反序列化ConnectRequest请求

        一旦确定当前客户端请求是“会话创建”请求,那么服务端就可以对其进行反序列化,并生成一个ConnectRequest请求实体。

判断是否是ReadOnly客户端

        在ZooKeeper的设计实现中,如果当前ZooKeeper服务器是以ReadOnly模式启动的,那么所有来自非ReadOnly型客户端的请求将无法被处理。因此,针对ConnectRequest,服务器会首先检查其是否是ReadOnly客户单,并以此来决定是否接收该“会话创建”请求。

检查客户端ZXID

        在正常情况下,同一个ZooKeeper集群中,服务端的ZXID必定大于客户端的ZXID,因此如果发现客户端的ZXID值大于服务端的ZXID值,那么服务端将不接受该客户端的“会话创建”请求。

协商sessionTimeout

        客户端在构造ZooKeeper实例的时候,会有一个sessionTimeout参数用于指定会话的超时时间。客户端向服务器发送这个超时时间后,服务器会根据自己的超时时间限制最终确定该会话的超时时间——这个过程就是sessionTimeout协商过程。

        默认情况下,ZooKeeper服务端对超时时间的限制介于2个tickTime到20个tickTime之间。即如果我们设置tickTime值为2000(单位:毫秒)的话,那么服务端就会限制客户端的超时时间,使之介于4秒到40秒之间。可以通过zoo.cfg中的相关配置来调整这个超时时间的限制。

判断是否需要重新创建会话

        服务端根据客户端请求中是否包含sessionID来判断该客户端是否需要重新创建会话。如果客户端请求中已经包含了sessionID,那么就认为该客户端正在进行会话重连。在这种情况下,服务端只需要重新打开这个会话,否则需要重新创建。

会话创建

为客户端生成sessionID

        在为客户端创建会话之前,服务端首先会为每个客户端都分配一个sessionID。分配方式其实很简单,每个ZooKeeper服务器在启动的时候,都会初始化一个会话管理器(SessionTracker),同时初始化sessionID,我们将其称为“基准sessionID”。因此针对每个客户端,只需要在这个“基准sessionID”的基础上进行逐个递增就可以了。

        由于sessionID是ZooKeeper会话的一个重要标识,许多与会话相关的运行机制都是基于这个sessionID的,因此,无论是哪台服务器为客户端分配的sessionID,都无比保证全局唯一。在ZooKeeper中,是通过保证“基准sessionID”的全局唯一来确保每次分配的sessionID在集群内部都各不相同。因此,“基准sessionID”的初始化算法非常重要。

注册会话

        创建会话总重要的工作就是向SessionTracker中注册会话。SessionTracker中维护了两个比较重要的数据结构,分别是sessionsWithTimeout和sessionById。前者根据sessionID保存了所有会话的超时时间,而后者则是根据sessionID保存了所有会话实体。在会话创建初期,就应该将该客户端会话的相关信息保存到这两个数据结构中,方便后续会话管理器进行管理。

激活会话

        向SessionTracker注册完会话后,接下来还需要对会话进行激活操作。激活会话过程涉及ZooKeeper会话管理的分桶策略。激活会话的核心是为会话安排一个区块,以便会话清理程序能够快速高效地进行会话清理。

生成会话密码

        服务端在创建一个客户端会话的时候,会同时为客户端生成一个会话密码,连同sessionID一起发送给客户端,作为会话在集群中不同机器间转移的凭证。会话密码的生成算法非常简单,如下:

预处理

将请求交给ZooKeeper的PrepRequestProcessor处理器进行处理

        ZooKeeper对于每个客户端请求的处理模型采用了典型的责任链模式——每个客户端请求都会由几个不同的请求处理器依次进行处理。

        另外,在提交给第一个请求处理器前,ZooKeeper还会根据该请求所属的会话,进行一次激活会话操作,以确保当前会话处于激活状态。完成会话激活之后,ZooKeeper就会将请求提交给第一个请求处理器:PrepRequestProcessor。

创建请求事务头

         对于事务请求,ZooKeeper首先会为其创建请求事务头。请求事务头是每一个ZooKeeper事务请求中非常重要的一部分,服务端后续的请求处理器都是基于该请求头来识别当前请求是否是事务请求。请求事务头包含了一个事务请求最基本的一些信息,包括sessionID、ZXID、CXID和请求类型等,如下表所示。

 

属性说明
clientId客户端ID,用来唯一标识该请求所属的客户端
cxid客户端的操作序列号
zxid该事务请求对应的事务ZXID
time服务器开始处理该事务请求的时间
type事务请求的类型,例如create、delete、setData和createSession等,这些事务类型都被定义在org.apache.zookeeper.ZooDefs.OpCode类中

创建请求事务体

         对于事务请求,ZooKeeper还会为其创建请求的事务体。在此处由于是“会话创建”请求,因此会创建事务体CreateSessionTxn。

注册与激活会话

        此处的注册与激活会话过程,和上面步骤9中提到的过程是一致的,虽然重复了,但是不会引起额外的问题。此处进行会话注册与激活的目的是处理由非Leader服务器转发过来的会话创建请求。在这种情况下,其实尚未在Leader的SessionTracker中进行会话的注册,因此需要在此处进行一次注册与激活。

事务处理

将请求交给ProposalRequestProcessor处理器

        完成对请求的预处理后,PropRequestProcessor处理器会将请求交付给自己的下一级处理器:ProposalRequestProcessor。

        ProposalRequestProcessor处理器,顾名思义,是一个与提案相关的处理器。所谓的提案,是ZooKeeper中针对事务请求所展开的一个投票流程中对事务操作的包装。从ProposalRequestProcessor处理器开始,请求的处理将会进入三个子处理流程,分别是Sync流程、Proposal流程和Commit流程。

Sync流程

        所谓Sync流程,其核心就是使用SyncRequestProcessor处理器记录事务日志的过程。ProposalRequestProcessor处理器在接收到一个上级服务器流转过来的请求后,首先会判断该请求是否是事务请求。针对每个事务请求,都会通过事务日志的形式将其记录下来。Leader服务器和Follower服务器的请求处理链路中都会有这个处理器,两者在事务日志的记录功能上是完全一致的。

        完成事务日志记录后,每个Follower服务器都会向Leader服务器发送ACK消息,表明自身完成了事务日志的记录,以便Leader服务器统计每个事务请求的投票情况。

Proposal流程

        在ZooKeeper的实现中,每一个事务请求都需要集群中过半机器投票认可才能被真正应用到ZooKeeper的内存数据库中去,这个投票与统计过程被称为“Proposal流程”。

发起投票

        如果当前请求是事务请求,那么Leader服务器就会发起一轮事务投票。在发起事务投票之前,首先会检查当前服务端的ZXID是否可用。关于ZooKeeper的ZXID可用性检查,如果当前服务端的ZXID不可用,那么将会抛出XidRolloverException异常。

生成提议Proposal

        如果当前服务端的ZXID可用,那么就可以开始事务投票了。ZooKeeper会将之前创建的请求头和事务体,以及ZXID和请求本身序列化到Proposal对象中——此处生成的Proposal对象就是一个提议,即针对ZooKeeper服务器状态的一次变更申请。

广播提议

        生成提议后,Leader服务器会以ZXID作为标识,将该提议放入投票箱outstandingProposals中,同时会将该提议广播给所有的Follower服务器。

收集投票

        Follower服务器在接收到Leader发来的这个提议后,会进入Sync流程来进行事务日志的记录,一旦日志记录完成后,就会发送ACK消息给Leader服务器,Leader服务器根据这些ACK消息来统计每个提议的投票情况。

        当一个提议获得了集群中过半机器的投票,那么就认为该提议同构,接下去就可以进入提议的Commit阶段了。

将请求放入toBeApplied队列

        在该提议被提交之前,ZooKeeper首先会将其放入toBeApplied队列中去。

广播COMMIT消息

        一旦ZooKeeper确认一个提议已经可以被提交了,那么Leader服务器就会向Follower和Observer服务器发送COMMIT消息,以便所有服务器都能够提交该提议。这里需要注意的一点是,由于Observer服务器并未参加之前的提议投票,因此Observer服务器尚未保存任何关于该提议的信息,所以在广播COMMIT消息的时候,需要区别对待,Leader会向其发送一种被称为“INFORM”的消息。该消息体中包含了当前提议的内容。而对于Follower服务器,由于已经保存了所有关于该提议的信息,因此Leader服务器只需要向其发送ZXID即可。

Commit流程

将请求交付给CommitProcessor处理器

        CommitProcessor处理器在收到请求后,并不会立即处理,而是会将其放入queuedRequests队列中。

处理queuedRequests队列请求

        CommitProcessor处理器会有一个单独的线程来处理从上一级处理器流转下来的请求。当检测到queuedRequests队列中已经有新的请求进来,就会逐个从队列中取出请求进行处理。

标记nextPending

        如果从queuedRequests队列中取出的请求是一个事务请求,那么就需要进行集群中服务器之间的投票处理,同时需要将nextPending标记为当前请求。标记nextPending的作用,一方面是为了确保事务请求的顺序性,另一方面也是便于CommitProcessor处理器检测当前集群中是否正在进行事务请求的投票。

等待Proposal投票

        在Commit流程处理的同时,Leader已经根据当前事务请求生成了一个提议Proposal,并广播给了所有的Follower服务器。因此,在这个时候,Commit流程需要等待,直到投票结束。

投票通过

        如果一个提议已经获得了过半机器的投票认可,那么将会进入请求提交阶段。ZooKeeper会将该请求放入committedRequests队列中,同时唤醒Commit流程。

提交请求

        一旦发现committedRequests队列中已经有可以提交的请求了,那么Commit流程就会开始提交请求。当然在提交之前,为了保证事务请求的顺序执行,Commit流程还会对比之前标记的nextPending和committedRequests队列中第一个请求是否一致。

        如果检查通过,那么Commit流程就会将该请求放入toProcess队列中,然后交付给下一个请求处理器:FinalRequestProcessor。

事务应用

交付给FinalRequestProcessor处理器

        请求流转到FinalRequestProcessor处理器后,也就接近请求处理的尾声了。FinalRequestProcessor处理器会首先检查outstandingChanges队列中请求的有效性,如果发现这些请求已经落后于当前正在处理的请求,那么直接从outstandingChanges队列中移除。

事务应用

        在之前的请求逻辑中,我们仅仅是将该事务请求记录到了事务日志中去,而内存数据库中的状态尚未变更。因此,在这个环节,我们需要将事务变更应用到内存数据库中。但是需要注意的一点是,对于“会话创建”这类事务请求,ZooKeeper做了特殊处理——因为在ZooKeeper内存中,会话的管理都是由SessionTracker负责的,而在会话创建的步骤9中,ZooKeeper已经将会话信息注册到了SessionTracker中,因此此处无须对内存数据库做任何处理,只需要再次向SessionTracker进行会话注册即可。

将事务请求放入队列:commitProposal

        一旦完成事务请求的内存数据库应用,就可以将该请求放入commitProposal队列中。commitProposal队列用来保存最近被提交的事务请求,以便集群间机器进行数据的快速同步。

会话响应

        客户端请求在经过ZooKeeper服务端处理链路的所有请求处理器的处理后,就进入最后的话响应阶段了。会话响应阶段非常简单,大体分为以下4个步骤。

统计处理

        至此,客户端的“会话创建”请求已经从ZooKeeper请求处理链路上的所有请求处理器间完成了流转。到这一步,ZooKeeper会计算请求在服务端处理所花费的时间,同时还会统计客户端连接的一些基本信息,包括lastZxid(最新的ZXID)、lastOp(最后一次和服务端的操作)和lastLatency(最后一次请求处理所花费的时间)等。

创建响应ConnectResponse

        ConnectResponse就是一个会话创建成功后的响应,包含了当前客户端与服务端之间的通信协议版本号protocolVersion、会话超时时间、sessionID和会话密码。

序列化ConnectResponse

I/O层发送响应给客户端

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值