Zookeeper事务日志预分配空间解读
作者:恐龙弟旺仔 发布时间:2022-03-16 22:36:49
前言
Zookeeper的通过快照日志和事务日志将内存信息保存下来,记录下来每次请求的具体信息。
尤其是其事务日志,每次处理事务请求时都需要将其记录下来。
Zookeeper事务日志的默认存储方式是磁盘文件,那么Zookeeper的总体性能就受限与磁盘文件的写入速度。
针对这个瓶颈,Zookeeper做了什么优化操作呢,本文我们就一起来了解下。
1.事务日志的预分配
事务日志的添加,我们需要从FileTxnLog.append()方法看起
public class FileTxnLog implements TxnLog {
volatile BufferedOutputStream logStream = null;
volatile OutputArchive oa;
volatile FileOutputStream fos = null;
// 追加事务日志
public synchronized boolean append(TxnHeader hdr, Record txn)
throws IOException
{
if (hdr == null) {
return false;
}
if (hdr.getZxid() <= lastZxidSeen) {
LOG.warn("Current zxid " + hdr.getZxid()
+ " is <= " + lastZxidSeen + " for "
+ hdr.getType());
} else {
lastZxidSeen = hdr.getZxid();
}
// 默认logStream为空
if (logStream==null) {
if(LOG.isInfoEnabled()){
LOG.info("Creating new log file: " + Util.makeLogName(hdr.getZxid()));
}
// 以下代码为创建事务日志文件
// 根据当前事务ID来创建具体文件名,并写入文件头信息
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);
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;
}
}
创建FileTxnLog对象时,其logStream属性为null,所以当第一次处理事务请求时,会先根据当前事务ID来创建一个文件。
1.1 事务日志预分配
public class FilePadding {
long padFile(FileChannel fileChannel) throws IOException {
// 针对新文件而言,newFileSize=64M
long newFileSize = calculateFileSizeWithPadding(fileChannel.position(), currentSize, preAllocSize);
if (currentSize != newFileSize) {
// 将文件扩充到64M,全部用0来填充
fileChannel.write((ByteBuffer) fill.position(0), newFileSize - fill.remaining());
currentSize = newFileSize;
}
return currentSize;
}
// size计算
public static long calculateFileSizeWithPadding(long position, long fileSize, long preAllocSize) {
// If preAllocSize is positive and we are within 4KB of the known end of the file calculate a new file size
// 初始时候position=0,fileSize为0,preAllocSize为系统参数执行,默认为64M
if (preAllocSize > 0 && position + 4096 >= fileSize) {
// If we have written more than we have previously preallocated we need to make sure the new
// file size is larger than what we already have
// Q:这里确实没看懂...
if (position > fileSize) {
fileSize = position + preAllocSize;
fileSize -= fileSize % preAllocSize;
} else {
fileSize += preAllocSize;
}
}
return fileSize;
}
}
预分配的过程比较简单,就是看下当前文件的剩余空间是否<4096,如果是,则扩容。
Q:
这里有一个不太明白的问题,position > fileSize的场景是怎样的呢?
2.创建新的事务日志文件时机
通过上述代码分析我们知道,当logStream=null时,就会创建一个新的事务日志文件,那么logStream对象什么时候为空呢?
搜索代码,只看到FileTxnLog.rollLog()方法会主动将logStream设置为null
public class FileTxnLog implements TxnLog {
public synchronized void rollLog() throws IOException {
if (logStream != null) {
this.logStream.flush();
this.logStream = null;
oa = null;
}
}
}
那么根据这个线索,我们来搜索下rollLog的调用链
SyncRequestProcessor.run() -> ZKDatabase.rollLog() -> FileTxnSnapLog.rollLog() -> FileTxnLog.rollLog()
最终看到是在SyncRequestProcessor.run()方法中发起调用的,而且只有这一条调用链,我们来分析下
2.1 SyncRequestProcessor.run()
public class SyncRequestProcessor extends ZooKeeperCriticalThread implements RequestProcessor {
public void run() {
try {
int logCount = 0;
setRandRoll(r.nextInt(snapCount/2));
while (true) {
...
if (si != null) {
// 追加事务日志
if (zks.getZKDatabase().append(si)) {
logCount++;
if (logCount > (snapCount / 2 + randRoll)) {
setRandRoll(r.nextInt(snapCount/2));
// 注意:在这里发起了rollLog
zks.getZKDatabase().rollLog();
...
}
} else if (toFlush.isEmpty()) {
...
}
toFlush.add(si);
if (toFlush.size() > 1000) {
flush(toFlush);
}
}
}
} catch (Throwable t) {
handleException(this.getName(), t);
running = false;
}
LOG.info("SyncRequestProcessor exited!");
}
}
需要注意下rollLog()方法执行的条件,就是logCount > (snapCount / 2 + randRoll)
snapCount是一个系统参数,System.getProperty("zookeeper.snapCount"),默认值为100000
randRoll是一个随机值
那么该条件触发的时机为:处理的事务请求数至少要大于50000。
这时就出现了一个笔者无法理解的情况:
通过对事务日志的观察可以看到其都是64M,而至少处理50000次事务请求后,Zookeeper才会分配一个新的事务日志文件,那么这个snapCount是一个经验值嘛?
如果事务请求的value信息都很大,那么可能到不了50000次,就会超过64M,理论上应该要创建一个新的文件了,但是貌似并没有,这个该怎么处理呢?
如果事务请求value信息都很小,那么即使到了50000次,也不会超过64M,那么之前预分配的文件大小就浪费了一部分。
来源:https://blog.csdn.net/qq_26323323/article/details/128158332
猜你喜欢
- 一、什么是过滤器过滤器是对数据进行过滤,预处理过程,当我们访问网站时,有时候会发布一些敏感信息,发完以后有的会用*替代,还有就是登陆权限控制
- 缘起工作时使用java开发服务器后台,用Jersey写Restful接口,发现有一个Post方法始终获取不到参数,查了半天,发现时获取参数的
- 代码如下import java.util.concurrent.Callable;import java.util.concurrent.E
- 1. 使用方法首先从http://repo1.maven.org/maven2/com/alibaba/druid/&
- 前言今天的文章从下面这张图片开始,这张图片Java开发们应该很熟悉了我们都知道无锁状态是对象头是有位置存储hashcode的,而变为偏向锁状
- 单线程是安全的,因为线程只有一个,不存在多个线程抢夺同一个资源代码例子:public class SingleThread {int num
- 介绍Java命令模式(Command Pattern)是一种行为型设计模式,它将请求封装成一个对象,从而使不同的请求可以进行参数化,并支持请
- 一、目的本篇文章的目的是记录本人使用flutter加载与调用第三方aar包。二、背景本人go后端,业余时间喜欢玩玩flutter。一直有一个
- 类与对象:类是抽象的数据类型,对象是抽象的数据类型的具体化。使用new 关键字创建对象,默认初始化为null一个项目只存在一个main方法,
- 引言最近,各大平台都新增了评论区显示发言者ip归属地的功能,例如哔哩哔哩,微博,知乎等等。Java 中是如何获取 IP&
- PermissionManage项目地址:https://github.com/why168/AndroidProjects/tree/ma
- 1、前言随着技术的发展,微信的一系列服务渗透进了我们的生活,但是我们应该怎样进行微信方面的开发呢。相信很多的小伙伴们都很渴望知道吧。这篇文章
- Java基础面试题及答案集锦(基础题122道,代码题19道),具体详情如下所示:1、面向对象的特征有哪些方面1.抽象:抽象就是忽略一个主题中
- 如下所示:package cn.sunzn.md5;import java.security.MessageDigest;import ja
- 此解决方案是针对window的,因为日志默认保存路径在C盘,linux忽略。学习RocketMQ过程中,总是出现com.alibaba.ro
- 前段时间写了一篇基于mybatis实现的多数据源博客。感觉不是很好,这次打算加入git,来搭建一个基于Mybatis-Plus的多数据源项目
- 首先我们常用的注解包括@Entity、@Table、@Id、@IdClass、@GeneratedValue、@Basic、@Transie
- 标准c++中string类函数介绍注意不是CString之所以抛弃char*的字符串而选用C++标准程序库中的string类,是因为他和前者
- 一、非配置文件注入1、注入普通字符串直接附在属性名上,在 Bean 初始化时,会赋初始值。@Value("admin")
- 一、安装activeMQLinux环境ActiveMQ部署方法:https://www.aspxhome.com/article/16232