分布式学习手记09 - ZooKeeper 核心技术(下)
ZooKeeper 的核心技术细节第三部分,包括:请求处理、数据与存储。
请求处理
ZooKeeper 如何处理客户端发起的一次请求
会话创建请求
ZooKeeper 服务端对于会话创建的处理,分为请求接收、会话创建、预处理、事务处理、事务应用和会话响应 6 个环节
请求接收
-
I/O 层接收来自客户端的请求
由 NIOServerCnxn 负责接收来自客户端的所有请求,并将请求内容从底层网络 I/O 中完整读取出来
-
判断是否是客户端 “会话创建” 请求
请求对应的 NIOServerCnxn 实体若没被初始化,则该请求是 “会话创建” 请求
-
反序列化 ConnectRequest 请求
对此会话创建请求反序列化,生成一个 ConnectRequest 实体
-
判断是否是 ReadyOnly 客户端
-
检查客户端 ZXID
若客户端 ZXID 大于服务端 ZXID,拒绝该请求
-
协商 sessionTimeout
-
判断是否需要重新创建会话
若客户端请求已包含 sessionID,确定为会话重连,重新打开会话,否则需要重新创建会话
会话创建
-
为客户端生成 sessionID
由会话管理器 SessionTracker 为每个会话维护全局唯一的 “基准 sessionID”,以此递增
-
向 SessionTracker 注册会话
sessionsWithTimeout 根据 sessionID 保存所有会话超时时间;
sessionsById 根据 sessionID 保存所有会话实体
-
激活会话
-
生成会话密码
预处理
-
请求提交给 PrepRequestProcessor 处理
-
创建请求事务头
-
创建请求事务体
-
注册与激活会话
事务处理
-
请求交给 ProposalRequestProcessor 处理
-
Sync 流程
由 SyncRequestProcessor 记录事务日志
-
Proposal 流程
事务请求需通过半数以上的节点投票,投票与统计的过程即 Proposal 流程
-
Leader 服务器发起事务投票
-
生成提议 Proposal
-
广播提议
-
收集投票
-
将请求放入 toBeApplied 队列
-
广播 COMMIT 消息
-
-
Commit 流程
-
请求交付给 CommitProcessor
先不处理,存入 queuedRequests 队列
-
处理 queuedRequests 队列中的请求
-
标记 nestPending
-
等待 Proposal 投票
-
投票通过
-
提交请求
-
-
事务应用
-
交付给 FinalRequestProcessor
-
事务应用
将事务变更应用到内存数据库
-
将事务请求放入 commitProposal 队列
commitProposal 队列保存最近被提交的事务请求,以便集群内机器进行数据的快速同步
会话响应
-
统计处理
-
创建响应 ConnectResponse
ConnectResponse 是会话创建成功后的响应
-
序列化 ConnectResponse
-
I/O 层发送响应给客户端
SetData 请求
客户端通过 SetData 接口来更新 ZooKeeper 服务器上数据节点的内容
预处理
-
I/O 层接收来自客户端的请求
-
判断是否是 “会话创建” 请求
ZooKeeper 会对收到的每一个客户端请求先判断是不是会话创建请求
-
请求交给 PrepRequestProcessor
-
创建请求事务头
-
会话检查
检查会话是否有效有效未超时
-
反序列化请求,创建 ChangeRecord
将请求反序列化,生成 SetDataRequest 请求
生成 ChangeRecord,存入 outstandingChanges 事务变更队列
-
ACL 权限检查
客户端是否具有数据更新的权限
-
数据版本(version)检查
-
创建请求事务体 SetDataTxn
-
保存事务操作到 outstandingChanges 队列
事务处理
类似上节 “会话创建” 的事务处理:由 ProposalRequestProcessor 发起,通过 Sync、Proposal 和 Commit 三个子流程协作完成
事务应用
-
交付给 FinalRequestProcessor
-
事务应用
事务头、事务体交由内存数据库 ZKDatabase 进行事务应用
-
将事务请求放入 commitProposal 队列
请求响应
-
统计处理
-
创建响应体 SetDataResponse
SetDataResponse 是数据更新成功后的响应,包含当前数据节点的最新状态 stat
-
创建响应头
-
序列化响应
-
I/O 层发送响应给客户端
事务请求转发
为保证事务请求被顺序执行,从而确保 ZooKeeper 集群的数据一致性,所有事务请求必须由 Leader 服务器处理。
Follower 或 Observer 如果接到了来自客户端的事务请求,必须转发给 Leader 服务器处理。
GetData 请求
GetData 请求是 “非事务请求”,省去了许多事务请求的处理逻辑
预处理
-
I/O 层接收来自客户端的请求
-
判断是否是客户端 “会话创建” 请求
-
将请求交给 PrepRequestProcessor
-
会话检查
非事务处理
-
反序列化 GetDataRequest 请求
-
获取数据节点
根据 GetDataRequest 包含的内容,从 ZKDatabase 获取该节点及其 ACL 信息
-
ACL 检查
-
获取数据内容和 stat,注册 Watcher
请求响应
-
创建响应体 GetDataResponse
GetDataResponse 包含当前数据节点的内容和状态 stat
-
创建响应头
-
统计处理
-
序列化响应
-
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 位则是操作序列号
-
日志格式
略
-
日志写入
-
确定是否有事务日志文件可写
第一次事务日志写入,或上一个事务日志写满时,需要创建新的事务日志文件
-
确定事务日志文件是否需要扩容(预分配)
客户端每一次事务操作,ZooKeeper 都会将其写入事务日志文件
因此,为提高事务请求响应速度,预先用 “0” 填充文件空间(预分配),避免反复磁盘 Seek,提高写入性能
-
事务序列化
-
生成 Checksum
-
写入事务日志文件流
-
事务日志刷入磁盘
-
-
日志截断
若某台机器事务 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 次” 事务日志记录后产生一个数据快照
数据快照的过程:
-
确定是否进行数据快照
“过半随机” 策略,分散快照时机,避免集群并发压力
logCount:代表当前已经记录的事务日志数量,当满足如下随机临界值后,进行快照
logCount > ( snapCount / 2 + randRoll )
randRoll:小于 snapCount / 2 的随机数
-
切换事务日志文件
检测当前事务日志文件是否已写满
-
创建数据快照异步线程
创建单独的异步线程进行数据快照,避免影响主线程
-
获取全量数据和会话信息
从 ZKDatabase 获取 DataTree 和会话信息
-
生成快照文件名
根据已提交的最大 ZXID 生成数据快照文件名
-
数据序列化
-
初始化
ZooKeeper 启动期间,首先进行数据初始化工作,将存储在硬盘上的数据加载到内存数据库中
包括 “从快照文件中加载快照数据” 和 “根据事务日志进行数据订正”
初始化流程:
服务器启动
-
初始化 FileTxnSnapLog
FileTxnSnapLog 数据访问层,用于衔接上层业务与底层数据存储,包含两部分:
FileTxnLog - 事务日志管理器
FileSnap - 快照数据管理器
-
初始化 ZKDatabase
-
创建 PlayBackListener 监听器
用于接收事务应用过程中的回调,在数据恢复后期负责事务数据订正
处理快照文件
-
获取最新的 100 个快照文件
每一个快照文件都是全量数据,所以最晚生成的快照文件包含最新的全量数据
-
解析快照文件
从最晚生成的快照文件开始解析,解析失败则前推一个处理
-
获取最新的 ZXID
根据快照恢复出的最新的 ZXID 称为 zxid_for_snap,但不包含应用到内存数据库但尚未形成快照的事务记录
处理事务日志
-
获取所有 zxid_for_snap 之后提交的事务
从事务日志中获取尚未打入快照的事务
-
将 ZXID 比 zxid_for_snap 大的事务逐个应用
-
获取最新的 ZXID
到此基本将所有事务完整应用到了内存数据库中,完成了数据初始化过程
校验 epoch
- 比对校验 Leader 周期 epoch
数据同步
数据同步即 Leader 服务器将 “没在 Learner 服务器上提交过的事务请求” 同步给 Learner 服务器的过程
-
获取 Learner 状态
Leader 从 Learner 注册时发来的 ACKEPOCH 数据包中解析其状态(currentEpoch、lastZxid)
-
数据同步初始化
Leader 从内存数据库中提取出事务请求对应的提议缓存队列 proposals,确认三个参数:
-
peerLastZxid:该 Learner 服务器最后处理的 ZXID
-
minCommittedLog:Leader 服务器提议缓存队列中的最小 ZXID
-
maxCommittedLog:Leader 服务器提议缓存队列中的最大 ZXID
-
-
根据 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 服务器
-