RocketMQ 源码学习--重要机制-01 过期文件删除源码解析

问题

  1. 由于RocketMQ操作CommitLog、ConsumeQueue文件,都是基于内存映射方法并在启动的时候,会加载commitlog、ConsumeQueue目录下的所有文件,为了避免内存与磁盘的浪费,不可能将消息永久存储在消息服务器上,所以需要一种机制来删除已过期的文件。

  2. 如果让你来实现怎么实现,思考一下,借鉴于Redis删除策略,惰性删除和定时删除,惰性删除呢,因为消息可以查询之前的,所以也不建议此方法,那么就剩下定时删除了,启动一个定时任务然后超了某个时间之后进行删除。

    1. 定时任务
    2. 获取文件最后修改时间
    3. 是否超了某个时间点,超了则删除,继续下一个文件
  3. 下面看看RocketMQ是如何实现的

原理

  • RocketMQ顺序写Commitlog、ConsumeQueue文件,所有写操作全部落在最后一个CommitLog或ConsumeQueue文件上,之前的文件在下一个文件创建后,将不会再被更新。
  • RocketMQ清除过期文件的方法是:如果非当前写文件在一定时间间隔内没有再次被更新,则认为是过期文件,可以被删除,RocketMQ不会管这个这个文件上的消息是否被全部消费 。默认每个文件的过期时间为72小时。通过在Broker配置文件中设置fileReservedTime来改变过期时间,单位为小时。接下来详细分析RocketMQ是如何设计与实现上述机制的。想一个问题,是否存在这么一个情况,消息还在consumerQueue中,但是CommitLog重已经没有了,是不是需要保证CommitLog和ConsumerQueue同步删除

源码

入口在 DefaultMessageStore中,直接看start方法 --> this.addScheduleTask();

主要清除CommitLog、ConsumeQueue的过期文件。CommitLog 与 ConsumeQueue 对于过期文件的删除算法

addScheduleTask

  • RocketMQ 会每隔10s调度一次cleanFilesPeriodically,已检测是否需要清除过期文件。执行频率可以通过设置cleanResourceInterval,默认为10s。
csharp 复制代码
​
private void addScheduleTask() {
​
  this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
    @Override
    public void run() {
      //阶段性的删除文件
      DefaultMessageStore.this.cleanFilesPeriodically();
    }
  }, 1000 * 60, this.messageStoreConfig.getCleanResourceInterval(), TimeUnit.MILLISECONDS);
  //。。。。省略其他定时任务。。。。
}

cleanFilesPeriodically()

  • 主要清除CommitLog、ConsumeQueue的过期文件。CommitLog 与 ConsumeQueue 对于过期文件的删除算法
csharp 复制代码
private void cleanFilesPeriodically() {
  this.cleanCommitLogService.run();
  this.cleanConsumeQueueService.run();
}
  • 通过之前的学习,肯定知道这个 Service肯定是 ServiceThread实现类,也就是Thread,此处的run呢,这个就要看看基础了,可不是start方法哈。但是,仔细看,其实啥也不是,就是一个普通的类,只是里面有个run方法而已。额。。。。。。
  • 由于两个Service逻辑类似,只去关注commitLog方法了

CleanCommitLogService

  • 分成了两步

    • 第一个步骤:尝试删除过期文件
    • 第二个步骤:删除上一步落网之鱼,重试删除被hange(由于被其他线程引用在第一阶段未删除的文件),在这里再重试一次。
kotlin 复制代码
class CleanCommitLogService {
​
  //省略了一些属性
​
  public void run() {
    try {
​
      //K1 删除过期文件
      this.deleteExpiredFiles();
​
      //k1 删除被挂起的文件
      this.redeleteHangedFile();
    } catch (Throwable e) {
      DefaultMessageStore.log.warn(this.getServiceName() + " service has exception. ", e);
    }
  }
}

deleteExpiredFiles

  • 大体分为两步

    1. 达到了删除的条件
    2. 进行删除逻辑

