【ZooKeeper】请求处理

雨点打透心脏的1/2处 2022-10-21 03:43 207阅读 0赞

1 会话创建请求

ZooKeeper 服务端对于会话创建的处理,大体可以分为请求接收、会话创建、预处理、 事务处理、事务应用和会话响应 6 大环节,其大体流程如图 7-39 所示。
在这里插入图片描述

请求接收

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

    在这里插入图片描述

  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 协商过程。

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

  7. 判断是否需要重新创建会话。
    服务端根据客户端请求中是否包含 sessionlD 来判断该客户端是否需要重新创建 会话。如果客户端请求中已经包含了 sessionlD, 那么就认为该客户端正在进行会 话重连。在这种情况下,服务端只需要重新打开这个会话,否则需要重新创建。会话创建
  8. 为客户端生成 sessionlD 。
    在为客户端创建会话之前,服务端首先会为每个客户端都分配一个 sessionlD 。分 配方式其实很简单,每个 ZooKeeper 服务器在启动的时候,都会初始化一个会话 管理器(SessionTracker), 同时初始化 sessionlD, 我们将其称为“基准 sessionlD” 。因此针对每个客户端,只需要在这个“基准 sessionlD” 的基础上进行逐个递增就 可以了。由于 sessionlD 是 ZooKeeper 会话的一个重要标识,许多与会话相关的运行机制都 是基于这个 sessionlD 的,因此,无论是哪台服务器为客户端分配的 sessionlD, 都务必保证全局唯一。在 ZooKeeper 中,是通过保证“基准 sessionlD” 的全局唯 一来确保每次分配的sessionlD 在集群内部都各不相同。因此,“基准 sessionlD” 的初始化算法非常重要,之前已经详细介绍了 ZooKeeper 的会话管理器是如何完成 sessionlD 的初始化工作的。
  9. 注册会话。
    创建会话最重要的工作就是向 SessionTracker 中注册会话。 SessionTracker 中维护了两个比较重要的数据结构,分别是 sessionsWithTimeout 和 sessionsByld 。前者根据 sessionlD保存了所有会话的超时时间,而后者则是根据 sessionlD 保存了所有会话实体。 在会话创建初期,就应该将该客户端会话的相关信息保存到这两个数据结构中,方便后 续会话管理器进行管理。
  10. 激活会话。
    向 SessionTracker 注册完会话后,接下来还需要对会话进行激活操作。激活会话 过程涉及 ZooKeeper 会话管理的分桶策略,已经进行了详细讲解, 这里就不再赘述。此处,读者需要了解的就是,激活会话的核心是为会话安排一 个区块,以便会话清
    理程序能够快速高效地进行会话清理。
  11. 生成会话密码。
    服务端在创建一个客户端会话的时候,会同时为客户端生成一个会话密码,连同sessionlD 一起发送给客户端,作为会话在集群中不同机器间转移的凭证。会话 密码的生成算法非常简单,如下:

    static final private long superSecret = 0XB3415C00L;

    1. Random r = new Random(sessionld superSecret);
    2. r.nextBytes(passwd);

预处理

  1. 将请求交给 ZooKeeper 的 PrepRequestProcessor 处理器进行处理。
    ZooKeeper 对于每个客户端请求的处理模型采用了典型的责任链模式一每个客 户端请求都会由几个不同的请求处理器依次进行处理。
    另外,在提交给第一个请求处理器前, ZooKeeper 还会根据该请求所属的会话, 进行一次激活会话操作,以确保当前会话处于激活状态。完成会话激活之后, ZooKeeper 就会将请求提交给第一个请求处理器: PrepRequestProcessor.
  2. 创建请求事务头。
    对于事务请求, ZooKeeper 首先会为其创建请求事务头。请求事务头是每一个 ZooKeeper 事务请求中非常重要的一部分,服务端后续的请求处理器都是基于该 请求头来识别当前请求是否是事务请求。请求事务头包含了一个事务请求最基本 的一些信息,包括sessionlD 、 ZXID, CXID 和请求类型等,如表所示。
    在这里插入图片描述
  3. 创建请求事务体。
    对于事务请求, ZooKeeper 还会为其创建请求的事务体。在此处由于是“会话创 建”请求,因此会创建事务体 CreateSessionTxn 。
  4. 注册与激活会话
    此处的注册与激活会话过程,和上面步骤 9 中提到的过程是一致的,虽然重复了, 但是读者可以放心,不会引起额外的问题。此处进行会话注册与激活的目的是处 理由非Leader 服务器转发过来的会话创建请求。在这种情况下,其实尚未在 Leader 的SessionTracker 中进行会话的注册,因此需要在此处进行一次注册与激活。

