前面的几章已经讲了Broker会创建commitlog、cq和index三类文件来存储消息,使用户能够快速找到消息内容。为了防止存储内容日渐庞大,得定期给清理掉历史文件,本章就是从源码角度来讲解Broker如何清理历史文件。还是先从方法调用链开始:
DefaultMessageStore.start
->DefaultMessageStore.addScheduleTask
->DefaultMessageStore.this.cleanFilesPeriodically() // 定时任务异步执行
->cleanCommitLogService.run() // commitlog文件的清理
->cleanConsumeQueueService.run() // cq文件清理
addScheduleTask 定时任务异步执行
private void addScheduleTask() {
// 初始延迟60s,每10秒执行一次清理历史文件的操作
this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
// 历史文件清理
DefaultMessageStore.this.cleanFilesPeriodically();
}
}, 1000 * 60, this.messageStoreConfig.getCleanResourceInterval(), TimeUnit.MILLISECONDS);
// 初始延迟1ms,每10分钟执行一次操作
this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
// Broker自身的一些合法性检查,比如commitlog文件名,有兴趣的读者可以自行进入研究
DefaultMessageStore.this.checkSelf();
}
}, 1, 10, TimeUnit.MINUTES);
// 初始延迟1ms,每1秒执行一次操作
this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
if (DefaultMessageStore.this.getMessageStoreConfig().isDebugLockEnable()) {
try {
if (DefaultMessageStore.this.commitLog.getBeginTimeInLock() != 0) {
// 针对写文件时,加锁时间太久的操作,打印线程堆栈信息,方便debug调试
long lockTime = System.currentTimeMillis() - DefaultMessageStore.this.commitLog.getBeginTimeInLock();
if (lockTime > 1000 && lockTime < 10000000) {
String stack = UtilAll.jstack();
final String fileName = System.getProperty("user.home") + File.separator + "debug/lock/stack-"
+ DefaultMessageStore.this.commitLog.getBeginTimeInLock() + "-" + lockTime;
MixAll.string2FileNotSafe(stack, fileName);
}
}
} catch (Exception e) {
}
}
}
}, 1, 1, TimeUnit.SECONDS);
// this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
// @Override
// public void run() {
// DefaultMessageStore.this.cleanExpiredConsumerQueue();
// }
// }, 1, 1, TimeUnit.HOURS);
}
15.1 commitlog过期清理
private void deleteExpiredFiles() {
int deleteCount = 0;
// 文件保留时长,默认72小时
long fileReservedTime = DefaultMessageStore.this.getMessageStoreConfig().getFileReservedTime();
// 删除文件的时间间隔,默认100ms
int deletePhysicFilesInterval = DefaultMessageStore.this.getMessageStoreConfig().getDeleteCommitLogFilesInterval();
// 强制销毁映射文件的时间间隔,默认是2分钟(1000 * 120ms),这个值是针对映射文件被多次引用的情况
int destroyMapedFileIntervalForcibly = DefaultMessageStore.this.getMessageStoreConfig().getDestroyMapedFileIntervalForcibly();
// 决定什么时候开始执行删除,默认是04,代表凌晨4点
boolean timeup = this.isTimeToDelete();
// 判断磁盘空间是否不足,默认使用大于85%
boolean spacefull = this.isSpaceToDelete();
// 手动设置的删除次数,默认是0
boolean manualDelete = this.manualDeleteFileSeveralTimes > 0;
// 时间到了、磁盘空间不够了、手动删除,三者有一个满足,就可以继续执行
if (timeup || spacefull || manualDelete) {
if (manualDelete)
this.manualDeleteFileSeveralTimes--;
// 立即清除标识
boolean cleanAtOnce = DefaultMessageStore.this.getMessageStoreConfig().isCleanFileForciblyEnable() && this.cleanImmediately;
// 日志打印
log.info("begin to delete before {} hours file. timeup: {} spacefull: {} manualDeleteFileSeveralTimes: {} cleanAtOnce: {}",
fileReservedTime,
timeup,
spacefull,
manualDeleteFileSeveralTimes,
cleanAtOnce);
// 72 小时换算成毫秒
fileReservedTime *= 60 * 60 * 1000;
// 删除过期文件
deleteCount = DefaultMessageStore.this.commitLog.deleteExpiredFile(fileReservedTime, deletePhysicFilesInterval,
destroyMapedFileIntervalForcibly, cleanAtOnce);
if (deleteCount > 0) {
} else if (spacefull) {
log.warn("disk space will be full soon, but delete file failed.");
}
}
}
CommitLog.deleteExpiredFile
public int deleteExpiredFile(
final long expiredTime,
final int deleteFilesInterval,
final long intervalForcibly,
final boolean cleanImmediately
) {
// 继续调用
return this.mappedFileQueue.deleteExpiredFileByTime(expiredTime, deleteFilesInterval, intervalForcibly, cleanImmediately);
}
MappedFileQueue.deleteExpiredFileByTime
public int deleteExpiredFileByTime(final long expiredTime,
final int deleteFilesInterval,
final long intervalForcibly,
final boolean cleanImmediately) {
// 把所有的commitlog的映射文件对象,复制成Object数组
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];
// 该存活的最大时间戳,文件最后修改时间 + 过期时间(72小时)
long liveMaxTimestamp = mappedFile.getLastModifiedTimestamp() + expiredTime;
// 当前时间大于该存活的时间 或者 cleanImmediately标识为true
if (System.currentTimeMillis() >= liveMaxTimestamp || cleanImmediately) {
// destroy 主要做三件事:1.释放mmap的文件映射;2.关闭file channel;3.真实删除物理文件
if (mappedFile.destroy(intervalForcibly)) {
// 释放完成,该文件就可以删除了,放入files列表中
files.add(mappedFile);
// 删除数量+1
deleteCount++;
// 一次删除文件最大数量不能超过 DELETE_FILES_BATCH_MAX,默认是10
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;
}
}
}
// 批量删除文件
deleteExpiredFile(files);
return deleteCount;
}
批量删除文件
从下面的源码可以看出,这里应该叫清除缓存中的映射文件对象,真实的物理文件已经在destroy方法中删除了
void deleteExpiredFile(List<MappedFile> files) {
// 文件不空
if (!files.isEmpty()) {
Iterator<MappedFile> iterator = files.iterator();
while (iterator.hasNext()) {
MappedFile cur = iterator.next();
if (!this.mappedFiles.contains(cur)) {
iterator.remove();
log.info("This mappedFile {} is not contained by mappedFiles, so skip it.", cur.getFileName());
}
}
try {
// 缓存中清除
if (!this.mappedFiles.removeAll(files)) {
log.error("deleteExpiredFile remove failed.");
}
} catch (Exception e) {
log.error("deleteExpiredFile has exception.", e);
}
}
}
15.2 cq和index过期清理
class CleanConsumeQueueService {
private long lastPhysicalMinOffset = 0;
public void run() {
try {
// 执行删除过期文件操作
this.deleteExpiredFiles();
} catch (Throwable e) {
DefaultMessageStore.log.warn(this.getServiceName() + " service has exception. ", e);
}
}
private void deleteExpiredFiles() {
// 每次执行的间隔时间,默认100ms
int deleteLogicsFilesInterval = DefaultMessageStore.this.getMessageStoreConfig().getDeleteConsumeQueueFilesInterval();
long minOffset = DefaultMessageStore.this.commitLog.getMinOffset();
if (minOffset > this.lastPhysicalMinOffset) {
this.lastPhysicalMinOffset = minOffset;
ConcurrentMap<String, ConcurrentMap<Integer, ConsumeQueue>> tables = DefaultMessageStore.this.consumeQueueTable;
for (ConcurrentMap<Integer, ConsumeQueue> maps : tables.values()) {
for (ConsumeQueue logic : maps.values()) {
// 直接看这一行,里面的逻辑跟commitlog过期清理是一样的,唯一的区别是cq的删除是根据commitlog的最小物理偏移值来确定,所以就不冗余讲解了,有兴趣的读者自行研究
int deleteCount = logic.deleteExpiredFile(minOffset);
if (deleteCount > 0 && deleteLogicsFilesInterval > 0) {
try {
Thread.sleep(deleteLogicsFilesInterval);
} catch (InterruptedException ignored) {
}
}
}
}
// index文件的删除,同样也要根据commitlog的最小物理偏移来确定删除操作
DefaultMessageStore.this.indexService.deleteExpiredFile(minOffset);
}
}
public String getServiceName() {
return CleanConsumeQueueService.class.getSimpleName();
}
}