内存数据
ZooKeeper的数据模型是树结构,在内存数据库中,存储了整棵树的内容,包括所有的节点路径、节点数据、ACL信息,ZooKeeper会定时将这个数据存储到磁盘上。
DataTree是内存数据存储的核心,是一个树结构,代表了内存中一份完整的数据。DataTree不包含任何与网络、客户端连接及请求处理相关的业务逻辑,是一个独立的组件。
DataNode是数据存储的最小单元,其内部除了保存了节点的数据内容、ACL列表、节点状态之外,还记录了父节点的引用和子节点列表两个属性,其也提供了对子节点列表进行操作的接口。
ZKDatabase是ZooKeeper的内存数据库,管理ZooKeeper的所有会话、DataTree存储和事务日志。ZKDatabase会定时向磁盘dump快照数据,同时在ZooKeeper启动时,会通过磁盘的事务日志和快照文件恢复成一个完整的内存数据库。
ZooKeeper为了防止系统宕机或重启导致的数据丢失,会对数据进行定时持久化。有两种持久化方式:
事务日志
实时将每次事务操作记录到日志文件,比如创建、删除节点、更新节点数据等,这样就可以通过执行这些日志文件来恢复数据。
当ZooKeeper服务器启动完成需要进行第一次事务日志的写入,或是上一次事务日志写满时,都会处于与事务日志文件断开的状态,即ZooKeeper服务器没有和任意一个日志文件相关联。因此在进行事务日志写入前,ZooKeeper首先会判断FileTxnLog组件是否已经关联上一个可写的事务日志文件。若没有,则会使用该事务操作关联的ZXID作为后缀创建一个事务日志文件,同时构建事务日志的文件头信息,并立即写入这个事务日志文件中去,同时将该文件的文件流放入streamToFlush集合,该集合用来记录当前需要强制进行数据落盘的文件流。
ZooKeeper会采用磁盘空间预分配策略。当检测到当前事务日志文件剩余空间不足4096字节时,就会开始进行文件空间扩容,即在现有文件大小上,将文件增加65536KB(64MB),然后使用”0”填充被扩容的文件空间。
ZooKeeper对事务头和事务体的序列化,其中事务体又可分为会话创建事务、节点创建事务、节点删除事务、节点数据更新事务等。为保证日志文件的完整性和数据的准确性,ZooKeeper在将事务日志写入文件前,会计算生成Checksum。将序列化后的事务头、事务体和Checksum写入文件流中,然后将缓存数据强制刷入磁盘。
在ZooKeeper运行过程中,可能出现非Leader记录的事务ID比Leader上大,这是非法运行状态。此时,需要保证所有机器必须与该Leader的数据保持同步,即Leader会发送TRUNC命令给该机器,要求进行日志截断,Learner收到该命令后,就会删除所有包含或大于该事务ID的事务日志文件。
快照
为了加快ZooKeeper恢复的速度,ZooKeeper还提供了对树结构DataTree和session信息进行数据快照持久化的操作。
在ZooKeeper客户端请求ZooKeeper服务中,ZooKeeper的服务端首先是判断这个请求是否是事务请求,如果是事务请求,那么ZooKeeper服务端首先将这个请求记录在增量事务日志中,保证了其持久化,然后再进行更新内存数据DataTree;在这个过程中,它也会去判断是否生成snapshot快照文件,如果要生成snapshot,那么就创建一个新的线程去干snapshot的事情。
使用snapCount参数来配置每次数据快照之间的事务操作次数,即ZooKeeper会在snapCount次事务日志记录后进行一个数据快照。和事务日志文件一样,快照文件的命名也是有含义的,命名为snapShot.{zxid},后缀是该快照文件生成时已执行的最新的事务的zxid,即[1,zxid]的所有事务已应用到DataTree。
每进行一次事务日志记录之后,ZooKeeper都会检测当前是否需要进行数据快照。理论上进行snapCount次事务操作后就会开始数据快照,但是考虑到数据快照对于ZooKeeper所在机器的整体性能的影响,需要尽量避免ZooKeeper集群中的所有机器在同一时刻进行数据快照。因此ZooKeeper在具体的实现中,并不是严格地按照这个策略执行的,而是采取“过半随机”策略,snapCount/2 ~ snapCount次事务日志记录后进行一次数据快照。