分佈式系統模式 - Write-Ahead Log

在分佈式系統中,節點之間複製數據對於確保用戶服務連續性至關重要。正如 CAP 定理所總結的,根據在故障情況下數據一致性是否關鍵,或者是否優先考慮可用性,需要做出設計選擇。考慮 CP 的情況,有一種稱爲狀態機複製的技術可以實現容錯性,同時也能保證強一致性。在狀態機複製中,諸如鍵值存儲之類的存儲服務在多臺服務器上進行復制,並且用戶輸入按照相同的順序在每臺服務器上執行。其中的關鍵實現技術是在多臺服務器上覆制預寫式日誌(Write-Ahead Log),以形成複製日誌(Replicated Log)。

即使存儲數據的服務器出現故障,我們也需要提供強大的持久性保證。一旦服務器同意了執行某個操作,那麼即便其發生故障並重啓從而丟失所有內存中的狀態,也應該能夠完成該操作,方式之一就是依賴 WAL(Write-Ahead Log)

在一些地方也被這種技術也被稱爲 Journaling

將每次狀態變更作爲命令存儲在硬盤上的文件中, 如上圖所示, anyway, 你把每一條狀態變更叫做命令、實體、記錄、條目、日誌都可以,注意這裏的日誌不是那個日誌的意思哈,意思大家都懂。

每個服務器進程都維護一個單獨的日誌,並按順序追加記錄。這種單一的日誌通過順序追加的方式,簡化了在系統重啓時及新命令被追加時對日誌的處理。每個日誌條目都會被賦予一個唯一的標識符 (比如某種順序的 id、條目離文件開頭的偏移量),這個標識符有助於實施對日誌的某些其他操作,比如查找、分段日誌、使用低水位標記清理日誌等。

日誌更新可以通過單一更新隊列的方式 (後續再講) 來實現。

確保記錄到日誌文件中的內容確實能被實際保存到磁盤上至關重要。各類編程語言所提供的文件處理庫均提供某種方法,或者系統系統調用,可迫使操作系統立即將文件修改內容同步到底層物理存儲設備上。但是,使用這種刷新 (Flush) 操作時,需要注意存在一個需要權衡的因素。

每收到一條狀態變更都生成一條日誌條目寫入到磁盤,可以保證所有的狀態變更不丟失,這聽起來不錯,但是魚與熊掌不可兼得,在狀態變更很頻繁的情況下,每次刷新數據到磁盤會是一個瓶頸,因爲它是一個耗時的操作,所以經常我們權衡之後做一個取捨,將批量日誌條目一次刷新到磁盤,相信你能理解這個苦衷。

既然涉及到存儲,那麼就有可能持久化的數據被損壞的情況,已經每年我都會遇到一兩次的比特跳變的情況,每一個比特位從 0 被改到 1,或者從 1 改到 0,這個時候我們可能需要加校驗位,使用 CRC 等技術對數據進行校驗,甚至糾正。

如果總往一個日誌文件中追加日誌,一段時間後日志文件會變得很大,這個時候就需要分段日誌和低水位標記等技術了。

因爲網絡失敗和重試等原因,日誌中可能包含重複的條目。我們需要冪等操作去處理。

通過複製日誌的方式,將日誌寫入到分佈式節點,可以避免某個節點 crash 無法恢復。

etcd 是一個很好的學習分佈式技術的開源項目。在 etcd 的實現中,關鍵部分分佈式鍵值存儲系統的持久化就使用了預寫式日誌(Write-Ahead Log,簡稱 WAL)。etcd 使用 WAL 來保證即使在節點突然崩潰或者意外終止的情況下,也能安全可靠地恢復數據狀態。當 etcd 接收到對鍵值對的修改請求時,首先將這些改動操作記錄到 WAL 中,然後再應用到內存中的數據庫狀態。這樣,即便在數據還未完全同步到磁盤數據庫之前發生故障,重啓後也能通過回放 WAL 中的日誌記錄來恢復服務的狀態一致性。同時,etcd 還利用 WAL 實現了數據的複製與集羣間的同步,進一步增強了系統的容錯性和可靠性。

感興趣的同學可以查看 etcd 的代碼實現(https://rpcx.io/r/4c98):

// WAL 是穩定存儲的邏輯表現形式。
// WAL 處於讀取模式或追加模式,但不能同時處於兩種模式。
// 新創建的 WAL 處於追加模式,準備好接收記錄的追加。
// 剛剛打開的 WAL 處於讀取模式,準備好讀取已存在的記錄。
// 在讀取完所有先前的記錄之後,WAL 將準備再次追加記錄。
type WAL struct {
    lg *zap.Logger // zap 日誌器,用於內部日誌記錄

    dir string // 存儲底層文件的實際目錄路徑

    // dirFile 是指向 WAL 目錄的一個文件描述符,用於在重命名時同步目錄
    dirFile *os.File

    metadata []byte           // 每個 WAL 文件頭部記錄的元數據信息
    state    raftpb.HardState // 在 WAL 文件頭部記錄的硬狀態信息

    start     walpb.Snapshot // 用於開始讀取 WAL 的快照信息
    decoder   Decoder        // 用於解碼 WAL 記錄的解碼器
    readClose func() error   // 關閉解碼器讀取器時調用的關閉函數

    unsafeNoSync bool // 如果設置爲 true,則不進行 fsync 操作(即不確保數據立即同步至磁盤)

    mu      sync.Mutex // 互斥鎖,用於保護併發訪問
    enti    uint64     // 已保存至 WAL 的最後一條記錄的索引
    encoder *encoder   // 用於編碼記錄的編碼器

    locks []*fileutil.LockedFile // WAL 持有的已鎖定文件列表(文件名按遞增順序排列)
    fp    *filePipeline         // 文件管道,用於管理多個 WAL 文件
}

30 個分佈式系統模式,我會在後面的文章中徐徐道來。

這些模式由 Unmesh Joshi 整理,並且由大師 Martin Fowler 監製:https://rpcx.io/r/4c97

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/3qTPyg17i7n1IiJuIYBOrA