事务处理

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

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

    • Sync 流程

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

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

    • Proposal 流程

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

    • (1) 发起投票(ProposalRequestProcessor)
      如果当前请求是事务请求,那么 Leader 服务器就会发起一轮事务投票。在发 起事务投票之前,首先会检查当前服务端的 ZXID 是否可用。关于 ZooKeeper 的 ZXID 可用性检查,如果当前服务端的 ZXID 不可用,那么将会抛出 XidRolloverException 异常。
    • (2) 生成提议 Proposal(ProposalRequestProcessor)
      如果当前服务端的 ZXID 可用,那么就可以开始事务投票了。 ZooKeeper 会将 之前创建的请求头和事务体,以及 ZXID 和请求本身序列化到 Proposal 对象中 此处生成的Proposal 对象就是一个提议,即针对 ZooKeeper 服务器状态的一次变更申请。
    • (3) 广播提议(ProposalRequestProcessor)
      生成提议后, Leader 服务器会以 ZXID 作为标识,将该提议放入投票箱 outstandingProposals 中,同时会将该提议广播给所有的 Follower 服务器。
    • (4) 收集投票(CommitProcessor)
      Follower 服务器在接收到 Leader 发来的这个提议后,会进入 Sync 流程来进行 事务日志的记录,一旦日志记录完成后,就会发送 ACK 消息给 Leader 服务器, Leader 服务器根据这些 ACK 消息来统计每个提议的投票情况。当一个提议获得了集群中过半机器的投票,那么就认为该提议通过,接下去就 可以进入提议的 Commit 阶段了。
    • (5) 将请求放入 toBeApplied 队列(ToBeCommitProcessor)
      在该提议被提交之前, ZooKeeper 首先会将其放入 toBeApplied 队列中去。
    • (6) 广播 COMMIT 消息(FinalRequestProcessor )
      一旦 ZooKeeper 确认一个提议已经可以被提交了,那么 Leader 服务器就会向 Follower和 Observer 服务器发送 COMMIT 消息,以便所有服务器都能够提交 该提议。这里需要注意的一点是,由于 Observer 服务器并未参加之前的提议 投票,因此 Observer 服务器尚未保存任何关于该提议的信息,所以在广播 COMMIT 消息的时候,需要区别对待,Leader 会向其发送一种被称为“ INFORM” 的消息,该消息体中包含了当前提议的内容。而对于 Follower 服务器,由于 已经保存了所有关于该提议的信息,因此 Leader 服务器只需要向其发送 ZXID 即可。

      • Commit 流程(CommitProcessor)
    • (1) 将请求交付给 CommitProcessor 处理器。
      CommitProcessor 处理器在收到请求后,并不会立即处理,而是会将其放 入queuedRequests 队列中。

    • (2) 处理 queuedRequests 队列请求。
      CommitProcessor 处理器会有一个单独的线程来处理从上一级处理器流转 下来的请求。当检测到 queuedRequests 队列中已经有新的请求进来,就会 逐个从队列中取出请求进行处理。
    • (3) 标记 nextPending 。
      如果从 queuedRequests 队列中取出的请求是一个事务请求,那么就需要进 行集群中各服务器之间的投票处理,同时需要将 nextPending 标记为当前 请求。标记 nextPending的作用,一方面是为了确保事务请求的顺序性, 另一方面也是便于 CommitProcessor 处理器检测当前集群中是否正在进行 事务请求的投票。
    • (4) 等待 Proposal 投票。
      在 Commit 流程处理的同时, Leader 已经根据当前事务请求生成了一个提议 Proposal,并广播给了所有的 Follower 服务器。因此,在这个时候, Commit 流程需要等待,直到投票结束。
    • (5) 投票通过。
      如果一个提议已经获得了过半机器的投票认可,那么将会进入请求提交阶段。ZooKeeper 会将该请求放入 committedRequests 队列中,同时唤醒 Commit 流程。
    • (6) 提交请求。
      一旦发现 committedRequests 队列中已经有可以提交的请求了,那么 Commit 流程就会开始提交请求。当然在提交以前,为了保证事务请求的顺序 执行, Commit 流程还会对比之前标记的 nextPending 和 committedRequests 队列中第一个请求是否一致。如果检查通过,那么 Commit 流程就会将该请求放入 toProcess 队列中(交付给 ToBeCommitProcessor),然 后交付给下一个请求处理器: FinalRequestProcessor 。

