分割日誌(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: