分割日誌(Segmented Log)
將大檔案切分為更容易處理的多個更小的檔案。
問題背景
單一的日誌檔案可能會增長到很大,並且在程式啟動時讀取從而成為效能瓶頸。老的日誌需要定時清理,但是對於一個大檔案進行清理操作很費勁。
解決方案
將單一日誌切分為多個,日誌在達到一定大小時,會切換到新檔案繼續寫。
//寫入日誌
public Long writeEntry(WALEntry entry) {
//判斷是否需要另起新檔案
maybeRoll();
//寫入檔案
return openSegment.writeEntry(entry);
}
private void maybeRoll() {
//如果當前檔案大小超過最大日誌檔案大小
if (openSegment.
size() >= config.getMaxLogSize()) {
//強制刷盤
openSegment.flush();
//存入儲存好的排序好的老日誌檔案列表
sortedSavedSegments.add(openSegment);
//獲取檔案最後一個日誌id
long lastId = openSegment.getLastLogEntryId();
//根據日誌id,另起一個新檔案,開啟
openSegment = WALSegment.open(lastId, config.getWalDir());
}
}
如果日誌做了切分,那麼需要快速以某個日誌位置(或者日誌序列號)定位到某個檔案的機制。可以通過兩種方式實現:
- 每一個日誌切分檔案的名稱都是包含特定開頭以及日誌位置偏移量(或者日誌序列號)
- 每一個日誌序列號包含檔名稱以及 transaction 偏移。
//建立檔名稱
public static String createFileName(Long startIndex) {
//特定日誌字首_起始位置_日誌字尾
return logPrefix + "_" + startIndex + "_" + logSuffix;
}
//從檔名稱中提取日誌偏移量
public static Long getBaseOffsetFromFileName(String fileName) {
String[] nameAndSuffix = fileName.split(logSuffix);
String[] prefixAndOffset = nameAndSuffix[0].split("_");
if (prefixAndOffset[0].equals(logPrefix))
return Long.parseLong(prefixAndOffset[1]);
return -1l;
}
在檔名包含這種資訊之後,讀操作就分為兩步:
- 給定一個偏移(或者 transaction id),獲取到大於這個偏移日誌所在檔案
- 從檔案中讀取所有大於這個偏移的日誌
//給定偏移量,讀取所有日誌
public List<WALEntry> readFrom(Long startIndex) {
List<WALSegment> segments = getAllSegmentsContainingLogGreaterThan(startIndex);
return readWalEntriesFrom(startIndex, segments);
}
//給定偏移量,獲取所有包含大於這個偏移量的日誌檔案
private List<WALSegment> getAllSegmentsContainingLogGreaterThan(Long startIndex) {
List<WALSegment> segments = new ArrayList<>();
//Start from the last segment to the first segment with starting offset less than startIndex
//This will get all the segments which have log entries more than the startIndex
for (int i = sortedSavedSegments.size() - 1; i >= 0; i--) {
WALSegment walSegment = sortedSavedSegments.get(i);
segments.add(walSegment);
if (walSegment.getBaseOffset() <= startIndex) {
break; // break for the first segment with baseoffset less than startIndex
}
}
if (openSegment.getBaseOffset() <= startIndex) {
segments.add(openSegment);
}
return segments;
}
舉例
基本所有主流 MQ 的儲存,例如 RocketMQ,Kafka 還有 Pulsar 的底層儲存 BookKeeper,都運用了分段日誌。
RocketMQ:
Kafka:
Pulsar儲存實現BookKeeper:
另外,基於一致性協議 Paxos 或者 Raft 的儲存,一般會採用分段日誌,例如 Zookeeper 以及 TiDB。
每日一刷,輕鬆提升技術,斬獲各種offer: