1.爲什麼會有文件過期刪除機制
由於RocketMQ操作CommitLog、ConsumeQueue文件是基於文件內存映射機制,並且在啓動的時候會將所有的文件加載,爲了避免內存與磁盤的浪費、能夠讓磁盤能夠循環利用、避免因爲磁盤不足導致消息無法寫入等引入了文件過期刪除機制
2.RocketMQ刪除過期文件的思路
RocketMQ順序寫CommitLog文件、ComsumeQueue文件,所有的寫操作都會落到最後一個文件上,因此在當前寫文件之前的文件將不會有數據插入,也就不會有任何變動,因此可通過時間來做判斷,比如超過72小時未更新的文件將會被刪除
PS:RocketMQ刪除過期文件時不會關注該文件的內容是否全部被消費
3.文件過期刪除機制實現
3.1 觸發刪除的操作
image.png
由上圖可知,觸發文件清除操作的是一個定時任務,而且只有定時任務
// Resource reclaim interval
private int cleanResourceInterval = 10000;
文件過期刪除定時任務的週期由該刪除決定,默認每10s執行一次
3.2 刪除源碼分析
MesssageSDefaultStore#CleanCommitLogService#deleteExpiredFiles
private void deleteExpiredFiles() {
//省略
}
我們一點一點來分析其中的代碼
long fileReservedTime = DefaultMessageStore.this.getMessageStoreConfig().getFileReservedTime();
int deletePhysicFilesInterval = DefaultMessageStore.this.getMessageStoreConfig().getDeleteCommitLogFilesInterval();
int destroyMapedFileIntervalForcibly = DefaultMessageStore.this.getMessageStoreConfig().getDestroyMapedFileIntervalForcibly();
上面三個屬性需要重點講解下
- fileReservedTime:文件過期時間,也就是從文件最後一次的更新時間到現在爲止,如果超過該時間,則是過期文件可被刪除
- deletePhysicFilesInterval:刪除物理文件的時間間隔,在一次定時任務觸發時,可能會有多個物理文件超過過期時間可被刪除,因此刪除一個文件後需要間隔deletePhysicFilesInterval這個時間再刪除另外一個文件,我猜測可能是由於刪除文件是一個非常耗費IO的操作,會引起消息插入消費的延遲(相比於正常情況下),所以不建議直接刪除所有過期文件
- destroyMapedFileIntervalForcibly:在刪除文件時,如果該文件還被線程引用,此時會阻止此次刪除操作,同時將該文件標記不可用並且紀錄當前時間戳destroyMapedFileIntervalForcibly這個表示文件在第一次刪除拒絕後,文件保存的最大時間,在此時間內一直會被拒絕刪除,當超過這個時間時,會將引用每次減少1000,直到引用 小於等於 0爲止,即可刪除該文件,實現代碼如下:
ReferenceResource#shutdown
public void shutdown(final long intervalForcibly) {
if (this.available) {
this.available = false;
this.firstShutdownTimestamp = System.currentTimeMillis();
this.release();
} else if (this.getRefCount() > 0) {
if ((System.currentTimeMillis() - this.firstShutdownTimestamp) >= intervalForcibly) {
this.refCount.set(-1000 - this.getRefCount());
this.release();
}
}
}
MesssageSDefaultStore#CleanCommitLogService#deleteExpiredFiles
boolean timeup = this.isTimeToDelete();
boolean spacefull = this.isSpaceToDelete();
boolean manualDelete = this.manualDeleteFileSeveralTimes > 0;
if (timeup || spacefull || manualDelete) {
//執行刪除邏輯
}
-
image.png
UtilAll#isItTimeToDo
public static boolean isItTimeToDo(final String when) {
String[] whiles = when.split(";");
if (whiles.length > 0) {
Calendar now = Calendar.getInstance();
for (String w : whiles) {
int nowHour = Integer.parseInt(w);
if (nowHour == now.get(Calendar.HOUR_OF_DAY)) {
return true;
}
}
}
return false;
}
-
spacefull:磁盤空間是否充足,磁盤不足返回true,執行過期文件刪除策略,我們進去看看this.isSpaceToDelete()這個方法
MesssageSDefaultStore#CleanCommitLogService#isSpaceToDelete
double ratio = DefaultMessageStore.this.getMessageStoreConfig().getDiskMaxUsedSpaceRatio() / 100.0; //是否立即清除 cleanImmediately = false; { String storePathPhysic = DefaultMessageStore.this.getMessageStoreConfig().getStorePathCommitLog(); double physicRatio = UtilAll.getDiskPartitionSpaceUsedPercent(storePathPhysic); if (physicRatio > diskSpaceWarningLevelRatio) { /**#1*/ boolean diskok = DefaultMessageStore.this.runningFlags.getAndMakeDiskFull(); if (diskok) { DefaultMessageStore.log.error("physic disk maybe full soon " + physicRatio + ", so mark disk full"); } cleanImmediately = true; } else if (physicRatio > diskSpaceCleanForciblyRatio) {/**#2*/ cleanImmediately = true; } else {/**#3*/ boolean diskok = DefaultMessageStore.this.runningFlags.getAndMakeDiskOK(); if (!diskok) { DefaultMessageStore.log.info("physic disk space OK " + physicRatio + ", so mark disk ok"); } } if (physicRatio < 0 || physicRatio > ratio) {/**#4*/ DefaultMessageStore.log.info("physic disk maybe full soon, so reclaim space, " + physicRatio); return true; } }
1:物理使用率大於diskSpaceWarningLevelRatio(默認90%可通過參數設置),則會阻止新消息的插入
2:物理使用率大於diskSpaceCleanForciblyRatio(默認85%,可設置),則過進行過期物理文件的刪除
3:恢復磁盤可寫 配合 #1使用
4:物理磁盤使用率小於diskMaxUsedSpaceRatio 表示磁盤使用正常
-
manualDelete:預留,手工觸發,目前rocketmq暫未封裝
MappedFileQueue#deleteExpiredFileByTime
for (int i = 0; i < mfsLength; i++) {
MappedFile mappedFile = (MappedFile) mfs[i];
long liveMaxTimestamp = mappedFile.getLastModifiedTimestamp() + expiredTime;
if (System.currentTimeMillis() >= liveMaxTimestamp || cleanImmediately) {
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;
}
}
image.png
從第一個文件開始遍歷,判斷該文件的最大存活時間(該文件的最後一次更新時間+文件的存活時間)小於 當前系統時間 或者 需要強制刪除文件(上面有講,哪些情況下強制刪除),則執行MappedFile#destroy 方法清除MappedFile佔用的相關資源,如果執行成功則將該文件加入待刪除文件列表中統一從磁盤中刪除。DELETE_FILES_BATCH_MAX 決定每次能夠刪除的文件個數上限、deleteFilesInterval連續清除兩個文件的時間間隔由該參數決定(deletePhysicFilesInterval)
4.總結-整體流程
- 開啓定時任務每10s掃描是否有文件需要刪除
- 有三種情況會進入刪除文件操作:到了deleteWhere指定的時間點(默認是凌晨4點)、磁盤不足、手動觸發
- 對於磁盤不足的情況,當磁盤使用率大於磁盤空間警戒線水位(默認是90%),會阻止消息寫入,當超過85%時會強制刪除文件(需要設置允許強制刪除參數,否者不生效),其他兩種情況都只能刪除過期的文件(文件最後更新時間+文件最大的存活時間 < 當前時間)
- 當被刪除的文件存在引用時,會有一個文件刪除緩存時間,在這段時間內,該文件不會被刪除,主要是留給引用該文件程序一些時間,當超過了文件刪除緩存時間後,每次都會將該文件的引用減少1000,直到減少小於等於0後才釋放該文件引用的相關資源,然後將該文件放入一個“文件刪除集合”中
- 一次連續刪除文件中間會存在一定的間隔,不會連續釋放文件相關的資源
- 一次連續刪除的文件和不大於10
- 將“文件刪除集合”中的文件從