請問你知道分散式系統設計模式的分割日誌思想麼?

乾貨滿滿張雜湊發表於2021-02-11

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

在檔名包含這種資訊之後,讀操作就分為兩步:

  1. 給定一個偏移(或者 transaction id),獲取到大於這個偏移日誌所在檔案
  2. 從檔案中讀取所有大於這個偏移的日誌
//給定偏移量,讀取所有日誌
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:
image

Kafka:
image

Pulsar儲存實現BookKeeper:
image

另外,基於一致性協議 Paxos 或者 Raft 的儲存,一般會採用分段日誌,例如 Zookeeper 以及 TiDB。

每日一刷,輕鬆提升技術,斬獲各種offer:

image

相關文章