etcd-raft-儲存分析

java06051515發表於2020-03-18

etcd raft介紹

etcd raft是目前使用最廣泛的raft庫,如果想深入瞭解raft請直接閱讀論文 “In Search of an Understandable Consensus Algorithm”( raft.github.io/raft.pdf ), etcd raft在etcd, Kubernetes, Docker Swarm, Cloud Foundry Diego, CockroachDB, TiDB, Project Calico, Flannel等分散式系統中都有應用,在生成環境得到了驗證。 傳統raft庫的實現都是單體設計(整合了儲存層、訊息序列化、網路層等), etcd raft繼承了簡約的設計理念,只實現了最核心的raft演算法, 這樣更加的靈活。etcd將網路、日誌儲存、快照等功能分開,透過獨立的模組實現,使用者可以在需要時呼叫。etcd自身實現了自己的一套raft配套庫:etcd-wal(用於儲存日誌),snap(用於儲存快照),MemoryStorage(用於儲存當前日誌、快照、狀態等資訊以供raft核心程式使用)。

etcd-raft-儲存分析

etcd wal介紹

WAL是write ahead log的縮寫,etcd使用wal模組來完成raft日誌的持久化儲存,etcd對wal的所有實現都放在wal目錄中。

wal資料結構

type WAL struct {
lg *zap.Logger

dir string  // the living directory of the underlay files

// dirFile is a fd for the wal directory for syncing on Rename
dirFile *os.File

metadata []byte  // metadata recorded at the head of each WAL
state raftpb.HardState  // hardstate recorded at the head of WAL

start walpb.Snapshot  // snapshot to start reading
decoder *decoder  // decoder to decode records
readClose func() error  // closer for decode reader

mu sync.Mutex
enti uint64  // index of the last entry saved to the wal
encoder *encoder  // encoder to encode records

locks []*fileutil.LockedFile  // the locked files the WAL holds (the name is increasing)
fp *filePipeline
}

上述為wal的資料結構,透過用wal.go檔案中的Create()方法來獲取wal的例項。wal首先會建立一個臨時目錄並初始化相關變數,並建立和初始化第一個wal檔案,等所有的操作都初始化完成後直接更改臨時目錄的名字完成wal例項的初始化。

檔案組織

wal的所有日誌放在一個指定目錄下,日誌的檔名以 .wal 作為結尾,格式為-.wal,seq和index的格式都為%016x,例如:0000000000000001-0000000000000001.wal。index代表這個檔案中第一條raft日誌的index,seq是這個檔案的序列號(依次遞增)。

每個檔案的大小預設為64M,當檔案大於64M時,wal會自動生成新的日誌檔案用於儲存日誌。每個日誌檔案都會使用flock鎖定檔案,引數為LOCK_EX,這是一把獨有鎖,同一時間只能有一個程式可以操作這個日誌檔案,所以當wal佔有這個檔案時,透過程式是無法刪除這個檔案的。

日誌邏輯組織

wal日誌可以儲存多種型別的資料,具體如下。

etcd-raft-儲存分析


