Zookeeper服务数据与文件

Zookeeper服务数据与文件

ZooKeeper 框架能够实现的功能,而无论是什么程序,其本质就是对数据的操作。比如 MySQl 数据库操作的是数据表,Redis 数据库操作的是存储在内存中的 Key-Value 值。不同的数据格式和存储方式对系统运行的效率和处理能力都有很大影响。本课时就来学习,在 ZooKeeper 程序运行期间,都会处理哪些数据,以及他们的存储格式和存储位置。

ZooKeeper 服务提供了创建节点、添加 Watcher 监控机制、集群服务等丰富的功能。这些功能服务的实现,离不开底层数据的支持。从数据存储地点角度讲,ZooKeeper 服务产生的数据可以分为内存数据和磁盘数据。而从数据的种类和作用上来说,又可以分为事务日志数据和数据快照数据。

1.内存数据

数据节点实际上就是 ZooKeeper 在服务运行过程中所操作的数据。ZooKeeper 的数据模型可以看作一棵树形结构,而数据节点就是这棵树上的叶子节点。从数据存储的角度看,ZooKeeper 的数据模型是存储在内存中的。我们可以把 ZooKeeper 的数据模型看作是存储在内存中的数据库,而这个数据库不但存储数据的节点信息,还存储每个数据节点的 ACL 权限信息以及 stat 状态信息等。

而在底层实现中,ZooKeeper 数据模型是通过 DataTree 类来定义的。如下面的代码所示,DataTree 类定义了一个 ZooKeeper 数据的内存结构。DataTree 的内部定义类 nodes 节点类型、root 根节点信息、子节点的 WatchManager 监控信息等数据模型中的相关信息。可以说,一个 DataTree 类定义了 ZooKeeper 内存数据的逻辑结构。

1
2
3
4
5
6
7
8
9
10
11
public class DataTree {

private DataNode root

private final WatchManager dataWatches

private final WatchManager childWatches

private static final String rootZookeeper = "/";

}

2.事务日志

为了整个 ZooKeeper 集群中数据的一致性,Leader 服务器会向 ZooKeeper 集群中的其他角色服务发送数据同步信息,在接收到数据同步信息后, ZooKeeper 集群中的 Follow 和 Observer 服务器就会进行数据同步。而这两种角色服务器所接收到的信息就是 Leader 服务器的事务日志。在接收到事务日志后,并在本地服务器上执行。这种数据同步的方式,避免了直接使用实际的业务数据,减少了网络传输的开销,提升了整个 ZooKeeper 集群的执行性能。

在我们启动一个 ZooKeeper 服务器之前,首先要创建一个 zoo.cfg 文件并进行相关配置,其中有一项配置就是 dataLogDir 。在这项配置中,我们会指定该台 ZooKeeper 服务器事务日志的存放位置

在 ZooKeeper 服务的底层实现中,是通过 FileTxnLog 类来实现事务日志的底层操作的。如下图代码所示,在 FileTxnLog 类中定义了一些属性字段,分别是:

  • preAllocSize:可存储的日志文件大小。如用户不进行特殊设置,默认的大小为 65536*1024 字节。

  • TXNLOG_MAGIC:设置日志文件的魔数信息为ZKLG。

  • VERSION:设置日志文件的版本信息。

  • lastZxidSeen:最后一次更新日志得到的 ZXID。

1
2
3
4
5
6
7
8
9
10
11
public class FileTxnLog implements TxnLog, Closeable {

private static final Logger LOG;

public static final int TXNLOG_MAGIC = ByteBuffer.wrap("ZKLG".getBytes()).getInt();

public static final int VERSION = 2;

/** Maximum time we allow for elapsed fsync before WARNing */
private static final long fsyncWarningThresholdMS;
}
1
2
3
4
5
6
public class FilePadding {

private static final Logger LOG;
private static long preAllocSize = 65536 * 1024;
... ...
}

定义了事务日志操作的相关指标参数后,在 FileTxnLog 类中调用 static 静态代码块,来将这些配置参数进行初始化。比如读取 preAllocSize 参数分配给日志文件的空间大小等操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static {
LOG = LoggerFactory.getLogger(FileTxnLog.class);

/** Local variable to read fsync.warningthresholdms into */
Long fsyncWarningThreshold;
if ((fsyncWarningThreshold = Long.getLong(ZOOKEEPER_FSYNC_WARNING_THRESHOLD_MS_PROPERTY)) == null) {
fsyncWarningThreshold = Long.getLong(FSYNC_WARNING_THRESHOLD_MS_PROPERTY, 1000);
}
fsyncWarningThresholdMS = fsyncWarningThreshold;

Long logSize = Long.getLong(txnLogSizeLimitSetting, -1);
if (logSize > 0) {
LOG.info("{} = {}", txnLogSizeLimitSetting, logSize);

// Convert to bytes
logSize = logSize * 1024;
txnLogSizeLimit = logSize;
}
}

