ZooKeeper系统模型之事务日志。

文件存储

        在部署ZooKeeper集群的时候需要配置一个目录:dataDir。这个目录是ZooKeeper中默认用于存储事务日志文件的,其实在ZooKeeper中可以为事务日志单独分配一个文件存储目录:dataLogDir。

        如果我们确定dataLogDir为/home/admin/zkData/zk_log,那么ZooKeeper在运行过程中会在该目录下建立一个名字为version-2的子目录,关于这个目录,我们在下面的“日志格式”部分会再次讲解,这里只是简单提下:该目录确定了当前ZooKeeper使用的事务日志格式版本号。也就是说,等到下次某个ZooKeeper版本对事务日志格式进行变更时,这个目录也会有所变更。

        运行一段时间后,我们可以发现在/home/admin/zkData/zk_log/version-2目录下会生成类似下面这样的文件:

        这些文件就是ZooKeeper的事务日志了。不难发现,这些文件都具有以下两个特点。

  • 文件大小都出奇的一致:这些文件的文件大小都是67108880KB,即64MB。
  • 文件名后缀非常非常有规律,都是一个十六进制数字,同时随着文件修改时间的推移,这个十六进制后缀变大。

        关于这个事务日志文件名的后缀,这里需要再补充一点的是,该后缀其实是一个事务ID:ZXID,并且是写入该事务日志文件第一条事务记录的ZXID。使用ZXID作为文件后缀可以帮助我们迅速定位到某一个事务操作所在的事务日志。同时,使用ZXID作为事务日志后缀的另一个优势是,ZXID本身由两部分组成,高32位代表当前Leader周期(epoch),低32位则是真正的操作序列号。因此,将ZXID作为文件后缀,我们就可以清除的看出当前运行时ZooKeeper的Leader周期。例如上述4个事务日志,前两个文件的epoch是44(十六进制2c对应十进制44),而后面两个文件的epoch则是45。

日志格式

         下面我们再来看看这个事务日志里面到底有些什么内容。为此,我们首先部署一个全新的ZooKeeper服务器,配置相关的事务日志存储目录,启动之后,进行如下一系列操作。

  1. 创建/test_log节点,初始值为“v1”。
  2. 更新/test_log节点的数据为“v2”。
  3. 创建/test_log/c节点,初始值为“v1”。
  4. 删除/test_log/c节点。

        经过如上四步操作后,在ZooKeeper事务日志存储目录中就可以看到产生了一个事务日志,使用二进制编辑器将这个文件打开后,就可以烂到类似于下图所示的文件内容——这就是序列化之后的事务日志了。

        对于这个事务日志,我们无法直接通过肉眼识别出其究竟包含了哪些事务操作,但可以发现的一点是,该事务日志中除前面有一些有效地文件内容外,文件后面的绝大部分都被“0”(\0)填充。这个空字符填充和ZooKeeper中事务日志在磁盘上的空间预分配有关,在“日志写入”部分会重点讲解ZooKeeper事务日志文件的磁盘空间预分配策略。

        在上图中我们已经大体上看到了ZooKeeper事务日志的模样。显然,在上图中,除了一些节点路径我们可以隐约的分辨出来之外,就基本上无法看明白其他内容信息了。那么我们不禁要问,是否有一种方式,可以把这些事务日志转换成正常日志文件,以便让开发与运维人员能够清楚的看明白ZooKeeper的事务操作呢?答案是肯定的。

        ZooKeeper提供了一套简易的事务日志格式化工具org.apache.zookeeper.Server.LogFormatter,用于将这个默认的事务日志文件转换成可视化的事务操作日志,使用方法如下:

  • Java LogFormatter 事务日志文件

        例如,我们针对执行上述系统操作之后的事务日志文件,执行以下代码:

