前言

通过后面的一些文章的学习和理解,咱们对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 » 0x00000000000000ctiffle » Thu Jan 01 08:00:00 C S T 1970mZxid - OxOOOOOOGOOQOOOOmtime = Thu  Jttxi 01 08:0D:0 © C S T 1972pZxid » 0*00000300000003cversion = 2dataVersion = 0aclVersion = 0ephemeralOwner = 0x00000000000000dataLength = 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最大的快照文件开始,进行一一解析,进行反序列化操作,而后生成DataTreesessionWithTimeout,并且依据checkSum校验完整性,如果校验失败,会放弃这个快照文件,抉择第二个ZXID最大的快照文件,持续解析,顺次类推,如果读取到的最多一百个快照文件都失败了,那么就间接启动失败,如果有校验胜利的,则应用该文件进行全量复原。

4.当快照文件复原全量数据实现后,此时曾经创立了DataTree实例和sessionsWithTimeOuts汇合了,这个时候咱们也晓得快照文件对应的最新的ZXID,而这个时候咱们就须要找到比snap中的ZXID大的事物日志,进行增量复原和数据修改,每一条事务日志被复原后,就会利用到快照复原进去的DataTree和sessionsWithTimeOuts中,并且会回调PlayBackListener 监听器,将这一
事务操作记录转换成 Proposal , 并保留到ZkDatabase.committedLog 中,以便 Follower 进行疾速同步操作。

5.当事务日志复原结束后,数据的初始化过程根本完结,这个时候再去获取一个ZXID,用来作为上次服务器失常阶段提交的最大事务ID,这个时候依据ZXID解析进去上一次leader的周期-epochOfZxid,同时在磁盘的currentEpochacceptedEpoch文件读取上次记录的epoch进行校验,至此数据初始化流程实现

数据同步

当zookeeper初始化实现后,集群选举后,Learner服务器会向Leader实现注册当前,就会触发数据同步环节。在后面的文章中,咱们学习过,注册Learner的最初阶段,会发送给Leader服务器一个ACKEPOCH数据包,Leader会依据发来的数据包解析进去Learner机器以后currentEpochlastZxid,接着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介于minCommittedLogmaxCommittedLog之间。

在执行同步的过程中,首先Leader服务器会发送一个DIFF指令给所有的须要同步的Learner服务器,用于告诉差异化数据,而差异化数据则是通过PROPOSALCOMMIT数据包实现,Leader在发送完差异化数据当前,就会把Learner退出到forwardingFollower或者observingLearners队列中,并且这个时候Leader会发送一个NEWLEADER指令,用于告诉对应的Learner,曾经将所有的缓存队列中的Proposal都同步过来了。同样的Learner服务器在收到了DIFF指令后,开启DIFF同步阶段,而后将收到的数据包,顺次的利用到内存数据库中,最初等到收到Leader发送的NEWLEADER指令后,代表Leader曾经全副发送结束,此时Learner会反馈一个ACK音讯,Leader承受到ACK音讯后,代表此时Learner服务器曾经承受完所有的同步数据,此时会持续期待其余的Learner的ACK响应,直到集群中过半的Learner服务器都响应了为止。

至此,Zookeeper曾经认为实现了数据同步操作,间接凋谢集群,提供对外的服务。