• Zookeeper事务日志预分配空间解析


    前言:

    Zookeeper的通过快照日志和事务日志将内存信息保存下来,记录下来每次请求的具体信息。

    尤其是其事务日志,每次处理事务请求时都需要将其记录下来。

    Zookeeper事务日志的默认存储方式是磁盘文件,那么Zookeeper的总体性能就受限与磁盘文件的写入速度。

    针对这个瓶颈,Zookeeper做了什么优化操作呢,本文我们就一起来了解下。

    1.事务日志的预分配

    事务日志的添加,我们需要从FileTxnLog.append()方法看起

    1. public class FileTxnLog implements TxnLog {
    2. volatile BufferedOutputStream logStream = null;
    3. volatile OutputArchive oa;
    4. volatile FileOutputStream fos = null;
    5. // 追加事务日志
    6. public synchronized boolean append(TxnHeader hdr, Record txn)
    7. throws IOException
    8. {
    9. if (hdr == null) {
    10. return false;
    11. }
    12. if (hdr.getZxid() <= lastZxidSeen) {
    13. LOG.warn("Current zxid " + hdr.getZxid()
    14. + " is <= " + lastZxidSeen + " for "
    15. + hdr.getType());
    16. } else {
    17. lastZxidSeen = hdr.getZxid();
    18. }
    19. // 默认logStream为空
    20. if (logStream==null) {
    21. if(LOG.isInfoEnabled()){
    22. LOG.info("Creating new log file: " + Util.makeLogName(hdr.getZxid()));
    23. }
    24. // 以下代码为创建事务日志文件
    25. // 根据当前事务ID来创建具体文件名,并写入文件头信息
    26. logFileWrite = new File(logDir, Util.makeLogName(hdr.getZxid()));
    27. fos = new FileOutputStream(logFileWrite);
    28. logStream=new BufferedOutputStream(fos);
    29. oa = BinaryOutputArchive.getArchive(logStream);
    30. FileHeader fhdr = new FileHeader(TXNLOG_MAGIC,VERSION, dbId);
    31. fhdr.serialize(oa, "fileheader");
    32. // Make sure that the magic number is written before padding.
    33. logStream.flush();
    34. filePadding.setCurrentSize(fos.getChannel().position());
    35. streamsToFlush.add(fos);
    36. }
    37. // 预分配代码在这里
    38. filePadding.padFile(fos.getChannel());
    39. byte[] buf = Util.marshallTxnEntry(hdr, txn);
    40. if (buf == null || buf.length == 0) {
    41. throw new IOException("Faulty serialization for header " +
    42. "and txn");
    43. }
    44. Checksum crc = makeChecksumAlgorithm();
    45. crc.update(buf, 0, buf.length);
    46. oa.writeLong(crc.getValue(), "txnEntryCRC");
    47. Util.writeTxnBytes(oa, buf);
    48. return true;
    49. }
    50. }

    创建FileTxnLog对象时,其logStream属性为null,所以当第一次处理事务请求时,会先根据当前事务ID来创建一个文件。

    1.1 事务日志预分配

    1. public class FilePadding {
    2. long padFile(FileChannel fileChannel) throws IOException {
    3. // 针对新文件而言,newFileSize=64M
    4. long newFileSize = calculateFileSizeWithPadding(fileChannel.position(), currentSize, preAllocSize);
    5. if (currentSize != newFileSize) {
    6. // 将文件扩充到64M,全部用0来填充
    7. fileChannel.write((ByteBuffer) fill.position(0), newFileSize - fill.remaining());
    8. currentSize = newFileSize;
    9. }
    10. return currentSize;
    11. }
    12. // size计算
    13. public static long calculateFileSizeWithPadding(long position, long fileSize, long preAllocSize) {
    14. // If preAllocSize is positive and we are within 4KB of the known end of the file calculate a new file size
    15. // 初始时候position=0,fileSize为0,preAllocSize为系统参数执行,默认为64M
    16. if (preAllocSize > 0 && position + 4096 >= fileSize) {
    17. // If we have written more than we have previously preallocated we need to make sure the new
    18. // file size is larger than what we already have
    19. // Q:这里确实没看懂...
    20. if (position > fileSize) {
    21. fileSize = position + preAllocSize;
    22. fileSize -= fileSize % preAllocSize;
    23. } else {
    24. fileSize += preAllocSize;
    25. }
    26. }
    27. return fileSize;
    28. }
    29. }

    预分配的过程比较简单,就是看下当前文件的剩余空间是否<4096,如果是,则扩容。

    Q:

    这里有一个不太明白的问题,position > fileSize的场景是怎样的呢?

    2.创建新的事务日志文件时机

    通过上述代码分析我们知道,当logStream=null时,就会创建一个新的事务日志文件,那么logStream对象什么时候为空呢?

    搜索代码,只看到FileTxnLog.rollLog()方法会主动将logStream设置为null

    1. public class FileTxnLog implements TxnLog {
    2. public synchronized void rollLog() throws IOException {
    3. if (logStream != null) {
    4. this.logStream.flush();
    5. this.logStream = null;
    6. oa = null;
    7. }
    8. }
    9. }

    那么根据这个线索,我们来搜索下rollLog的调用链

    SyncRequestProcessor.run() -> ZKDatabase.rollLog() -> FileTxnSnapLog.rollLog() -> FileTxnLog.rollLog()

    最终看到是在SyncRequestProcessor.run()方法中发起调用的,而且只有这一条调用链,我们来分析下

    2.1 SyncRequestProcessor.run()

    1. public class SyncRequestProcessor extends ZooKeeperCriticalThread implements RequestProcessor {
    2. public void run() {
    3. try {
    4. int logCount = 0;
    5. setRandRoll(r.nextInt(snapCount/2));
    6. while (true) {
    7. ...
    8. if (si != null) {
    9. // 追加事务日志
    10. if (zks.getZKDatabase().append(si)) {
    11. logCount++;
    12. if (logCount > (snapCount / 2 + randRoll)) {
    13. setRandRoll(r.nextInt(snapCount/2));
    14. // 注意:在这里发起了rollLog
    15. zks.getZKDatabase().rollLog();
    16. ...
    17. }
    18. } else if (toFlush.isEmpty()) {
    19. ...
    20. }
    21. toFlush.add(si);
    22. if (toFlush.size() > 1000) {
    23. flush(toFlush);
    24. }
    25. }
    26. }
    27. } catch (Throwable t) {
    28. handleException(this.getName(), t);
    29. running = false;
    30. }
    31. LOG.info("SyncRequestProcessor exited!");
    32. }
    33. }

    需要注意下rollLog()方法执行的条件,就是logCount > (snapCount / 2 + randRoll)

    snapCount是一个系统参数,System.getProperty("zookeeper.snapCount"),默认值为100000

    randRoll是一个随机值

    那么该条件触发的时机为:处理的事务请求数至少要大于50000。

    这时就出现了一个笔者无法理解的情况:

    通过对事务日志的观察可以看到其都是64M,而至少处理50000次事务请求后,Zookeeper才会分配一个新的事务日志文件,那么这个snapCount是一个经验值嘛?

    如果事务请求的value信息都很大,那么可能到不了50000次,就会超过64M,理论上应该要创建一个新的文件了,但是貌似并没有,这个该怎么处理呢?

    如果事务请求value信息都很小,那么即使到了50000次,也不会超过64M,那么之前预分配的文件大小就浪费了一部分。

    总结:

    希望有比较懂的小伙伴给点意见,感谢!

  • 相关阅读:
    【正则表达式】
    卡那霉素(Kanamycin偶联卵清白蛋白 (KAN-OVA)
    小白学java
    【PyTorch实战】用RNN写诗
    Centos7安装黑客矩阵特效软件cmatrix
    VS2022+opencv4.6.0安装和配置过程
    Zookeeper
    基于SpringBoot+Vue的餐饮管理系统设计与实现
    你掉进过新技术的“大坑”吗?
    启动IIS管理器上网站内容报错“万维网发布服务(W3SVC)已停止。除非万维网发布服务(W3SVC)正在运行,否则无法启动网站”
  • 原文地址:https://blog.csdn.net/qq_26323323/article/details/128158332