数据分类
整体分为3类
- 内存数据
- 磁盘数据
- 快照
- 事务日志
zk启动过程中,3类数据的关系
数据分类.png
内存数据
两个对象:DataTree 和 DataNode
内部数据结构.png
1、DataTree的内部数据结构,是ConcurrentHashMap。
key 为 节点路径
value 为 DataNode。DataNode是节点内容、ACL信息等
2、ephemerals有也是一个map。用于存储临时节点。那他的key和value是什么呢?
临时节点是跟session绑定的,sessionId作为key。value是节点路径list。
所以找节点,再去DataTree里去找。
事务日志
文件存储
zk的事务日志,都是记录在文件中的。
格式为:log.ZXID
事务日志文件有两个特点:
- 文件大小都是64M
- 后缀都是该文件中第一个事务的ZXID
使用第一个事务的ZXID来做文件名,因为事务ID是顺序的,所以文件名就可以当做索引了。
使用ZXID作为后缀的另一个优势是,ZXID本身由两部分组成,高32位代表当前Leader的周期(epoch),低32位则是真正的操作序列号。因此ZXID,可以清楚的知道现在处于哪个epoch。
日志内部的格式为二进制(乱码)。需要使用转换工具转换才能可读。记录所有事无记录。
日志写入
-
确定是否有事务日志可写
在进入事务日志写入之前判断是否已经关联到一个可写的事务日志文件。
如果关联了,直接写。
如果没有关联,是用该ZXID创建文件,然后写。
zk也是有一层缓冲的,不是直接写文件。这个缓冲是缓冲数据流:streamsToFlush。会有刷脏页策略刷脏页(配置文件配置)。 -
确定事务日志是否需要扩容(预分配)
zk的事务日志文件会采取“磁盘空间预分配策略”,什么意思呢?当检测到事务日志文件剩余空间不足4KB时,就会开始进行文件扩容。文件空间扩容的过程其实非常简单,就是在现有文件大小的基础上,将文件大小增加到64M(最大就是64M)。使用"0"填充这些被扩容的文件。
为什么要进行预分配。防止磁盘频繁seek(seek指的是磁盘臂寻址)。 -
事务序列化
分为事务头和事务体的序列化,序列化后最终生成一个字节数组。 -
生成checksum
为了保证事务日志的完整性和数据的准确性,zk在将事务日志写入文件前,根据序列化产生的字节数组来计算checksum。zk默认使用Adler32算法来计算checksum值。 -
写入事务日志文件流
写入BufferedOutputStream -
刷脏页
提供接口可以手动刷脏页。也可以使用zk的默认刷脏页策略。
日志截断
在zk的运行过程中,可能会出现非leader机器上的事务ID比leader服务器的大。无论是怎么发生的,这都是一个非法的运行时状态。
zk要遵循一个原则:只要集群中存在leader,那么所有机器都必须与该leader的数据保持同步。
因此,一旦出现这种情况,leader会发送trunc命令给这个机器,要求进行日志截断,非leader服务器接收到命令后,会删除所有比leader的事务ID大的所有事务日志文件。
快照数据
和事务日志类似,zk的快照数据也是使用特定的磁盘进行存储。
格式为:snapshot.ZXID。
和事务日志文件不同的是,ZooKeeper的快照数据文件没有采用“预分配”机制,因此不会像事务日志文件那样内容中可能包含大量的“0”.每个快照数据文件中的所有内容都是有效地,因此该文件的大小在一定程度上能够反映当前ZooKeeper内存中全量数据的大小。
快照生成的时机
每进行一次事务日志记录之后,zk都会检查当前是否需要进行数据快照。
理论上进行snapCount次事务操作后就会开始数据快照。snapCount是配置文件的配置项,默认100000。但是考虑到数据快照对于zk所在机器的整体性能的影响,需要尽量避免zk集群中的所有机器在同一时刻进行数据快照。因此zk引入了“过半随机”策略,符合如下条件,才进行数据快照
logCount > (snapCount / 2 + randRoll)。
其中logCount代表了当前已经记录的事务日志数量,randRoll为1~snapCount / 2之间的随机数。因此上面的条件就相当于:如果我们配置的snapCount值为默认的100000,那么zk会在50000~100000次事务日志记录后进行一次数据快照。
切换事务日志文件
满足上述条件,zk就要开始进行数据快照了。当执行快照的时候,认为事务日志已经被“写满”,不管是否到达64M。基于最后一个事务生成快照,然后重新开一个事务日志。
异步线程
为了保证数据快照过程中不影响zk主流程,这里需要创建一个单独的异步线程来进行数据快照。
获取全量数据和会话信息
数据快照本质上就是将内存中所有数据节点信息(DataTree)和会话信息保存到本地磁盘中。因此这里会先从zkDatabase中获取到DataTree和会话信息。
服务器初始化
数据的初始化工作,其实就是从磁盘中加载数据的过程,主要包括了从快照文件中加载快照数据 和 根据事务日志进行数据校正 这两个过程。
服务器初始化.png-
初始化 FileTxnSnapLog
FileTxnSnapLog是zk事务日志和快照数据访问层,用于衔接上层业务和底层数据存储。底层数据存储包含了事务日志和快照数据两部分,因此FileTxnSnapLog内部又分为FileTxnlog和FileSnap的初始化,分别代表事务日志管理器和快照数据管理器的初始化。 -
初始化ZKDatabase
完成FileTxnSnapLog的初始化后,我们就完成了zk服务器和底层数据存储的对接,接下来就要开始构建内存数据库zkDatabase了。
首先会初始化一个DataTree,然后将FileTxnSnapLog交给ZKDatabase,以便内存数据库能对事务日子和快照数据进行访问。
DataTree是zk内存数据的核心模型,其实就是一颗树(map),在每个zk服务器内部都是单例的。DataTre默认初始化节点:/、/zookeeper、/zookeeper/quota
-
创建PlayBackListener监听器
PlayBackListener 主要用来接收事务的回调。用来在事务修订过程中进行对应的数据订正。 -
处理快照文件
完成内存数据库的初始化后,zk就开始从磁盘中恢复数据了。很显然,先从快照文件开始加载。 -
获取最新的100个快照文件
理论上只需要最新的就可以了。为什么要最近的100个呢??(不够100,就获取所有所有的)。
答:只是先拿着,最终肯定是从最近的开始解析,防止只拿一个,解析失败了,没有快照文件可用了。 -
解析快照文件
按时间最新,逐个进行解析。直到解析成功。也就是第1个就解析成功改了,那么只解析了1次。第一个没成功,解析第二个,直到解析成功。如果100个都失败,那无论如何都无法恢复一个完整的DataTree了,则认为无法从磁盘加载数据,服务器启动失败。 -
获取最新的ZXID
拿到快照中最新的ZXID。注意这个ZXID是快照中最新的,而不是zk最新的。毕竟是快照维度的。后续还要用事务日志把快照后面的数据补齐。
这个ZXID暂时命名: zxid_for_snap -
处理事务日志
把快照ZXID,之后的事务,增量添加到DataTree -
获取zxid_for_snap之后提交的事务
从事务文件中获取,之前提过,通过文件名就可以索引到了 -
填充到最新的数据
基于事务日志中的数据,增量添加数据。
有一个细节需要注意,每当有一个事务被应用到内存数据库中去后,zk会回调(3)中创建的PlayBackListener监听器,将这一事务操作记录转换成proposal,并保存到ZKDatabase.committedLog中,以便Follower进行快速同步。 -
获取最新的ZXID
这个ZXID就是上次服务器运行正常时提交的最大事务ID了。 -
校验epoch
epoch是zk中的”纪元“,标识当前leader周期,每次选举产生一个新的epoch。
在完成数据加载后,zk会从(11)中确定的ZXID中解析出事务处理的epoch。同时也会从磁盘的currentEpoch和acceptdEpoch文件中读取出上次记录的最新的epoch值,进行校验。 -
初始化完成
网友评论