etcd-raft-儲存分析


  • crcType 每個新的日誌檔案的第一條記錄都會是crcType型別的記錄,crcType也只會在每個日誌檔案的開始時寫入,用於記錄上一個檔案最後的crc是多少
  • metadataType 每個新的日誌檔案中metadataType緊跟在crcType記錄後面,每個日誌檔案只會出現一次
  • stateType 這種日誌型別會在兩種情況下加入:
  1. 自動切分日誌檔案時,新的日誌檔案中,緊跟在metadataType後面會存入一條stateType的日誌
  2. 當raft核心程式ready中返回hard state時也會儲存該型別的日誌
  • snapshotType wal日誌中只會儲存snapshot的term和index,具體的資料儲存在專門的snapshot中,每次儲存快照都會在wal日誌中儲存一個wal的快照。當儲存快照時,會將快照之前index的日誌檔案都釋放掉。wal中儲存的snapshot主要用於檢查快照是否正確。
  • 日誌讀寫

    wal透過封裝的encoder和decoder模組來實現日誌讀寫。

    寫日誌

    encoder模組把會增量的計算crc和資料一起寫入到wal檔案中。 下面為encoder資料結構

    type encoder struct {
    mu sync.Mutex
    bw *ioutil.PageWriter

    crc hash.Hash32
    buf []byte  //快取空間,預設為1M,降低資料分配的壓力
    uint64buf []byte
    }

    wal透過encoder實現寫日誌,在這個模組中會完成crc的計算。wal為了更好的管理資料,日誌中的每條資料都會以8位元組對齊(wal會自動對齊位元組)。 日誌寫入的流程如下。

    etcd-raft-儲存分析


    圖中的crc是增量計算,以之前的所有日誌資料為增量基礎。 wal只關注寫入日誌,不會校驗日誌的index是否重複,但是如果重啟這個Node的話,系統會自動過濾掉中間混雜的日誌。

    日誌切分

    wal實現了日誌自動切分,當日志資料大於預設的64M時就會生成新的檔案寫入日誌,日誌的切分透過wal.go檔案中的cut方法來實現。cut方法只會在呼叫wal中的Save方法才會觸發呼叫,新檔案的第一條記錄就是上一個wal檔案最後的crc。

    日誌compact

    wal沒有實現日誌的自動compact,系統只提供了MemoryStorage的日誌compact方法(需要使用者主動呼叫)。

    file_pipeline模組

    wal新建新的檔案時都是先新建一個tmp檔案,當所有操作都完成後再重新命名這個檔案。wal使用file_pipeline這個模組在後臺啟動一個協程時刻準備一個臨時檔案以供使用,從而避免臨時建立檔案的開銷。

    etcd snap介紹

    etcd raft自帶了 go.etcd.io/etcd/etcdserver/api/snap模組來實現快照的儲存。

    檔案組織

    在snap模組中一個快照用一個字尾名為.snap的檔案儲存,檔案格式為-.snap, term和index分別代表快照日誌所處的term和index。 每個快照具體儲存結構如下圖:

    etcd-raft-儲存分析


    詳細介紹

    系統可以有多個快照,snap模組使用Snapshotter結構統一管理快照。

    type Snapshotter struct {
    lg *zap.Logger
    dir string
    }

    上面snapshotter的結構程式碼,snapshotter主要用於儲存和讀取快照。

    快照具體儲存的內容需要使用者來指定,例如在raft的官方例子中直接將當時的kv資料Marshal之後儲存到快照中。

    func (s *kvstore) getSnapshot() ([]byte, error) {
    s.mu.RLock()
    defer s.mu.RUnlock()
    return  json.Marshal(s.kvStore)
    }

    何時打快照

    在etcd-raft中使用者可以選擇何時打快照,在etcd的官方案例中打快照的方法是maybeTriggerSnapshot(),這個方法在節點的Ready()方法返回時呼叫,當前提交的index值與上一次大快照的index值大於10000時會打新的快照。

    etcd MemoryStorage介紹

    MemoryStorage用於儲存raft節點臨時的資料,包括entrys、快照等。使用者將資料儲存到memoryStorage中,raft節點也會使用這些資料。包括entrys的傳遞、快照的傳送等都是從memoryStorage中傳送。

    // MemoryStorage implements the Storage interface backed by an
    // in-memory array.
    type MemoryStorage struct {
    // Protects access to all fields. Most methods of MemoryStorage are
    // run on the raft goroutine, but Append() is run on an application
    // goroutine.
    sync.Mutex

    hardState pb.HardState
    snapshot pb.Snapshot
    // ents[i] has raft log position i+snapshot.Metadata.Index
    ents []pb.Entry
    }

    memoryStorage會儲存最新的entrys(包括哪些沒有commit)、快照和狀態,使用者在收到其它節點傳送的相關資料時需要將資料儲存到memorystorage中。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31559758/viewspace-2681225/,如需轉載,請註明出處,否則將追究法律責任。

相關文章