分布式学习手记09 - ZooKeeper 核心技术(下)

2 分钟读完

ZooKeeper 的核心技术细节第三部分,包括:请求处理、数据与存储。

请求处理

ZooKeeper 如何处理客户端发起的一次请求

会话创建请求

ZooKeeper 服务端对于会话创建的处理,分为请求接收、会话创建、预处理、事务处理、事务应用和会话响应 6 个环节

请求接收

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

    由 NIOServerCnxn 负责接收来自客户端的所有请求,并将请求内容从底层网络 I/O 中完整读取出来

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

    请求对应的 NIOServerCnxn 实体若没被初始化,则该请求是 “会话创建” 请求

  3. 反序列化 ConnectRequest 请求

    对此会话创建请求反序列化,生成一个 ConnectRequest 实体

  4. 判断是否是 ReadyOnly 客户端

  5. 检查客户端 ZXID

    若客户端 ZXID 大于服务端 ZXID,拒绝该请求

  6. 协商 sessionTimeout

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

    若客户端请求已包含 sessionID,确定为会话重连,重新打开会话,否则需要重新创建会话

会话创建

  1. 为客户端生成 sessionID

    由会话管理器 SessionTracker 为每个会话维护全局唯一的 “基准 sessionID”,以此递增

  2. 向 SessionTracker 注册会话

    sessionsWithTimeout 根据 sessionID 保存所有会话超时时间;

    sessionsById 根据 sessionID 保存所有会话实体

  3. 激活会话

  4. 生成会话密码

预处理

  1. 请求提交给 PrepRequestProcessor 处理

  2. 创建请求事务头

  3. 创建请求事务体

  4. 注册与激活会话

事务处理

  1. 请求交给 ProposalRequestProcessor 处理

    • Sync 流程

      由 SyncRequestProcessor 记录事务日志

    • Proposal 流程

      事务请求需通过半数以上的节点投票,投票与统计的过程即 Proposal 流程

      1. Leader 服务器发起事务投票

      2. 生成提议 Proposal

      3. 广播提议

      4. 收集投票

      5. 将请求放入 toBeApplied 队列

      6. 广播 COMMIT 消息

    • Commit 流程

      1. 请求交付给 CommitProcessor

        先不处理,存入 queuedRequests 队列

      2. 处理 queuedRequests 队列中的请求

      3. 标记 nestPending

      4. 等待 Proposal 投票

      5. 投票通过

      6. 提交请求

事务应用

  1. 交付给 FinalRequestProcessor

  2. 事务应用

    将事务变更应用到内存数据库

  3. 将事务请求放入 commitProposal 队列

    commitProposal 队列保存最近被提交的事务请求,以便集群内机器进行数据的快速同步

会话响应

  1. 统计处理

  2. 创建响应 ConnectResponse

    ConnectResponse 是会话创建成功后的响应

  3. 序列化 ConnectResponse

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

SetData 请求

客户端通过 SetData 接口来更新 ZooKeeper 服务器上数据节点的内容

预处理

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

  2. 判断是否是 “会话创建” 请求

    ZooKeeper 会对收到的每一个客户端请求先判断是不是会话创建请求

  3. 请求交给 PrepRequestProcessor

  4. 创建请求事务头

  5. 会话检查

    检查会话是否有效有效未超时

  6. 反序列化请求,创建 ChangeRecord

    将请求反序列化,生成 SetDataRequest 请求

    生成 ChangeRecord,存入 outstandingChanges 事务变更队列

  7. ACL 权限检查

    客户端是否具有数据更新的权限

  8. 数据版本(version)检查

  9. 创建请求事务体 SetDataTxn

  10. 保存事务操作到 outstandingChanges 队列

事务处理

类似上节 “会话创建” 的事务处理:由 ProposalRequestProcessor 发起,通过 Sync、Proposal 和 Commit 三个子流程协作完成

