本文僅介紹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中的最后一個文件。