事务应用

  1. 交付给 FinalRequestProcessor 处理器。
    请求流转到 FinalRequestProcessor 处理器后,也就接近请求处理的尾声了。FinalRequestProcessor 处理器会首先检查 outstandingChanges 队列 中请求的有效性,如果发现这些请求已经落后于当前正在处理的请求,那么直接 从 outstandingChanges 队列中移除。
  2. 事务应用。
    在之前的请求处理逻辑中,我们仅仅是将该事务请求记录到了事务日志中去,而 内存数据库中的状态尚未变更。因此,在这个环节,我们需要将事务变更应用到 内存数据库中。但是需要注意的一点是,对于“会话创建”这类事务请求, ZooKeeper 做了特殊处理——- 因为在 ZooKeeper 内存中,会话的管理都是由SessionTracker 负责的,而在会话创建的步骤 9 中, ZooKeeper 已经将会话信息 注册到了SessionTracker 中 , 因此 此 处 无须 对 内存 数 据 库做 任何 处 理 , 只需 要 再 次 向SessionTracker 进行会话注册即可。
  3. 将事务请求放入队列: commitProposal 。
    一旦完成事务请求的内存数据库应用,就可以将该请求放入 commitProposal 队列中。commitProposaI 队列用来保存最近被提交的事务请求,以便集群间机器进行数据的快速同步。

会话响应

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

  1. 统计处理。
    至此,客户端的“会话创建”请求已经从 ZooKeeper 请求处理链路上的所有请求 处理器间完成了流转。到这一步, ZooKeeper 会计算请求在服务端处理所花费的 时间,同时还会统计客户端连接的一些基本信息,包括 lastZxid (最新的 ZXID), lastOp (最后一次和服务端的操作)和 lastLatency (最后一次请求处理所花费的 时间)等。
  2. 创建响应 ConnectResponse
    ConnectResponse 就是一个会话创建成功后的响应,包含了当前客户端与服务端 之间的通信协议版本号 protocol Version, 会话超时时间、 sessionlD 和会话密码。
  3. 序列化 ConnectResponse
  4. I/O 层发送响应给客户端。

2 SetData 请求

服务端对于SetData 请求的处理,大体可以分为 4 大步骤,分别是请求的预处理、 事务处理、事务应用和请求响应,如图所示。

整个事务请求的处理流程和 7.8.1 节中会话创建请求的处理流程非常相近,尤其是事务 处理的投票部分,是完全一致的。因此,对于那些重复的处理步骤,在本节中将不会重 点展开讲解。
在这里插入图片描述

预处理

  1. I/O 层接收来自客户端的请求。
  2. 判断是否是客户端“会话创建”请求。
    ZooKeeper 对于每一个客户端请求,都会检查是否是“会话创建”请求。如果确 实是“会话创建”请求,那么就按照“会话创建”请求处理流程 执行。然而对于SetData 请求,因为此时已经完成了会话创建,因此按照正常 的事务请求进行处理。
  3. 将请求交给 ZooKeeper 的 PrepRequestProcessor 处理器进行处理。
  4. 创建请求事务头。
  5. 会话检查。
    客户端会话检查是指检查该会话是否有效,即是否已经超时。如果该会话已经超 时,那么服务端就会向客户端抛出 SessionExpiredException 异常。
  6. 反序列化请求,并创建 ChangeRecord 记录。
    面对客户端请求, ZooKeeper 首先会将其进行反序列化并生成特定的 SetDataRequest 请求。 SetDataRequest 请求中通常包含了数据节点路径 path, 更新的数据内容 data 和期望的数据节点版本 version 。同时,根据请 求中对应的 path, ZooKeeper 会生成一个 ChangeRecord 记录,并放入 outstandingChanges 队列中。

    outstandingChanges 队列中存放了当前 ZooKeeper 服务器正在进行处理的事 务请求,以便ZooKeeper 在处理后续请求的过程中需要针对之前的客户端请求的 相关处理,例如对于“会话关闭”请求来说,其需要根据当前正在处理的事务请 求来收集需要清理的临时节点。

  7. ACL 检查。
    由于当前请求是数据更新请求,因此 ZooKeeper 需要检查该客户端是否具有数据 更新的权限。如果没有权限,那么会抛出 NoAuthException 异常。关于 ZooKeeper 的 ACL 权限控制,已经做了详细讲解。
  8. 数据版本检查。
    我们已经讲解了 ZooKeeper 可依靠 version 属性来实现乐观锁机 制中的“写入校验”。如果 ZooKeeper 服务端发现当前数据内容的版本号与客户端 预期的版本不匹配的话,那么将会抛出异常。
  9. 创建请求事务体 SetDataTxn 。
  10. 保存事务操作到 outstandingChanges 队列中去。

