etcd raft 處理流程圖系列2-wal的讀寫

charlieroro發表於2021-08-10

本文僅介紹wal的基本處理,如create、open、close、read等操作,從wal目錄中載入snapshot,wal檔案的建立,以及讀取wal目錄中的所有資料(主要是entryTypestateTypemetadataType這幾類)和接收到node.Ready()之後的寫操作。

WAL的處理還是比較複雜的可以借鑑的地方也很多。WAL在編碼以及flush時使用快取來提升效率。flush的單位為分頁,每頁又分為8個section,section的作用是用來檢測寫入的資料是否被破壞,檢測邏輯為:如果某個section中的所有位元組都為0,則說明資料遭到破壞,反之則認為資料正常。在isTornEntry中,主要通過section機制來檢測WAL檔案中最後一個record是否因為資料破壞而導致json解析或crc校驗失敗。

wal很多地方用到了crc校驗,基本邏輯是在encoder寫入時會計算crc,在使用新檔案(如createcut)時儲存會crc。建立檔案時寫入的crc為0,切分檔案(新檔案由WAL.fp提供)時寫入的crc為前一個檔案的crc,一個檔案僅會在開頭儲存一個crc。在讀取WAL檔案時,decoder會在讀取到非crcTyperecorder時更新其crc,當讀到crcTyperecorder時會使用它計算出的crc與recorder中的crc進行比較,判斷是否存在資料篡改。每個recorder中都會儲存crc,crcType只是提供了一個執行crc校驗的機會(即只有遇到crcType型別才會進行crc校驗)。

在看程式碼時也給官方提了一些issue:132731328713286

建立

下面是wal的create流程,在建立檔案事先預分配檔案大小(64MB),用於提升效能。wal通過encode()函式將編碼後的資料寫入檔案,因此需要在對檔案執行寫操作時加鎖,寫入的資料以record為單位(record首先被寫入快取,當資料以頁為單位對齊時通過flush寫入檔案)。先計算資料的crc校驗碼,然後計算record的幀資料。寫資料時,先寫入幀資料,再寫入record。在寫入資料(無論是幀資料還是record)時,會以頁為單位將資料寫入檔案,不足一頁的資料會暫存在快取中。幀資料儲存了實際的資料大小和pad的資料大小,在讀取wal檔案時會用到該資訊。

wal的檔名由兩部分構成:seq和index,前者應該順序遞增的,以保證日誌檔案的連續性(isValidSeq會根據seq校驗日誌檔案的連續性)。

etcd raft 處理流程圖系列2-wal的讀寫

載入snapshot

下面是在wal目錄中載入snapshot的操作,該操作中用到了上面的幀資料。wal使用decode()函式進行解碼,首先取出在幀資料中解析出record的大小和padBytes的小,然後根據record的大小解碼資料,最後根據record的型別採集並返回所有snapshot。

etcd raft 處理流程圖系列2-wal的讀寫

從上面可以看到,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.
etcd raft 處理流程圖系列2-wal的讀寫

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,然後寫入metadatastate資訊,並重新計算crc,在讀取時可以校驗到此為止的crc。新檔案作為WAL.locks中的最後一個檔案。

etcd raft 處理流程圖系列2-wal的讀寫

原圖連結

相關文章