事务日志

Zookeeper的事务日志默认存储在DataLogDir参数的目录下,如果没有配置该参数,则位于dataDir下面。

如果我们确定dataLogDi r为/home/admin/zkData/zk_ log,那么ZooKeeper在运行过程中会在该目录下建立一个名字为version-2的子目录,该目录确定了当前ZooKeeper使用的事务日志格式版本号。也就是说,等到下次某个ZooKeeper版本对事务日志格式进行变更时,这个目录也会有所变更。

运行一段时间后,我们可以发在/home/admin/zkData/zk_ log/version-2 目录下会生成类似下面这样的文件:(单机模式)

zookeeper 运行日志文件 zookeeper的日志在哪_事务日志

这些就是Zookeeper的事务日志了。这些文件都具有以下两个特点:

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

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

日志格式:
启动Zookeeper,进行如下操作:

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

使用二进制编辑器打开文件如下——被序列化的事务日志

zookeeper 运行日志文件 zookeeper的日志在哪_后缀_02

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

//要保证如下这两包jar包存在
java -classpath .:slf4j-api-1.7.25.jar:zookeeper-3.4.14.jar org.apache.zookeeper.server.LogFormatter version-2/log.28

zookeeper 运行日志文件 zookeeper的日志在哪_日志文件_03

第一行:ZooKeeper Transactional Log File with dbid 0 txnlog format version 2

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

第二行:20-5-14 下午06时42分01秒 session 0x1000061da070000 cxid 0x0 zxid 0x28 createSession 30000

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

第三行:20-5-14 下午06时42分27秒 session 0x1000061da070000 cxid 0x2 zxid 0x29 create '/test_log,#7631,v{s{31,s{'world,'anyone}}},F,12

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

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

public synchronized boolean append(TxnHeader hdr, Record txn);

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

  1. 确认是否有事务日志可写
    当ZooKeeper服务器启动完成需要进行第一次事务日志的写入,或是上一个事务日志写满的时候,都会处于与事务日志文件断开的状态,即ZooKeeper服务器没有和任意一个日志文件相关联。因此,在进行事务日志写人前,ZooKeeper 首先会判断FileTxnLog组件是否已经关联上一一个可写的事务日志文件。如果没有关联上事务日志文件,那么就会使用与该事务操作关联的ZXID作为后缀创建一个事务日志文件,同时构建事务日志文件头信息(包含魔数magic、事务日志格式版本version和dbid),并立即写人这个事务日志文件中去。同时,将该文件的文件流放入一个集合: streamsToFlush。streamsToFlush集合是ZooKeeper用来记录当前需要强制进行数据落盘(将数据强制刷入磁盘上)的文件流,在后续的步骤6中会使用到。
  2. 确定事务日志文件是否需要扩容(预分配)
    Zookeeper的事务日志文件会采取"磁盘空间预分配"的策略。当检测到当前事务日志文件剩余空间不足4096字节(4KB)时,就会开始进行文件空间扩容。文件空间扩容的过程其实非常简单,就是在现有文件大小的基础上,将文件大小增加65536KB (64MB), 然后使用“0”(\0)填充这些被扩容的文件空间。因此在图7-44所示的事务日志文件中,我们会看到文件后半部分都被“0”填充了。
    对于客户端的每一次事务操作,ZooKeeper 都会将其写入事务日志文件中。因此,事务日志的写入性能直接决定了ZooKeeper服务器对事务请求的响应,也就是说,事务写入近似可以被看作是一个磁盘I/O的过程。严格地讲,文件的不断追加写入操作会触发底层磁盘I/O 为文件开辟新的磁盘块,即磁盘Seek。因此,为了避免磁盘Seek的频率,提高磁盘I/O的效率,ZooKeeper 在创建事务日志的时候就会进行文件空间“预分配”一在文件创建之初就向操作系统预分配一个很大的磁盘块,默认是64MB,而一旦已分配的文件空间不足4KB时,那么将会再次“预分配”,以避免随着每次事务的写入过程中文件大小增长带来的Seek开销,直至创建新的事务日志。事务日志“ 预分配”的大小可以通过系统属性zookeeper . preAllocSize来进行设置。
  3. 事务序列化
    事务序列化包括对事务头和事务体的序列化,分别是对TxnHeader (事务头)和Record(事务体)的序列化。其中事务体又可分为会话创建事务(CreateSessionTxn)、 节点创建事务(CreateTxn)、 节点删除事务(DeleteTxn) 和节点数据更新事务(SetDataTxn)等。
  4. 生成Checksum
    为了保证事务日志文件的完整性和数据的准确性,ZooKeeper在将事务日志写入文件前,会根据步骤3中序列化产生的字节数组来计算Checksum。ZooKeeper默认使用Adler32算法来计算Checksum值。
  5. 写入事务日志文件流
    将序列化后的事务头、事务体及Checksum 值写人到文件流中去。此时由于ZooKeeper使用的是Buffered0utputStream,因此写入的数据并非真正被写入到磁盘上。
  6. 事务日志刷入磁盘
    在步骤5中,已经将事务操作写入文件流中,但是由于缓存的原因,无法实时地写入磁盘文件中,因此我们需要将缓存数据强制刷入磁盘。在步骤1中我们已经将每个事务日志文件对应的文件流放入了streamsToFlush, 因此这里会从st reamsToFlush中提取出文件流,并调用FileChannel. force (boolean metaData)接口来强制将数据刷入磁盘文件中去。force接口对应的其实是底层的fsync接口,是一个比较耗费磁盘I/O资源的接口,因此ZooKeeper允许用户控制是否需要主动调用该接口,可以通过系统属性zookeeper . forceSync来设置。

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

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