删除条件判断

  1. 获取一些基本的变量信息

    1. fileReservedTime:文件保留时间,也就是从最后一次更新时间到现在,如果超过了该时间,则认为是过期文件,可以被删除。
    2. deletePhysicFilesInterval:删除物理文件的间隔,因为在一次清除过程中,可能需要删除的文件不止一个,该值指定两次删除文件的间隔时间。
    3. destroyMapedFileIntervalForcibly:在清除过期文件时,如果该文件被其他线程所占用(引用次数大于0,比如读取消息),此时会阻止此次删除任务,同时在第一次试图删除该文件时记录当前时间戳,destroyMapedFileIntervalForcibly表示第一次拒绝删除之后能保留的最大时间,在此时间内,同样可以被拒绝删除,同时会将引用减少1000个,超过该时间间隔后,文件将被强制删除。(上面的东西,感觉有点虚,什么引用啥的,后面继续看看
    arduino 复制代码
    //文件保留时间,也就是从最后一次更新时间到现在,如果超过了该时间,则认为是过期文件,可以被删除。
    long fileReservedTime = DefaultMessageStore.this.getMessageStoreConfig().getFileReservedTime();
    ​
    //删除物理文件的间隔,因为在一次清除过程中,可能需要删除的文件不止一个,该值指定两次删除文件的间隔时间。
    int deletePhysicFilesInterval = DefaultMessageStore.this.getMessageStoreConfig().getDeleteCommitLogFilesInterval();
    ​
    ​
    //无敌时间:表示第一次拒绝删除之后能保留的最大时间
    //   在清除过期文件时,如果该文件被其他线程所占用(引用次数大于0,比如读取消息),此时会阻止此次删除任务,同时在第一次试图删除该文件时记录当前时间戳,
    //   destroyMapedFileIntervalForcibly表示第一次拒绝删除之后能保留的最大时间,在此时间内,同样可以被拒绝删除,同时会将引用减少1000个,
    //   超过该时间间隔后,文件将被强制删除。
    int destroyMapedFileIntervalForcibly = DefaultMessageStore.this.getMessageStoreConfig().getDestroyMapedFileIntervalForcibly();
  2. 判断条件

    1. 是否时间到了,RocketMQ通过deleteWhen设置一天的固定时间执行一次删除过期文件操作,默认为凌晨4点。
    2. 判断磁盘空间是否充足,如果不充足,则返回true,表示应该触发过期文件删除操作。
    3. 预留,手工触发,可以通过调用excuteDeleteFilesManualy方法手工触发过期文件删除,目前RocketMQ暂未封装手工触发文件删除的命令
isSpaceToDelete
  1. 获取文件删除的临界点,超了次临界点,删除文件

    scss 复制代码
    //K1 获取maxUsedSpaceRatio,表示commitlog、consumequeue文件所在磁盘分区的最大使用量,如果超过该值,则需要立即清除过期文件。
    double ratio = DefaultMessageStore.this.getMessageStoreConfig().getDiskMaxUsedSpaceRatio() / 100.0;
  2. 删除标记位,注意此对象是属性字段,肯定后面删除的时候,也会判断此值

    ini 复制代码
    cleanImmediately = false;
  3. 获取commit的所有存储路径,然后依次进行判断,存在两个非常重要的值

    1. diskSpaceWarningLevelRatio=0.90:如果磁盘分区使用率超过该阔值,将设置磁盘不可写,此时会拒绝新消息的写入。
    2. diskSpaceCleanForciblyRatio=0.85:如果磁盘分区使用超过该阔值,建议立即执行过期文件清除,但不会拒绝新消息的写入。
    ini 复制代码
    String commitLogStorePath = DefaultMessageStore.this.getStorePathPhysic();
    String[] storePaths = commitLogStorePath.trim().split(MessageStoreConfig.MULTI_PATH_SPLITTER);
    Set<String> fullStorePath = new HashSet<>();
    ​
    //最小的分区比例
    double minPhysicRatio = 100;
    String minStorePath = null;
    ​
    //K2 获取所有的存储文件路径
    for (String storePathPhysic : storePaths) {
    ​
      //K2 获取当前物理磁盘使用率:获取commitlog所在磁盘分区总的存储容量
      double physicRatio = UtilAll.getDiskPartitionSpaceUsedPercent(storePathPhysic);
    ​
      //不断更新最小值
      if (minPhysicRatio > physicRatio) {
        minPhysicRatio =  physicRatio;
        minStorePath = storePathPhysic;
      }
    ​
      //IMP
      //   diskSpaceWarningLevelRatio=0.90:如果磁盘分区使用率超过该阔值,将设置磁盘不可写,此时会拒绝新消息的写入。
      //   diskSpaceCleanForciblyRatio=0.85:如果磁盘分区使用超过该阔值,建议立即执行过期文件清除,但不会拒绝新消息的写入。
      //K2 如果磁盘分区使用超过该阔值,建议立即执行过期文件清除,但不会拒绝新消息的写入。
      if (physicRatio > diskSpaceCleanForciblyRatio) {
        fullStorePath.add(storePathPhysic);
      }
    }
  4. 判断磁盘是否可用,用当前已使用物理磁盘率maxUsedSpaceRatio、diskSpaceWarningLevelRatio、diskSpaceCleanForciblyRatio,如果当前磁盘使用率达到上述阔值,将返回true表示磁盘已满,需要进行过期文件删除操作。此处将 cleanImmediately设置为了true

    kotlin 复制代码
    //IMP CommitLog设置哪些文件满了,但是没有针对文件进行禁止写入的情况
    DefaultMessageStore.this.commitLog.setFullStorePaths(fullStorePath);
    ​
    //K2 如果磁盘分区使用率超过该阔值,将设置磁盘不可写,此时会拒绝新消息的写入。
    if (minPhysicRatio > diskSpaceWarningLevelRatio) {
      boolean diskok = DefaultMessageStore.this.runningFlags.getAndMakeDiskFull();
      if (diskok) {
        DefaultMessageStore.log.error("physic disk maybe full soon " + minPhysicRatio +
                                      ", so mark disk full, storePathPhysic=" + minStorePath);
      }
    ​
      //IMP 立即清理
      cleanImmediately = true;
    ​
      //K2 如果磁盘分区使用超过该阔值,建议立即执行过期文件清除,但不会拒绝新消息的写入。
    } else if (minPhysicRatio > diskSpaceCleanForciblyRatio) {
    ​
      //IMP 立即清理
      cleanImmediately = true;
    } else {
    ​
      boolean diskok = DefaultMessageStore.this.runningFlags.getAndMakeDiskOK();
      if (!diskok) {
        DefaultMessageStore.log.info("physic disk space OK " + minPhysicRatio +
                                     ", so mark disk ok, storePathPhysic=" + minStorePath);
      }
    }
    ​
    if (minPhysicRatio < 0 || minPhysicRatio > ratio) {
      DefaultMessageStore.log.info("physic disk maybe full soon, so reclaim space, "
                                   + minPhysicRatio + ", storePathPhysic=" + minStorePath);
      return true;
    }

删除逻辑

  1. 上面三种情况任一满足,则进行清理工作

    scss 复制代码
    if (timeup || spacefull || manualDelete) {
      //.....清理工作......
    }
  2. 判断是否马上清理,此处看到了之前设置的 cleanImmediately字段信息

    kotlin 复制代码
    boolean cleanAtOnce = DefaultMessageStore.this.getMessageStoreConfig().isCleanFileForciblyEnable() && this.cleanImmediately;
  1. 清理过期文件

    ini 复制代码
    //IMP 清理过期文件
    deleteCount = DefaultMessageStore.this.commitLog.deleteExpiredFile(fileReservedTime, deletePhysicFilesInterval,
                                                                       destroyMapedFileIntervalForcibly, cleanAtOnce);
deleteExpiredFile
arduino 复制代码
public int deleteExpiredFile(
  final long expiredTime,
  final int deleteFilesInterval,
  final long intervalForcibly,
  final boolean cleanImmediately
) {
  return this.mappedFileQueue.deleteExpiredFileByTime(expiredTime, deleteFilesInterval, intervalForcibly, cleanImmediately);
}
java 复制代码
​
public int deleteExpiredFileByTime(final long expiredTime,
                                   final int deleteFilesInterval,
                                   final long intervalForcibly,
                                   final boolean cleanImmediately) {
  Object[] mfs = this.copyMappedFiles(0);
​
  if (null == mfs)
    return 0;
​
  int mfsLength = mfs.length - 1;
  int deleteCount = 0;
  List<MappedFile> files = new ArrayList<MappedFile>();
  if (null != mfs) {
    for (int i = 0; i < mfsLength; i++) {
      MappedFile mappedFile = (MappedFile) mfs[i];
      long liveMaxTimestamp = mappedFile.getLastModifiedTimestamp() + expiredTime;
​
      //K1 文件存储太久,或者,之前判断出的立即清理
      if (System.currentTimeMillis() >= liveMaxTimestamp || cleanImmediately) {
​
        //IMP 删除文件
        if (mappedFile.destroy(intervalForcibly)) {
          files.add(mappedFile);
          deleteCount++;
​
          if (files.size() >= DELETE_FILES_BATCH_MAX) {
            break;
          }
​
          if (deleteFilesInterval > 0 && (i + 1) < mfsLength) {
            try {
              Thread.sleep(deleteFilesInterval);
            } catch (InterruptedException e) {
            }
          }
        } else {
          break;
        }
      } else {
        //avoid deleting files in the middle
        break;
      }
    }
  }
​
  //K1 管理 mappedFiles 中的存储数据
  deleteExpiredFile(files);
​
  return deleteCount;
}
  • 两种条件执行清理

    • 文件已经存储很久了
    • 通过之前的判断,文件需要清理,
  • 有个疑问,是不是上面判断了,就必须清理文件呢,如果文件再被使用呢。。。。

MapperFile.destory
kotlin 复制代码
​
public boolean destroy(final long intervalForcibly) {
  this.shutdown(intervalForcibly);
​
  if (this.isCleanupOver()) {
    try {
      this.fileChannel.close();
      log.info("close file channel " + this.fileName + " OK");
​
      long beginTime = System.currentTimeMillis();
      boolean result = this.file.delete();
      log.info("delete file[REF:" + this.getRefCount() + "] " + this.fileName
               + (result ? " OK, " : " Failed, ") + "W:" + this.getWrotePosition() + " M:"
               + this.getFlushedPosition() + ", "
               + UtilAll.computeElapsedTimeMilliseconds(beginTime));
    } catch (Exception e) {
      log.warn("close file channel " + this.fileName + " Failed. ", e);
    }
​
    return true;
  } else {
    log.warn("destroy mapped file[REF:" + this.getRefCount() + "] " + this.fileName
             + " Failed. cleanupOver: " + this.cleanupOver);
  }
​
  return false;
}
shutDown
  • 注意几个点,一开始文件肯定是 available=true,一开始肯定是有用的,相当于一个开关,一开始是可用的,那么设置为不可用,那么文件处理的时候,肯定会判断,文件是否可用这样就可以关闭所有的写入程序。
  • 设置timestamp
  • 执行release方法。下面是挂起的逻辑,但是现在跑到了上面的逻辑岂不是就跑不到了。。。感觉有点不对呀,正在使用中的文件怎么办呢,岂不是就删除了
kotlin 复制代码
public void shutdown(final long intervalForcibly) {
​
  //K1 可以清理
  if (this.available) {
    //K2 设置available为false
    this.available = false;
​
    //K2 记录firstShutdownTimestamp 时间戳
    this.firstShutdownTimestamp = System.currentTimeMillis();
    this.release();
​
    //K1 如果被其他线程引用,本次不删除
  } else if (this.getRefCount() > 0) {
​
    //K2 在拒绝被删除保护期内(destroyMapedFileIntervalForcibly)每执行一次清理任务,将引用次数减去1000,引用数小于1后,该文件最终将被删除
    if ((System.currentTimeMillis() - this.firstShutdownTimestamp) >= intervalForcibly) {
      this.refCount.set(-1000 - this.getRefCount());
      this.release();
    }
  }
}

先看看如果走到了 else if的地方,那么肯定是文件已经设置了不可用了,而且文件还被某些文件正在使用中,那么如果超了间隔时间,就将引用减少1000,这样可以保证,超了一定时间之后,引用肯定会变低,还会执行release方法,该方法具体干了什么,瞅瞅

release方法
  • 好吧,原来,里面判断了引用数,如果被引用了,也就是设置avalialbe=false了,这样就很明显了,第一次来了,如果还有引用,那么只是更改了可用的标记位,并且引用减少,第二次来的时候,引用数还是大于0的,超了超时时间之后,就会执行cleanUp方法,然后执行文件的清理了。其实文件的清理的时候,也是用到了available进行进一步的判断,不得不说,模式设计的好,可以很好的控制所有的流程信息。
kotlin 复制代码
public void release() {
      long value = this.refCount.decrementAndGet();
​
      //IMP 引用大于0,不删除
      if (value > 0)
        return;
​
      synchronized (this) {
        this.cleanupOver = this.cleanup(value);
      }
    }
cleanUp
kotlin 复制代码
@Override
public boolean cleanup(final long currentRef) {
  
  //IMP 文件被设置了可用,就不去执行
  if (this.isAvailable()) {
    log.error("this file[REF:" + currentRef + "] " + this.fileName
              + " have not shutdown, stop unmapping.");
    return false;
  }
​
  if (this.isCleanupOver()) {
    log.error("this file[REF:" + currentRef + "] " + this.fileName
              + " have cleanup, do not do it again.");
    return true;
  }
​
  clean(this.mappedByteBuffer);
  TOTAL_MAPPED_VIRTUAL_MEMORY.addAndGet(this.fileSize * (-1));
  TOTAL_MAPPED_FILES.decrementAndGet();
  log.info("unmap file[REF:" + currentRef + "] " + this.fileName + " OK");
  return true;
}

引用学习链接:blog.csdn.net/prestigedin...

相关推荐
九圣残炎32 分钟前
【springboot】简易模块化开发项目整合Redis
spring boot·redis·后端
.生产的驴1 小时前
Electron Vue框架环境搭建 Vue3环境搭建
java·前端·vue.js·spring boot·后端·electron·ecmascript
爱学的小涛1 小时前
【NIO基础】基于 NIO 中的组件实现对文件的操作(文件编程),FileChannel 详解
java·开发语言·笔记·后端·nio
爱学的小涛1 小时前
【NIO基础】NIO(非阻塞 I/O)和 IO(传统 I/O)的区别,以及 NIO 的三大组件详解
java·开发语言·笔记·后端·nio
北极无雪1 小时前
Spring源码学习:SpringMVC(4)DispatcherServlet请求入口分析
java·开发语言·后端·学习·spring
爱码少年1 小时前
springboot工程中使用tcp协议
spring boot·后端·tcp/ip
2401_857622669 小时前
SpringBoot框架下校园资料库的构建与优化
spring boot·后端·php
2402_857589369 小时前
“衣依”服装销售平台:Spring Boot框架的设计与实现
java·spring boot·后端
哎呦没10 小时前
大学生就业招聘:Spring Boot系统的架构分析
java·spring boot·后端
_.Switch11 小时前
Python Web 应用中的 API 网关集成与优化
开发语言·前端·后端·python·架构·log4j