事务应用

  1. 交付给 FinalRequestProcessor

  2. 事务应用

    事务头、事务体交由内存数据库 ZKDatabase 进行事务应用

  3. 将事务请求放入 commitProposal 队列

请求响应

  1. 统计处理

  2. 创建响应体 SetDataResponse

    SetDataResponse 是数据更新成功后的响应,包含当前数据节点的最新状态 stat

  3. 创建响应头

  4. 序列化响应

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

事务请求转发

为保证事务请求被顺序执行,从而确保 ZooKeeper 集群的数据一致性,所有事务请求必须由 Leader 服务器处理。

Follower 或 Observer 如果接到了来自客户端的事务请求,必须转发给 Leader 服务器处理。

GetData 请求

GetData 请求是 “非事务请求”,省去了许多事务请求的处理逻辑

预处理

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

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

  3. 将请求交给 PrepRequestProcessor

  4. 会话检查

非事务处理

  1. 反序列化 GetDataRequest 请求

  2. 获取数据节点

    根据 GetDataRequest 包含的内容,从 ZKDatabase 获取该节点及其 ACL 信息

  3. ACL 检查

  4. 获取数据内容和 stat,注册 Watcher

请求响应

  1. 创建响应体 GetDataResponse

    GetDataResponse 包含当前数据节点的内容和状态 stat

  2. 创建响应头

  3. 统计处理

  4. 序列化响应

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

数据与存储

包括内存数据存储和硬盘数据存储

内存数据

  • DataTree

    DataTree 是一个树的数据结构,代表了内存中一份完整的数据

  • DataNode

    DataNode 是数据存储的最小单位,保存了节点的数据内容、ACL 列表和节点状态 stat;

    以及父节点的引用和子节点列表;

    还有对子节点列表操作的各个接口

  • nodes

    nodes 这个 Map 中存放了 ZooKeeper 服务器上所有的数据节点

           |- KEY   : path 数据节点的路径
    nodes -|
           |- VALUE : DataNode 节点的数据内容
    
  • ZKDatabase

    ZooKeeper 的内存数据库,管理所有会话、DataTree 存储和事务日志;

    定时向磁盘 dump 快照数据;

    集群启动时,通过磁盘上的事务日志和快照恢复完整的内存数据库

事务日志

  • 文件存储

    示例

    ... 67108880 02-23 16:10 log.2c01631713
    ... 67108880 02-23 16:10 log.2d0166a224
    

    后缀是写入该事务日志文件第一条事务记录的 ZXID:高 32 位代表当前 Leader 周期(epoch),低 32 位则是操作序列号

  • 日志格式

  • 日志写入

    1. 确定是否有事务日志文件可写

      第一次事务日志写入,或上一个事务日志写满时,需要创建新的事务日志文件

    2. 确定事务日志文件是否需要扩容(预分配)

      客户端每一次事务操作,ZooKeeper 都会将其写入事务日志文件

      因此,为提高事务请求响应速度,预先用 “0” 填充文件空间(预分配),避免反复磁盘 Seek,提高写入性能

    3. 事务序列化

    4. 生成 Checksum

    5. 写入事务日志文件流

    6. 事务日志刷入磁盘

  • 日志截断

    若某台机器事务 ID(peerLastZxid) 比 Leader 要大,则环境异常;

    Leader 向其发送 TRUNC 命令(回滚),要求其进行日志截断;

    Learner 收到命令后,删除所有包含或大于此 ID 的事务日志文件

数据快照 snapshot

