前言
通过后面的一些文章的学习和理解,咱们对 Zookeeper 有了肯定的了解,然而无论是节点长久化,还是启动流程中的数据恢复等,咱们都没有具体的去理解外部的数据存储和复原的机制,本篇文章就开始学习 Zookeeper 的数据存储相干。
内存存储
zookeeper 刚开始的时候,咱们就曾经晓得其构造就像一个内存数据库一样,依照树的构造,能把节点的门路、节点数据以及 ACL 和节点的数据存储,其外围就是依附 DataTree 实现的所谓树型存储构造。而每一个 DataTree 外部蕴含了多个 DataNode,每一个DataNode 则是 zookeeper 中最小的存储单元。
DataTree 存储了 Zookeeper 中所有节点的门路,所有节点的数据以及 ACL 信息,除此之外,具体的每个节点存储依赖 DataNode,而管控所有的 node 节点应用的经典的ConcurrentHashMap 键值对构造:
private final ConcurrentHashMap<String, DataNode> nodes = new ConcurrentHashMap<String, DataNode>();
每一个 DataNode 外部除了存储对应的数据内容、ACL 列表和每个节点对应的状态以外,还会保留树的一些信息,例如节点的父节点援用,以及以后节点的子节点列表信息等,以此实现更不便的治理和实现树型构造。
在 nodes 这个 Map 中,存储了 Zookeeper 所有的数据结构,基本上所有的增删改等操作,都是操作 map 中对应 path 下的DataNode(key 是 path,value 为 DataNode),另外,在 Zookeeper 中,咱们晓得长期节点的体现和长久化节点不同,其生命周期和会话进行绑定,因而为了便于操作和清理,DataTree 中会独自将长期节点保存起来:
private final MapcLong, HashSet<String» ephemerals =
new ConcurrentHashMap<Long, HashSet<String»();
事务日志
除了内存存储以外,咱们晓得事务操作的时候会有日志,而文件存储次要是依附事务日志文件保留,在咱们启动 zookeeper 的时候,往往会指定 dataDir 目录,这个目录是 zookeeper 中默认用来存储事务日志的目录,除此之外咱们能够给事务日志独自调配目录寄存,只须要指定 dataLogDir 属性即可
日志文件
在 zookeeper 运行了一段时间当前,咱们查看日志目录下的文件,能够看到大略如下的列表:
而比拟值得注意的是这些文件的大小,都是一样的 67108880KB,这大小换算成 MB 刚好是 64MB 大小,除此之外,能够看到 log 文件的命名都是log. 作为前缀,前面的名字都是十六进制的数字,那么这个是什么呢?其实这个是应用了一个ZXID 作为后缀,而抉择的则是以后日志中的第一条事务的 ZXID,而ZXID 咱们后面也理解过,是由两个局部组合而成,高 32 位代表以后 Leader 的选举周期 –epoch 的大小,而低 32 位则是该周期内的操作序列号,因而咱们能够依据事务日志的名称疾速的解析读取进去对应的 epoch 信息和先后顺序。
咱们轻易抉择一个日志文件关上,发现外面的内容无奈浏览,都是序列化后的事务日志:
能够看到外面的内容只能隐约的看到一些节点的门路以外,其余的简直分辨不进去了,而在 zookeeper 中提供了一个格式化日志的命令 –org.apache.zookeeper.Server.LogFormatter,应用形式只须要在目录下输出:
Java LogFormatter 日志文件
咱们轻易找一个日志文件输出命令,看看格式化后的内容:
第一行日志:
ZooKeeper Transactional Log F ile with dbid 0 txnlog format version 2
能够看到这句日志是日志记录的开始,通知咱们日志的以后版本号是 2,以及以后的 dbid 是 0,接着咱们看下一行日志:
01:07:41 session 0x144699552020000 cxid 0x0 zxid 0x300000002 createSession 30000
而第二行从左到右别离记录了事务的产生工夫、以后事务的会话 id、客户端序列号 cxid、事务 id–zxid 以及以后触发事务的动作是创立操作,接着咱们来看第三行日志的内容:
01:08:40 session 0x144699552020000 cxid 0x2 zxid 0x300000003 create
/test_log,#7631,v{s{31 ,s{/w orld,'anyone}}},F,2
这一行日志咱们看到,不仅有和第二行记录一样的以外,还记录了节点的门路,节点的数据内容,这里须要留神的是这里记录的形式的 #+ 值的 ASSCII 的码值,节点的 ACL 信息以及是否为长期节点,这里应用了 F / T 形式记录,F 代表是长期节点,T 为长久化节点,以及版本号,基本上一个事务大体上记录的内容就这么多,其余的日志大体上和这些相似,因而不再具体介绍
FileTxnLog
FileTxnLog 负责保护事物日志相干的操作,包含事物日志的写入和读取以及数据恢复等。首先咱们来看事物写入的办法:
public synchronized boolean append(TxnHeader hdr, Record txn);
从办法的定义能够看进去,如果要写入日志,须要传入两个参数,别离是事物头和事物音讯体,而整个办法的大略过程如下:
1. 当整个 Zookeeper 启动实现后第一次进行日志的写入或者是上一次日志刚好写满当前,都会处于一个与日志文件断开的状态。因而,在进行日志写入之前,Zookeeper 会先判断 FileTxnLog 组件是否曾经关联一个事物日志文件,如果没有关联的日志文件,那么就会应用该事物关联的 ZXID 作为后缀创立一个新的事物日志文件,同时会去创立事物日志头信息(其中包含 magic, 事物日志的版本号 version 和 dbid),并且立刻写入到这个事物日志文件中去,而后将文件流存入一个汇合中 –StreamsToFlush。
2. 在客户端触发每一次的事物操作的时候,会进行一次空间大小检测操作,当发现事物日志的残余空间有余 4096 字节 (4KB) 大小的时候,就会进行一次扩容操作,而每一次扩容 (包含第一次调配大小) 都是 65536KB(64MB)大小,而这些扩容的内容,还没应用的状况下,会事后应用 0 进行占满,这里波及到一个 IO 性能优化的中央,如果 Zookeeper 不事后调配空间大小,可能会导致事物日志在写入的过程中,频繁的触发 Seek,开拓新的空间,导致写入 IO 性能迟缓。当然默认的预调配大小 64MB, 如果须要调节大小,能够设置零碎参数:
zookeeper.preAllocSize来扭转大小
3. 在写入事物之前,会进行一次事物序列化,别离是对 TxnHeader 和 Record 的序列化,其中包含创立会话事物、节点创立事物、删除节点事物和更新节点事物等,序列化实现当前,为了保障事物写入的完整性和准确性,会依据序列化生成的字节数组计算一个 Checksum,在 Zookeeper 中默认应用的是Adler32 算法 来计算 Checksum 值。
4. 将序列化后的事物头、事物体音讯以及 checkSum 的值一起写入到文件流中,此时应用的是 BufferedOutputStream,因而会期待缓存区填充斥当前才会真正的写入日志文件中,当事物日志写入到 BufferedOutputStream 当前,因为文件流都存入了 stramToFlush,因而咱们会从中提取文件流,并且调用 FileChannel.force(boolean metaData) 办法进行强制刷盘操作,至此 Zookeeper 的一次事物日志操作写入实现。
留神:在 Zookeeper 运行过程中,因为会呈现 leader 机器出现异常等状况,最初变成非 leader 机器,从新选举进去的 leader 发现非 leader 机器上记录的事物 ID 大于本身的,那么因为遵循后面文章说过的,Zookeeper 要求所有的 follower 机器在 Leader 存在的过程中,必须和 Leader 保持一致,因而这个时候 Leader 就会发送一个 TRUNC 命令给这个 follower 机器,强制对这部分日志进行截断,follower 机器在收到申请当前,会将这部分大于 Leader 事物 ID 的日志信息删除。
Snapshot
在 Zookeeper 中,除了事物日志以外,还有一个外围的数据存储组件 –Snapshot(数据快照),与事物日志不同的是,数据快照用于记录某一时刻的 zookeeper 上的全量数据内容,并且存入磁盘文件中。和事物日志雷同的一点是,数据快照也反对指定 dataDir 属性进行配置存储的目录,咱们关上对应的存储目录,查看一下快照文件的格局,如下:
-rw-rw-r-- 1 admin admin 1258072 03-01 17:49 snapshot.2c021384ce
能够看到和事物日志很像的一点是,快照的数据文件命名格局也是应用 ZXID 的十六进制作为文件后缀,同样的,在数据恢复的阶段,会依据 ZXID 来确定和进行数据恢复。当然与事物日志不同的是,快照文件并没有预调配空间的机制,因而也能够认为快照文件中的数据都是过后全量数据的无效数据。
当咱们关上一个快照文件当前,发现和事物日志差不多,外面的内容也是被序列化后的,当然,Zookeeper 也提供了一个格式化工具 org .apache.zookeeper.server.SnapshotFormatter,应用的形式也和后面的事物日志格式化工具差不多,在快照所在的目录下,应用如下命令:
Java SnapshotFormatte 快照
这个时候咱们再去读取内容,会发现,曾经能胜利看到每个节点的状态信息,尽管看不到具体的数据内容,然而曾经对咱们运维很有帮忙了,大略信息如下:
CZxid » 0x00000000000000
ctiffle » Thu Jan 01 08:00:00 C S T 1970
mZxid - OxOOOOOOGOOQOOOO
mtime = Thu Jttxi 01 08:0D:0 © C S T 1972
pZxid » 0*00000300000003
cversion = 2
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x00000000000000
dataLength = 0
而在 Zookeeper 中,负责快照相干操作的类是FileSnap,包含解决快照的写入和读取等操作。咱们晓得,Zookeeper 的每一次事物操作,都会写入到事物日志中,当然同时也会写入到内存数据库中,而在触发了屡次事物写入日志的操作当前,就会触发一次快照的数据写入操作,而这个次数 snapCount 参数则是能够在 zookeeper 参数中进行配置,接下来咱们来看看快照的大略写入过程:
1. 每一次事物日志写入结束当前,Zookeeper 都会检测一次是否须要写入到快照中的操作,实践上达到 snapCount 次数当前的事物日志就要触发快照的 demp 操作,然而思考整体性能,Zookeeper 并不是每一次都会执行 demp,而是抉择应用了过半随机的准则,即:
logCount > (snapCount /2 + randRoll)
这里的 logCount 指的是以后记录的日志数量,snapCount 指的是配置的多少次事物日志触发一次快照,randRoll 则是 1 – snapCount/ 2 之间的一个随机数,如果咱们配置的事物日志的数量为 10000,那么则会在一半 + 随机值的次事物日志当前才开始写入快照。
2. 当事物日志数量刚好达到半数随机值当前,Zookeeper 会进行一次事物日志文件切换(即事物日志曾经须要写入 snapCount 个事物日志), 须要从新创立一个新的事物日志文件进去,这个时候为了保障性能稳固,会创立一个独自的线程用来解决 demp 快照的操作
3. 而生成快照的过程则是将所有节点和会话信息保留到本地磁盘文件中,而文件的命名规定则是依据以后曾经提交的最大 ZXID 来生成数据快照文件名。接下来会进行序列化操作,首先序列化文件头信息,这里蕴含了 magic, 事物日志的版本号 version 和 dbid,而后再对会话信息和 DataTree 别离序列化,同样序列化实现后会生成一个 CheckSum,一并写入到快照文件中,至此快照文件写入实现
数据初始化与数据同步
后面咱们有学习过,Zookeeper 的启动流程,其中有两个步骤,一个是初始化启动的时候,会去磁盘中加载数据,另外一个则是集群启动后,会有 follower 机器与 leader 机器进行数据同步的过程,接下来咱们来看看这两个过程是如何进行数据之间的复原与同步的。
初始化数据
1. 在 Zookeeper 中,进行数据恢复或者数据同步应用的是 FileTxnSnapLog 类,这个类属于连接业务与上层数据存储的类,其中蕴含类事物日志的操作,以及快照操作,因而 FileTxnSnapLog 的初始化就是事物日志操作类 –FileTxnSnapLog和快照治理类 –FileSnap的初始化过程。
2. 在 FileTxnSnapLog 类初始化实现后,会将其交给 ZKDatabase,实现初始化操作,包含创立初始化的一些节点,例如 /,/zookeeper 和 /zookeeper/quota 节点,除此之外,还会创立 一个保留所有会话超时工夫的记录器 –sessionsWithTimeouts,初始化实现后,会去创立一个 PlayBackListener 监听器,这个监听器用来承受事务利用过程中的回调,会在数据恢复的过程中,进行数据修改操作。
3. 实现内存数据库的初始化当前,就要读取快照文件,进行全量数据恢复了,这个时候会默认读取最多一百个最新的快照文件,而后从 ZXID 最大的快照文件开始,进行一一解析,进行反序列化操作,而后生成 DataTree 和sessionWithTimeout,并且依据 checkSum 校验完整性,如果校验失败,会放弃这个快照文件,抉择第二个 ZXID 最大的快照文件,持续解析,顺次类推,如果读取到的最多一百个快照文件都失败了,那么就间接启动失败,如果有校验胜利的,则应用该文件进行全量复原。
4. 当快照文件复原全量数据实现后,此时曾经创立了 DataTree 实例和 sessionsWithTimeOuts 汇合了,这个时候咱们也晓得快照文件对应的最新的 ZXID,而这个时候咱们就须要找到比 snap 中的 ZXID 大的事物日志,进行增量复原和数据修改,每一条事务日志被复原后,就会利用到快照复原进去的 DataTree 和 sessionsWithTimeOuts 中,并且会回调 PlayBackListener 监听器,将这一
事务操作记录转换成 Proposal , 并保留到ZkDatabase.committedLog 中,以便 Follower 进行疾速同步操作。
5. 当事务日志复原结束后,数据的初始化过程根本完结,这个时候再去获取一个 ZXID,用来作为上次服务器失常阶段提交的最大事务 ID,这个时候依据 ZXID 解析进去上一次 leader 的周期 -epochOfZxid,同时在磁盘的 currentEpoch 和acceptedEpoch文件读取上次记录的 epoch 进行校验,至此数据初始化流程实现
数据同步
当 zookeeper 初始化实现后,集群选举后,Learner 服务器会向 Leader 实现注册当前,就会触发数据同步环节。在后面的文章中,咱们学习过,注册 Learner 的最初阶段,会发送给 Leader 服务器一个 ACKEPOCH 数据包,Leader 会依据发来的数据包解析进去 Learner 机器以后 currentEpoch 和lastZxid,接着 Leader 服务器会从 Zookeeper 内存数据库中提取出事务对应的提议缓存队列:proposals,同时实现对以下三个 ZXID 的初始化,别离是 peerLastZxid(Learner 服务器最初解决的 ZXID),minCommittedLog(提议缓存队列 CommittedLog 中的最小的 ZXID),maxCommittedLog(提议缓存队列 CommittedLog 中的最大的 ZXID)。而在 Zookeeper 中,数据同步有四类,别离是DIFF 差异化同步、 回滚后差异化同步 、 仅回滚同步 以及 全量 SNAP 同步。
全量同步
全量 SNAP 同步产生在以下两个场景,一个是 peerLastZxid 的值小于 minCommittedLog,另外一个是 Leader 服务器上不存在提议缓存队列的状况下,此时都无奈依据提议缓存队列进行同步,只能抉择全量同步。
仅回滚同步 / 回滚后差异化同步
这两种同步的形式都是针对 zookeeper 运行过程中 Leader 故障后从新选举复原的场景,惟一的区别在于,仅回滚同步针对的是 Leader 机器在故障前刚好把事物执行存储实现,然而却没来得及发送给其余 follow 机器的场景,这个时候 Leader 机器再次复原当前,身份不再是 Leader,然而却存在大于 Leader 机器的事物日志,这个时候就须要进行事务回滚操作。而回滚后差别同步则是针对的 Leader 故障后, 原来的 Leader 机器保留了事务日志,当机器从新注册到集群中,复原服务当前,后续选出来的 Leader 机器和其余的 follow 机器此时曾经进行了屡次事务同步,这个时候就须要先把原来的多余的那条事务日志删除后,再次进行差异化同步操作。
DIFF 差异化同步
个别进行差异化同步的场景是 zookeeper 应用过程中最常见的,这种同步的场景往往是产生在 peerLastZxid 介于 minCommittedLog 和 maxCommittedLog 之间。
在执行同步的过程中,首先 Leader 服务器会发送一个 DIFF 指令给所有的须要同步的 Learner 服务器,用于告诉差异化数据,而差异化数据则是通过 PROPOSAL 和COMMIT数据包实现,Leader 在发送完差异化数据当前,就会把 Learner 退出到 forwardingFollower 或者 observingLearners 队列中,并且这个时候 Leader 会发送一个 NEWLEADER 指令,用于告诉对应的 Learner,曾经将所有的缓存队列中的 Proposal 都同步过来了。同样的 Learner 服务器在收到了 DIFF 指令后,开启 DIFF 同步阶段,而后将收到的数据包,顺次的利用到内存数据库中,最初等到收到 Leader 发送的 NEWLEADER 指令后,代表 Leader 曾经全副发送结束,此时 Learner 会反馈一个 ACK 音讯,Leader 承受到 ACK 音讯 后,代表此时 Learner 服务器曾经承受完所有的同步数据,此时会持续期待其余的 Learner 的ACK 响应,直到集群中过半的 Learner 服务器都响应了为止。
至此,Zookeeper 曾经认为实现了数据同步操作,间接凋谢集群,提供对外的服务。