java LogFormatter log.300000001

        执行后的输出结果如下图所示。

        在上图中,我们可以发现,所有的事务操作都被可视化显示出来了,并且每一行都对应了一次事务操作,我们列举几行事务操作日志来分析下这个文件。

        第一行:

        这一行是事务日志的文件头信息,这里输出的主要是事务日志的BDID和日志格式版本号。

        第二行:

        这一行就是一次客户端会话创建的事务操作日志,其中我们不难看出,从左向右分别记录了事务操作时间、客户端会话ID、CXID(客户端的操作序列号)、ZXID、操作类型和会话超时时间。

        第三行(图中用“...”代替了“0x144699552020000”):

        这一行是节点创建操作的事务操作日志,从左到右分别记录了事务操作时间、客户端会话ID、CXID、ZXID、操作类型、节点路径、节点数据内容(#7531,在上文中我们提到该节点创建时的初始值是v1。在LogFormatter中使用如下格式输出节点内容:#+内容的ASCII码值)、节点的ACL信息、是否是临时节点(F代表持久节点,T代表临时节点)和父节点的子节点版本号。

        其他几行事务日志的内容和以上两个示例说明基本上类似,这里不再赘述。通过可视化这个文件,我们还注意到一点,由于这是一个记录事务操作的日志文件,因此里面没有任何读操作的日志记录。

日志写入

        FileTxnLog负责维护事务日志对外的接口,包括事务日志的写入和读取等,首先来看日志的写入。将事务操作写入事务日志的工作主要由append方法来负责:

        从方法定义中我们可以看到,ZooKeeper在进行事务日志写入的过程中,会将事务头和事务体传给该方法。事务日志的写入过程大体可以分为如下6个步骤。

确定是否有事务日志可写

        当ZooKeeper服务器启动完成需要进行第一次事务日志的写入,或是上一个事务日志写满的时候,都会处于与事务日志文件断开的状态,即ZooKeeper服务器没有和任意一个日志相关联。因此,在进行事务日志写入前,ZooKeeper首先会判断FileTxnLog组件是否已经关联上一个可写的事务日志文件。如果没有关联上事务日志文件,那么就会使用与该事务操作关联的ZXID作为后缀创建一个事务日志文件,同时构建事务文件头信息(包括魔数magic、事务日志格式版本version和dbid),并立即写入这个事务日志文件中去。同时,将该文件的文本流放入一个集合:streamsToFlush。streamsToFlush集合是ZooKeeper用来记录当前需要强制进行数据落盘(将数据强制刷入磁盘上)的文件流在后续的步骤6中会使用到。

确定事务日志文件是否需要扩容(预分配)

        在前面“文件存储”部分我们已经提到,ZooKeeper的事务日志文件会采取“磁盘空间预分配”的策略。当检测到当前事务日志文件剩余空间不足4096字节(4KB)时,就会开始进行文件空间扩容。文件空间扩容的过程其实非常简单,就是在现有文件大小的基础上,将文件大小增加65536KB(64MB),然后使用“0”(\0)填充这些被扩容的文件空间。因此在第一张图的事务日志文件中,我们会看到我呢见后半部分都被“0”填充了。

        那么ZooKeeper为什么要进行事务日志文件的磁盘空间预分配呢?对于客户端的每一次事务操作,ZooKeeper都会将其写入事务日志文件中。因此,事务日志的写入性能直接决定了ZooKeeper服务器对事务请求的响应。也就是说,事务写入近似可以被看作是一个磁盘I/O的过程。严格地讲,文件的不断追加写入操作会触发底层磁盘I/O为文件开辟新的磁盘块,即磁盘Seek。因此,为了避免磁盘Seek的频率,提高磁盘I/O的效率,ZooKeeper在创建事务日志的时候就会进行文件空间“预分配”——在文件创建之初就向操作系统预分配一个很大的刺配块,默认是64MB,而一旦已分配的文件空间不足4KB时,那么将会再次“预分配”,以避免随着每次事务的写入过程中文件大小增长带来的Seek开销,直至创建新的事务日志。事务日志“预分配”的大小可以通过系统属性zookeeper.proAllocSize来进行设置。

事务序列化

        事务序列化包括对事务头和事务体的序列化,分别是对TxnHeader(事务头)和Record(事务体)的序列化。其中事务体又分为会话创建事务(CreateSessionTxn)、节点创建事务(CreateTxn)、节点删除事务(DeleteTxn)和节点数据更新事务(SetDataTxn)等。

生成Checksum

        为了保证事务日志文件的完整性和数据的准确性,ZooKeeper在将事务日志写入文件前,会根据步骤3中序列化产生的字节数组来计算Checksum。ZooKeeper默认使用Adler32算法来计算Checksum值。

写入事务日志文件流

        将序列化后的事务头、事务体及Checksum值写入到文件流中去。此时由于ZooKeeper使用的是BufferedOutputStream,因此写入的数据并非真正被写入到磁盘文件上。

事务日志刷入磁盘

        在步骤5中,已经将事务操作写入文件流中,但是由于缓存的原因,无法实时的写入磁盘文件中,因此我们需要将缓存数据强制刷入磁盘。在步骤1中我们已经将每个事务日志文件对应的文件流放入streamsToFlush,因此这里会从streamsToFlush中提取出文件流,并调用FileChannel.force(boolean metaData)接口来强制将数据刷入磁盘文件中去。force接口对应的其实是底层的fsync接口,是一个比较耗费磁盘I/O资源的接口,因此ZooKeeper允许用户控制是否需要主动调用该接口,可以通过系统属性zookeeper.forceSync来设置。

日志截断

        在ZooKeeper运行过程中,可能会出现这样的情况,非Leader机器上记录的事务ID(我们将其称为peerLastZxid)比Leader服务器大,无论这个情况是如何发生的,都是一个非法的运行时状态。同时,ZooKeeper遵循一个原则:只要集群中存在Leader,那么所有机器都必须与该Leader的数据保持同步。

        因此,一旦某台机器碰到上述情况,Leader会发送TRUNC命令给这个机器,要求其进行日志截断。Learner服务器在接收到该命令后,就会删除所有包含或大于peerLastZxid的事务日志文件。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值