Zookeeper的持久化就是将内存中的数据保存到磁盘上,防止数据丢失。
持久化主要分为两种
- 事务日志
- 数据快照
事务日志
- 事务日志主要是将每个事务操作先日志文件里,再进行实际的事务操作。这种先写日志后操作的方式被称为“write-ahead log”,这种机制被广泛使用的各种场景,比如Mysql的redo log,hdfs的editlog等等。有些是只先写元数据,有些是写数据本身。这种机制的好处是先写日志,可防止数据丢失。另一个附带好处是可以把多个IO合并成一个IO,大大提高吞吐量。
- 事务日志文件的命名规则:log.2c01311231
- 后缀就是写文件时最新的事务ID(ZXID),高32位代表选举任期(epoch),低32位代表事务序列号。2c即epoch,01311231即序列号。
- 事务日志的磁盘空间预分配策略:每个事务日志文件固定为64M,也就是创建文件时预先分配64M的空间,未使用的部分都是0。
- 日志写入过程
- 确定事务日志文件是否需要扩容
- 事务序列化
- 生成checksum
- 写入事务日志文件流
- 刷到磁盘
snapshot 数据快照
-
数据快照:将zookeeper服务器上某一时刻的全量内存数据序列化后写到磁盘文件中。可用于数据恢复。
-
快照文件的命名:snapshot.2c01311231。快照开始时刻的最新zxid当做后缀。
每次事务日志写入后都会判断是否需要进行快照:
// 写入事务日志成功后
if (zks.getZKDatabase().append(si)) {
// 判断是否进行快照
if (shouldSnapshot()) {
// 重置快照统计
resetSnapshotStats();
// roll the log 将事务日志输出流置为null,便于下次写事务日志时重新创建新文件
zks.getZKDatabase().rollLog();
// take a snapshot 尝试获取快照锁
if (!snapThreadMutex.tryAcquire()) {
LOG.warn("Too busy to snap, skipping");
} else {
// 创建异步线程进行快照任务,任务结束后线程销毁。不影响其他线程的运行。
new ZooKeeperThread("Snapshot Thread") {
public void run() {
try {
// 真正的快照方法:核心是内存数据结构的序列化
zks.takeSnapshot();
} catch (Exception e) {
LOG.warn("Unexpected exception", e);
} finally {
snapThreadMutex.release();
}
}
}.start();
}
}
}
下面看一下shouldSnapshot()方法是如何判断是否需要快照的
private boolean shouldSnapshot() {
// 获取从上次快照到现在的事务总数
int logCount = zks.getZKDatabase().getTxnCount();
// 获取从上次快照到现在的事务数据的大小
long logSize = zks.getZKDatabase().getTxnSize();
// snapCount和snapSizeInBytes都是配置参数。snapCount的默认大小是10万,
// randRoll是0到snapCount/2之间的随机数。
// logCount > (snapCount / 2 + randRoll)的含义是一种“超半随机”的策略。
// 事务数据大小的判断逻辑也是一样。 //
return (logCount > (snapCount / 2 + randRoll))
|| (snapSizeInBytes > 0 && logSize > (snapSizeInBytes / 2 + randSize));
}
// 重置半数随机数。在判断需要快照后(shouldSnapshot()返回true)会进行重置
private void resetSnapshotStats() {
// 半数随机数
randRoll = ThreadLocalRandom.current().nextInt(snapCount / 2);
randSize = Math.abs(ThreadLocalRandom.current().nextLong() % (snapSizeInBytes / 2));
}
上面源码注释中提到的“过半随机”的策略目的,是为了防止集群中zookeeper机器在同一时间都进行快照,尽量地错开运行,有助于提高性能。