RocketMQ順序寫Commitlog、ConsumeQueue文件,所有寫操作全部落在最後一個CommitLog或ConsumeQueue文件上,之前的文件在下一個文件創建後,將不會再被更新。
RocketMQ清除過期文件的方法是:如果非當前寫文件在一定時間間隔內沒有再次被更新,則認爲是過期文件,可以被刪除,RocketMQ不會管這個這個文件上的消息是否被全部消費。默認每個文件的過期時間爲72小時。通過在Broker配置文件中設置fileReservedTime來改變過期時間,單位爲小時。接下來詳細分析RocketMQ是如何設計與實現上述機制的。
DefaultMessageStore#addScheduleTask:
this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
DefaultMessageStore.this.cleanFilesPeriodically();
}
}, 1000 * 60, this.messageStoreConfig.getCleanResourceInterval(), TimeUnit.MILLISECONDS);
RocketMQ 會每隔10s調度一次cleanFilesPeriodically,已檢測是否需要清除過期文件。執行頻率可以通過設置cleanResourceInterval,默認爲10s。
DefaultMessageStore#cleanFilesPeriodically
private void cleanFilesPeriodically() {
this.cleanCommitLogService.run();
this.cleanConsumeQueueService.run();
}
主要清除CommitLog、ConsumeQueue的過期文件。CommitLog 與 ConsumeQueue 對於過期文件的刪除算法、邏輯大同小異,本文將以 CommitLog 過期文件爲例來詳細分析其實現原理。
DefaultMessageStore$CleanCommitLogService#run
public void run() {
try {
this.deleteExpiredFiles();
this.redeleteHangedFile();
} catch (Throwable e) {
DefaultMessageStore.log.warn(this.getServiceName() + " service has
exception. ", e);
}
}
整個執行過程分爲兩個大的步驟,第一個步驟:嘗試刪除過期文件;第二個步驟:重試刪除被hange(由於被其他線程引用在第一階段未刪除的文件),在這裏再重試一次。
DefaultMessageStore$CleanCommitLogService#deleteExpiredFiles
long fileReservedTime = DefaultMessageStore.this.getMessageStoreConfig().getFileReservedTime();
int deletePhysicFilesInterval = DefaultMessageStore.this.getMessageStoreConfig().getDeleteCommitLogFilesInterval();
int destroyMapedFileIntervalForcibly = DefaultMessageStore.this.getMessageStoreConfig().getDestroyMapedFileIntervalForcibly();
Step1:解釋一下這個三個配置屬性的含義。
- fileReservedTime:文件保留時間,也就是從最後一次更新時間到現在,如果超過了該時間,則認爲是過期文件,可以被刪除。
- deletePhysicFilesInterval:刪除物理文件的間隔,因爲在一次清除過程中,可能需要刪除的文件不止一個,該值指定兩次刪除文件的間隔時間。
- destroyMapedFileIntervalForcibly:在清除過期文件時,如果該文件被其他線程所佔用(引用次數大於0,比如讀取消息),此時會阻止此次刪除任務,同時在第一次試圖刪除該文件時記錄當前時間戳,destroyMapedFileIntervalForcibly表示第一次拒絕刪除之後能保留的最大時間,在此時間內,同樣可以被拒絕刪除,同時會將引用減少1000個,超過該時間間隔後,文件將被強制刪除。
DefaultMessageStore$CleanCommitLogService#deleteExpiredFiles:
boolean timeup = this.isTimeToDelete();
boolean spacefull = this.isSpaceToDelete();
boolean manualDelete = this.manualDeleteFileSeveralTimes > 0;
if (timeup || spacefull || manualDelete) {
//繼續執行刪除邏輯
return;
} else {
// 本次刪除任務無作爲。
}
Step2:RocketMQ在如下三種情況任意滿足之一的情況下將繼續執行刪除文件操作。
- 到了刪除文件的時間點,RocketMQ通過deleteWhen設置一天的固定時間執行一次刪除過期文件操作,默認爲凌晨4點。
- 判斷磁盤空間是否充足,如果不充足,則返回true,表示應該觸發過期文件刪除操作。
- 預留,手工觸發,可以通過調用excuteDeleteFilesManualy方法手工觸發過期文件刪除,目前RocketMQ暫未封裝手工觸發文件刪除的命令。
重點分析一下磁盤不足的判斷依據。
DefaultMessageStore$CleanCommitLogService#isSpaceToDelete
double ratio = DefaultMessageStore.this.getMessageStoreConfig().getDiskMaxUsedSpaceRatio() / 100.0; // @1
cleanImmediately = false;
{
String storePathPhysic = DefaultMessageStore.this.getMessageStoreConfig().getStorePathCommitLog();
double physicRatio = UtilAll.getDiskPartitionSpaceUsedPercent(storePathPhysic); // @2
if (physicRatio > diskSpaceWarningLevelRatio) { // @3
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) {
cleanImmediately = true;
} else {
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) {
DefaultMessageStore.log.info("physic disk maybe full soon, so reclaim space, " + physicRatio);
return true;
}
}
代碼@1:獲取maxUsedSpaceRatio,表示commitlog、consumequeue文件所在磁盤分區的最大使用量,如果超過該值,則需要立即清除過期文件。
代碼@2:通過File#getTotalSpace()獲取commitlog所在磁盤分區總的存儲容量,通過File#getFreeSpace()獲取commitlog目錄所在磁盤文件剩餘容量並得出當前該分區的物理磁盤使用率physicRatio 。
代碼@3:RocketMQ另外提供了兩個與磁盤空間使用率相關的系統級參數:
- -Drocketmq.broker.diskSpaceWarningLevelRatio=0.90:如果磁盤分區使用率超過該闊值,將設置磁盤不可寫,此時會拒絕新消息的寫入。
- -Drocketmq.broker.diskSpaceCleanForciblyRatio=0.85:如果磁盤分區使用超過該闊值,建議立即執行過期文件清除,但不會拒絕新消息的寫入。
判斷磁盤是否可用,用當前已使用物理磁盤率maxUsedSpaceRatio、diskSpaceWarningLevelRatio、diskSpaceCleanForciblyRatio,如果當前磁盤使用率達到上述闊值,將返回true表示磁盤已滿,需要進行過期文件刪除操作。
Step3:然後根據文件的最後一次更新時間與當前時間做比較,判斷是否過期,如果已過期,調用MappedFile的destory。
MappedFile#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();
}
}
}
如果available爲true,表示第一次執行shutdown方法,首先設置available爲false,並記錄firstShutdownTimestamp 時間戳,如果當前該文件被其他線程引用,則本次不強制刪除,如果沒有其他線程在使用該文件,則清除MappedFile相關資源,並最終執行File#delete()方法清除文件。在拒絕被刪除保護期內(destroyMapedFileIntervalForcibly)每執行一次清理任務,將引用次數減去1000,引用數小於1後,該文件最終將被刪除。
關於 ConsumeQueue 的過期文件刪除機制與Commitlog文件機制類似,本文就不重複講解。
本文重點是理解如下參數的含義:fileReservedTime、deletePhysicFilesInterval、destroyMapedFileIntervalForcibly、-Drocketmq.broker.diskSpaceWarningLevelRatio
-Drocketmq.broker.diskSpaceCleanForciblyRatio與獲取磁盤分區總容量與剩餘容量的方法。