数据快照用来记录 ZooKeeper 服务器上某一时刻的全量内存数据内容,将其写入磁盘文件

  • 文件存储

    形如

    ... 1258072 02-23 16:10 snapshot.2c021384ce
    ... 1258096 02-23 16:10 snapshot.2c0214dd50
    

    后缀为本次数据快照开始时刻的服务器最新 ZXID

    快照数据文件没有采用 “预分配” 机制,能真实反映当前内存中全量数据大小

  • 存储格式

  • 数据快照

    snapCount 参数配置每次数据快照之间的事务操作次数,即 ZooKeeper 会在 “snapCount 次” 事务日志记录后产生一个数据快照

    数据快照的过程:

    1. 确定是否进行数据快照

      “过半随机” 策略,分散快照时机,避免集群并发压力

      logCount:代表当前已经记录的事务日志数量,当满足如下随机临界值后,进行快照

      logCount > ( snapCount / 2 + randRoll )

      randRoll:小于 snapCount / 2 的随机数

    2. 切换事务日志文件

      检测当前事务日志文件是否已写满

    3. 创建数据快照异步线程

      创建单独的异步线程进行数据快照,避免影响主线程

    4. 获取全量数据和会话信息

      从 ZKDatabase 获取 DataTree 和会话信息

    5. 生成快照文件名

      根据已提交的最大 ZXID 生成数据快照文件名

    6. 数据序列化

初始化

ZooKeeper 启动期间,首先进行数据初始化工作,将存储在硬盘上的数据加载到内存数据库中

包括 “从快照文件中加载快照数据” 和 “根据事务日志进行数据订正”

初始化流程:

服务器启动

  1. 初始化 FileTxnSnapLog

    FileTxnSnapLog 数据访问层,用于衔接上层业务与底层数据存储,包含两部分:

    FileTxnLog - 事务日志管理器

    FileSnap - 快照数据管理器

  2. 初始化 ZKDatabase

  3. 创建 PlayBackListener 监听器

    用于接收事务应用过程中的回调,在数据恢复后期负责事务数据订正

处理快照文件

  1. 获取最新的 100 个快照文件

    每一个快照文件都是全量数据,所以最晚生成的快照文件包含最新的全量数据

  2. 解析快照文件

    从最晚生成的快照文件开始解析,解析失败则前推一个处理

  3. 获取最新的 ZXID

    根据快照恢复出的最新的 ZXID 称为 zxid_for_snap,但不包含应用到内存数据库但尚未形成快照的事务记录

处理事务日志

  1. 获取所有 zxid_for_snap 之后提交的事务

    从事务日志中获取尚未打入快照的事务

  2. 将 ZXID 比 zxid_for_snap 大的事务逐个应用

  3. 获取最新的 ZXID

    到此基本将所有事务完整应用到了内存数据库中,完成了数据初始化过程

校验 epoch

  1. 比对校验 Leader 周期 epoch

数据同步

数据同步即 Leader 服务器将 “没在 Learner 服务器上提交过的事务请求” 同步给 Learner 服务器的过程

  1. 获取 Learner 状态

    Leader 从 Learner 注册时发来的 ACKEPOCH 数据包中解析其状态(currentEpoch、lastZxid)

  2. 数据同步初始化

    Leader 从内存数据库中提取出事务请求对应的提议缓存队列 proposals,确认三个参数:

    • peerLastZxid:该 Learner 服务器最后处理的 ZXID

    • minCommittedLog:Leader 服务器提议缓存队列中的最小 ZXID

    • maxCommittedLog:Leader 服务器提议缓存队列中的最大 ZXID

  3. 根据 Leader 和 Learner 之间数据差异情况进行不同的数据同步方式

    • 直接差异化同步(DIFF 同步)

      minCommittedLog < peerLastZxid < maxCommittedLog

    • 先回滚再差异化同步(TRUNC + DIFF 同步)

      minCommittedLog < peerLastZxid < maxCommittedLog

      当 Leader 发现某个 Learner 包含一条自己没有的事务记录,则让其先回滚到 Leader 上存在的且最接近 peerLastZxid 的 ZXID,再 DIFF 同步

    • 仅回滚同步(TRUNC 同步)

      peerLastZxid > maxCommittedLog

      Leader 要求 Learner 回滚到 maxCommittedLog 对应的事务

    • 全量同步(SNAP 同步)

      peerLastZxid < minCommittedLog

      Leader 将本机上的全量内存数据都同步给 Learner 服务器

分类:

更新时间: