本文僅介紹wal的基本處理,如create、open、close、read等操作,從wal目錄中載入snapshot,wal檔案的建立,以及讀取wal目錄中的所有資料(主要是entryType
、stateType
、metadataType
這幾類)和接收到node.Ready()
之後的寫操作。
WAL的處理還是比較複雜的可以借鑑的地方也很多。WAL在編碼以及flush時使用快取來提升效率。flush的單位為分頁,每頁又分為8個section,section的作用是用來檢測寫入的資料是否被破壞,檢測邏輯為:如果某個section中的所有位元組都為0,則說明資料遭到破壞,反之則認為資料正常。在isTornEntry
中,主要通過section機制來檢測WAL檔案中最後一個record是否因為資料破壞而導致json解析或crc校驗失敗。
wal很多地方用到了crc校驗,基本邏輯是在encoder寫入時會計算crc,在使用新檔案(如create
或cut
)時儲存會crc。建立檔案時寫入的crc為0,切分檔案(新檔案由WAL.fp
提供)時寫入的crc為前一個檔案的crc,一個檔案僅會在開頭儲存一個crc。在讀取WAL檔案時,decoder會在讀取到非crcType
的recorder
時更新其crc,當讀到crcType
的recorder
時會使用它計算出的crc與recorder
中的crc進行比較,判斷是否存在資料篡改。每個recorder中都會儲存crc,crcType
只是提供了一個執行crc校驗的機會(即只有遇到crcType型別才會進行crc校驗)。
在看程式碼時也給官方提了一些issue:13273、13287、13286
建立
下面是wal的create流程,在建立檔案事先預分配檔案大小(64MB),用於提升效能。wal通過encode()
函式將編碼後的資料寫入檔案,因此需要在對檔案執行寫操作時加鎖,寫入的資料以record為單位(record首先被寫入快取,當資料以頁為單位對齊時通過flush寫入檔案)。先計算資料的crc校驗碼,然後計算record的幀資料。寫資料時,先寫入幀資料,再寫入record。在寫入資料(無論是幀資料還是record)時,會以頁為單位將資料寫入檔案,不足一頁的資料會暫存在快取中。幀資料儲存了實際的資料大小和pad的資料大小,在讀取wal檔案時會用到該資訊。
wal的檔名由兩部分構成:seq和index,前者應該順序遞增的,以保證日誌檔案的連續性(isValidSeq
會根據seq校驗日誌檔案的連續性)。
載入snapshot
下面是在wal目錄中載入snapshot的操作,該操作中用到了上面的幀資料。wal使用decode()
函式進行解碼,首先取出在幀資料中解析出record的大小和padBytes的小,然後根據record的大小解碼資料,最後根據record的型別採集並返回所有snapshot。
從上面可以看到,wal的encoder用於寫檔案,因此encoder會關聯到當前正在編輯的檔案,記錄了檔案控制程式碼、當前位元組偏移以及快取等資訊,一般會選擇WAL.locks
中的最後一個元素。而decoder用於讀取所有檔案,因此關聯到多個wal檔案,記錄了這些檔案控制程式碼。
讀取所有資料
下圖是從wal目錄中嘗試讀取所有資訊(如metadata、entries、state)的過程。涉及讀取wal目錄中的檔案資訊,以此構建WAL
結構,然後通過生成的decoder來將檔案解碼為不同型別的資料進行處理。最終返回解碼後的資料。需要注意decoder的檔案是有序的,可以從原始碼fileutil.ReadDir
看出來,其對檔名進行了sort.Strings(names)
操作。
此外,在讀取檔案時,根據檔案的讀寫模式分別進行了處理。讀模式下只需讀完所有檔案,關閉檔案並返回結果即可。寫模式下檔案是加鎖的,在decodeRecord
中會讀取lastValidOff
(frameSizeBytes + recBytes + padBytes)長度的資料,並將該長度之後的資料歸0,防止檔案中出現被破壞的資料,由於對檔案的修改會改變檔案的crc校驗,但好在新的record不會立即重新整理到檔案中(原始碼中的描述如下),更新檔案的encoder,後續通過encoder將資料最終寫入檔案即可。
// decodeRecord() will return io.EOF if it detects a zero record,
// but this zero record may be followed by non-zero records from
// a torn write. Overwriting some of these non-zero records, but
// not all, will cause CRC errors on WAL open. Since the records
// were never fully synced to disk in the first place, it's safe
// to zero them out to avoid any CRC errors from new writes.
WAL的儲存
raftexample的serveChannels
中當接收到node.Ready()
傳來的資料時,會對這些資料進行持久化。如下圖,首先會儲存狀態和entry資訊,如果locks中最後一個檔案(該檔案)的內容大於或等於SegmentSizeBytes
時需要切割檔案。
在切分檔案時,將已有的資料同步到檔案中,後面的操作就是新建一個檔案。新檔案來自於WAL.fp
是在建立檔案時建立的,fp提供檔案的程式碼邏輯如下,可以看到它通過迴圈建立檔案的方式來為WAL源源不斷地提供日誌檔案。
for {
f, err := fp.alloc()
if err != nil {
fp.errc <- err
return
}
select {
case fp.filec <- f:
case <-fp.donec:
os.Remove(f.Name())
f.Close()
return
}
}
首先在新檔案中記錄當前的crc,然後寫入metadata
和state
資訊,並重新計算crc,在讀取時可以校驗到此為止的crc。新檔案作為WAL.locks中的最後一個檔案。