经过参数定义和日志文件的初始化创建后,在 ZooKeeper 服务器的 dataDir 路径下就生成了一个用于存储事务性操作的日志文件。我们知道在 ZooKeeper 服务运行过程中,会不断地接收和处理来自客户端的事务性会话请求,这就要求每次在处理事务性请求的时候,都要记录这些信息到事务日志中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/**
* append an entry to the transaction log
* @param hdr the header of the transaction
* @param txn the transaction part of the entry
* returns true iff something appended, otw false
* 向事务日志追加一个条目
*/
public synchronized boolean append(TxnHeader hdr, Record txn) throws IOException {
return append(hdr, txn, null);
}

@Override
public synchronized boolean append(TxnHeader hdr, Record txn, TxnDigest digest) throws IOException {
if (hdr == null) {
return false;
}
if (hdr.getZxid() <= lastZxidSeen) {
LOG.warn(
"Current zxid {} is <= {} for {}",
hdr.getZxid(),
lastZxidSeen,
Request.op2String(hdr.getType()));
} else {
lastZxidSeen = hdr.getZxid();
}
if (logStream == null) {
LOG.info("Creating new log file: {}", Util.makeLogName(hdr.getZxid()));

logFileWrite = new File(logDir, Util.makeLogName(hdr.getZxid()));
fos = new FileOutputStream(logFileWrite);
logStream = new BufferedOutputStream(fos);
oa = BinaryOutputArchive.getArchive(logStream);
FileHeader fhdr = new FileHeader(TXNLOG_MAGIC, VERSION, dbId);
fhdr.serialize(oa, "fileheader");
// Make sure that the magic number is written before padding.
logStream.flush();
filePadding.setCurrentSize(fos.getChannel().position());
streamsToFlush.add(fos);
}
filePadding.padFile(fos.getChannel());
byte[] buf = Util.marshallTxnEntry(hdr, txn, digest);
if (buf == null || buf.length == 0) {
throw new IOException("Faulty serialization for header " + "and txn");
}
Checksum crc = makeChecksumAlgorithm();
crc.update(buf, 0, buf.length);
oa.writeLong(crc.getValue(), "txnEntryCRC");
Util.writeTxnBytes(oa, buf);

return true;
}

从对事务日志的底底层代码分析中可以看出,在 datadir 配置参数路径下存放着 ZooKeeper 服务器所有的事务日志,所有事务日志的命名方法都是“log.+ 该条事务会话的 zxid”。

3.数据快照

说到快照,可能很多技术人员都不陌生。一个快照可以看作是当前系统或软件服务运行状态和数据的副本。在 ZooKeeper 中,数据快照的作用是将内存数据结构存储到本地磁盘中。因此,从设计的角度说,数据快照与内存数据的逻辑结构一样,都使用 DataTree 结构。在 ZooKeeper 服务运行的过程中,数据快照每间隔一段时间,就会把 ZooKeeper 内存中的数据存储到磁盘中,快照文件是间隔一段时间后对内存数据的备份。

因此,与内存数据相比,快照文件的数据具有滞后性。而与上面介绍的事务日志文件一样,在创建数据快照文件时,也是使用 zxid 作为文件名称。

在代码层面,ZooKeeper 通过 FileTxnSnapLog 类来实现数据快照的相关功能。如下图所示,在FileTxnSnapLog 类的内部,最核心的方法是 save 方法,在 save 方法的内部,首先会创建数据快照文件,之后调用 FileSnap 类对内存数据进行序列化,并写入到快照文件中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public void save(DataTree dataTree,

ConcurrentHashMap<Long, Integer> sessionsWithTimeouts,

boolean syncSnap)

throws IOException {

long lastZxid = dataTree.lastProcessedZxid;

File snapshotFile = new File(snapDir, Util.makeSnapshotName(lastZxid));

LOG.info("Snapshotting: 0x{} to {}", Long.toHexString(lastZxid),

snapshotFile);

snapLog.serialize(dataTree, sessionsWithTimeouts, snapshotFile, syncSnap);

}

4.总结

我们知道在 ZooKeeper 服务的运行过程中,会涉及内存数据事务日志数据快照这三种数据文件。从存储位置上来说,事务日志和数据快照一样,都存储在本地磁盘上;而从业务角度来讲,内存数据就是我们创建数据节点、添加监控等请求时直接操作的数据。事务日志数据主要用于记录本地事务性会话操作,用于 ZooKeeper 集群服务器之间的数据同步。事务快照则是将内存数据持久化到本地磁盘。

这里要注意的一点是,数据快照是每间隔一段时间才把内存数据存储到本地磁盘,因此数据并不会一直与内存数据保持一致。在单台 ZooKeeper 服务器运行过程中因为异常而关闭时,可能会出现数据丢失等情况。


博客说明

文章所涉及的资料来自互联网整理和个人总结,意在于个人学习和经验汇总,不用于任何的商业用途。如有侵权,请联系本人删除。谢谢!


Zookeeper服务数据与文件
https://nanchengjiumeng123.top/2022/09/20/framework/zookeeper/2022-09-20_Zookeeper服务数据与文件/
作者
Yang Xin
发布于
2022年9月20日
许可协议