事务处理

对于事务请求, ZooKeeper 服务端都会发起事务处理流程。无论对于会话创建请求还是 SetData 请求,或是其他事务请求,事务处理流程都是一致的,都是由 ProposalRequestProcessor 处理器发起,通过 Sync 、 Proposal 和 Commit 三个子 流程相互协作完成的。

事务应用

  1. 交付给 FinalRequest Processor 处理器。
  2. 事务应用。
    ZooKeeper 会将请求事务头和事务体直接交给内存数据库 ZKDatabase 进行事务 应用,同时返回 ProcessTxnResuIt 对象,包含了数据节点内容更新后的 stat 。
  3. 将事务请求放入队列: commitProposal 。

请求响应

  1. 统计处理。
  2. 创建响应体 SetDataResponse 。
    SetDataResponse 是一个数据更新成功后的响应,主要包含了当前数据节点的 最新状态 stat 。
  3. 创建响应头。
    响应头是每个请求响应的基本信息,方便客户端对响应进行快速的解析,包括当 前响应对应的事务 ZXID 和请求处理是否成功的标识 err 。
  4. 序列化响应。
  5. I/O 层发送响应给客户端。

3 事务请求转发

在事务请求的处理过程中,需要我们注意的一个细节是,为了保证事务请求被顺序执行, 从而确保 ZooKeeper 集群的数据一致性,所有的事务请求必须由 Leader 服务器来处理。 但是,相信读者很容易就会发现一个问题,并不是所有的 ZooKeeper 都和 Leader 服务器 保持连接,那么如何保证所有的事务请求都由 Leader 来处理呢?

ZooKeeper 实现了非常特别的事务请求转发机制:所有非 Leader 服务器如果接收到了来 自客户端的事务请求,那么必须将其转发给 Leader 服务器来处理。

在 Follower 或是 Observer 服务器中,第一个请求处理器分别是 FollowerRequestProcessor 和ObserverRequestProcessor, 无论是哪个处理器,都会检查当前请求是否是事务请求, 如果是事务请求,那么就会将该客户端请求以 REQUEST 消息的形式转发给 Leader 服务器。 Leader 服务器在接收到这个消息后,会解析出客户端的原始请求,然后提交到自己的请求处理 链中开始进行事务请求的处理。

4 GetData 请求

服务端对于 GetData 请求的处理,大体可以分为 3 大步骤,分别是请求的预处理、非事务处理和请求响应,如图 7-42 所示。
在这里插入图片描述

预处理

  1. I/O层接收来自客户端的请求。
  2. 判断是否是客户端“会话创建”请求。
  3. 将请求交给 ZooKeeper 的 PrepRequestProcessor 处理器进行处理。
  4. 会话检查。
    由于 GetData 请求是非事务请求,因此省去了许多事务预处理逻辑,包括创建请求事 务头、ChangeRecord 和事务体等,以及对数据节点版本的检查。

非事务处理

  1. 反序列化 GetDataRequest 请求。
  2. 获取数据节点。
    根据步骤 5 中反序列化出的完整 GetDataRequest 对象(包括了数据节点的 path 和Watcher 注册情况), ZooKeeper 会从内存数据库中获取到该节点及其 ACL 信息。
  3. ACL 检查。
  4. 获取数据内容和 stat, 注册 Watcher 。
    此处所说的注册 Watcher 和讲解的客户端 Watcher 的注册过程是一致的。

请求响应

  1. 创建响应体 GetDataResponse 。
    GetDataResponse 是一个数据获取成功后的响应,主要包含了当前数据节点的 内容和状态stat 。
  2. 创建响应头。
  3. 统计处理。
  4. 序列化响应。
  5. I/O 层发送响应给客户端。

发表评论

表情:
评论列表 (有 0 条评论,207人围观)

还没有评论,来说两句吧...

相关阅读