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

标签:Zookeeper,事务日志,预分配,空间
0
投稿

猜你喜欢

  • myBatis实现三 级嵌套复杂对象的赋值问题

    2023-11-23 06:42:26
  • SpringBoot过滤器与拦截 器深入分析实现方法

    2023-11-28 23:04:15
  • java实现简单的webservice方式

    2023-11-25 03:59:58
  • Android中获取资源 id 及资源 id 的动态获取

    2023-06-30 04:38:06
  • 深入理解java内置锁(synchronized)和显式锁(ReentrantLock)

    2023-11-19 00:10:57
  • OpenCV实现人脸识别简单程序

    2023-07-07 00:31:12
  • Java8使用stream实现list中对象属性的合并(去重并求和)

    2023-06-23 13:44:40
  • Springboot自带定时任务实现动态配置Cron参数方式

    2023-11-10 10:21:31
  • Java实战之基于swing的QQ邮件收发功能实现

    2023-11-15 01:34:26
  • Java中不可或缺的关键字volatile详析

    2023-07-22 12:11:29
  • java实现微信小程序登录态维护的示例代码

    2023-08-22 18:29:46
  • QT5实现简单的TCP通信的实现

    2023-11-02 21:24:48
  • JetBrains 产品输入激活码 Key is invalid 完美解决方案

    2023-11-08 08:57:55
  • Java字符判断的小例子

    2023-08-26 17:29:49
  • JAVA如何按字节截取字符串

    2023-11-25 13:31:41
  • flutter实现发送验证码功能

    2023-07-05 19:03:12
  • Java 超详细讲解数据结构中的堆的应用

    2021-08-19 08:10:53
  • eclipse如何搭建Springboot项目详解

    2023-11-29 04:52:36
  • java字符串的重要使用方法以及实例

    2023-11-13 23:11:51
  • JAVA.io读写文件方式汇总

    2022-10-14 06:32:21
  • asp之家 软件编程 m.aspxhome.com