4. 数据与存储
ZkDatabase是ZK的内存数据库,其负责ZK的所有会话、DataTree存储和事务日志。其会定时向磁盘 dump 快照数据,同时在 ZK 启动的时候,会通过磁盘上的事务日志和快照数据恢复内存数据。
ZK 中的数据存储分为两部分:内存数据存储、磁盘数据存储。其中内存数据中最核心的及时DataTree,而磁盘数据包括 快照数据和事务日志。
内存数据
DataTree是一个“树”形结构,按路径形成一个树形结构。其内部结构如下:
1 | ConcurrentHashMap<String, DataNode> nodes = |
nodes用来保存所有path到节点的映射;
ephemerals来保存所有零时节点集合;
dataWatches和childWatches分别是监听器,当数据节点发生变化时触发相应的watch;
aclCache保存所有的访问控制信息。
DataNode的数据结构如下:
1 | byte data[]; |
其创建节点的逻辑如下:
1 | public void createNode(final String path, byte data[], List<ACL> acl, long ephemeralOwner, int parentCVersion, long zxid, long time, Stat outputStat) { |
磁盘数据
ZK中,FileTxnSnapLog是磁盘存储的核心类。
1 | public class FileTxnSnapLog { |
磁盘文件分为两部分:快照文件和事务日志文件。其中快照文件将定期保存内存数据库内容,事务日志则保存所有事务记录。
恢复时,首先选择一个可用的最近保存的快照文件进行恢复,其后再读取事务日志进行恢复。
下面重点来看看其 写入日志和数据快照的过程 和 服务启动恢复的过程:
日志写入
服务端负责日志写入的是SyncRequestProcessor。
其过程如下:
- 首先向事务日志中追加一条记录;
- 然后判断追加的记录数量是否大于
snapCount / 2 + randRoll(其中snapCount默认为100000,randRoll为随机数,之所以这么做是为了确保所有服务器不是同一时间做快照),如果大于则首先滚动日志文件(将FileTxnLog.logStream设置为null),然后起异步线程snapInProcess进行快照。 - 默认当积累的事务记录超过1000个则进行flush到磁盘。
下面分别来看追加事务记录和快照的过程:
- 追加事务记录:其最终会调用
FileTxnLog.append方法,其内部执行过程如下:- 首先判断
logStream==null,若是则根据请求的Zxid创建新的事务日志文件,并为文件预分配64MB的空间,填充0; - 其后序列化事务头和事务体,然后据此生成
CheckSum,写入文件中。
- 首先判断
- 快照:其最终会调用
ZookeeperServer.takeSnapshot方法:
1 | txnLogFactory.save(zkDb.getDataTree(), zkDb.getSessionWithTimeOuts(), syncSnap); |
跟据DataTree.lastProcessedZxid在snapDir目录下生成一个快照文件。然后序列化DataTree和Session存入文件中。
数据恢复
当服务器重启时,会调用ZkDatabase.loadDataBase进行数据恢复。
其最终会调用FileTxnSnapLog.restore方法:
- 首先找到最近可用的快照文件恢复
DataTree和Session;- 近可能多找出可用的100个快照文件,按文件名进行逆序排序;
- 选取最近的快照文件进行反序列化恢复
DataTree和Session,并做一次校验。
- 根据快照文件恢复后的
DataTree对应的lastProcessZxod获取事务记录,然后将事务应用到DataTree和Session上。(FileTxnSnapLog.fastForwardFromEdits) - 需要特别提出的是,在事务记录恢复的过程中,会回调
ZKDatabase.commitProposalPlaybackListener,将事务加入到committedLog中,并保存minCommitedLog和maxCommitedLog,其在选举后Learner与Leader见的数据同步过程中起着重要作用。
数据同步
在前面《选举算法与实现》章节中,提到完成选举后,Leader与各Learner会进行一次数据同步,有三种模式:DIFF、SNAP、TURNC。这里令Learner发送到Leader的Zxid为peerLastZxid。
- 在
peerLastZxid在minCommitedLog和maxCommitedLog之间时,执行DIFF操作,直接从Leader的ZkDataabse.committedLog中消费; - 当
peerLastZxid小于minCommitedLog,则进行全量数据同步; - 当
peerLastZxid大于maxCommitedLog,Learner则需要进行回退操作。