问题
-
由于RocketMQ操作CommitLog、ConsumeQueue文件,都是基于内存映射方法并在启动的时候,会加载commitlog、ConsumeQueue目录下的所有文件,为了避免内存与磁盘的浪费,不可能将消息永久存储在消息服务器上,所以需要一种机制来删除已过期的文件。
-
如果让你来实现怎么实现,思考一下,借鉴于Redis删除策略,惰性删除和定时删除,惰性删除呢,因为消息可以查询之前的,所以也不建议此方法,那么就剩下定时删除了,启动一个定时任务然后超了某个时间之后进行删除。
- 定时任务
- 获取文件最后修改时间
- 是否超了某个时间点,超了则删除,继续下一个文件
-
下面看看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
-
大体分为两步
- 达到了删除的条件
- 进行删除逻辑
删除条件判断
-
获取一些基本的变量信息
- fileReservedTime:文件保留时间,也就是从最后一次更新时间到现在,如果超过了该时间,则认为是过期文件,可以被删除。
- deletePhysicFilesInterval:删除物理文件的间隔,因为在一次清除过程中,可能需要删除的文件不止一个,该值指定两次删除文件的间隔时间。
- 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();
-
判断条件
- 是否时间到了,RocketMQ通过deleteWhen设置一天的固定时间执行一次删除过期文件操作,默认为凌晨4点。
- 判断磁盘空间是否充足,如果不充足,则返回true,表示应该触发过期文件删除操作。
- 预留,手工触发,可以通过调用excuteDeleteFilesManualy方法手工触发过期文件删除,目前RocketMQ暂未封装手工触发文件删除的命令
isSpaceToDelete
-
获取文件删除的临界点,超了次临界点,删除文件
scss//K1 获取maxUsedSpaceRatio,表示commitlog、consumequeue文件所在磁盘分区的最大使用量,如果超过该值,则需要立即清除过期文件。 double ratio = DefaultMessageStore.this.getMessageStoreConfig().getDiskMaxUsedSpaceRatio() / 100.0;
-
删除标记位,注意此对象是属性字段,肯定后面删除的时候,也会判断此值
inicleanImmediately = false;
-
获取commit的所有存储路径,然后依次进行判断,存在两个非常重要的值
- diskSpaceWarningLevelRatio=0.90:如果磁盘分区使用率超过该阔值,将设置磁盘不可写,此时会拒绝新消息的写入。
- diskSpaceCleanForciblyRatio=0.85:如果磁盘分区使用超过该阔值,建议立即执行过期文件清除,但不会拒绝新消息的写入。
iniString 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); } }
-
判断磁盘是否可用,用当前已使用物理磁盘率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; }
删除逻辑
-
上面三种情况任一满足,则进行清理工作
scssif (timeup || spacefull || manualDelete) { //.....清理工作...... }
-
判断是否马上清理,此处看到了之前设置的 cleanImmediately字段信息
kotlinboolean cleanAtOnce = DefaultMessageStore.this.getMessageStoreConfig().isCleanFileForciblyEnable() && this.cleanImmediately;
-
清理过期文件
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...