TiDB Binlog 原始碼閱讀系列文章(六)Pump Storage 介紹(下)
作者:Chunzhu Li
在 上篇文章 中,我們主要介紹了 Pump Storage 是如何對 binlog 進行持久化儲存、排序、配對的。在文中我們提到 binlog 的持久化鍵值儲存主要是由 valueLog
元件完成的。同時,大家如果在上文點開 writeToValueLog
程式碼閱讀的話會發現在其中還會使用一個 slowChaser
元件。slowChaser
元件主要用於避免在寫 kv 環節中 GoLevelDB 寫入太慢甚至出現 write paused 時影響 Pump Storage 的執行效率的問題。
接下來,本篇文章重點介紹 valueLog
與 slowChaser
這兩個元件。
valueLog
valueLog
元件的程式碼位於 pump/storage/vlog.go 中,主要作用是管理磁碟中的所有存放 Binlog Event 的 logFile 檔案。Pump 本地 GoLevelDB 中儲存的 key value 中,key 用 Binlog 的 StartTs/CommitTs
拼成,value 則只是一個索引,指向 valueLog
中的一條 Binlog 記錄。valueLog
的結構體定義如下所示:
type valueLog struct {
buf *bytes.Buffer // buf to write to the current log file
dirPath string
sync bool
maxFid uint32
filesLock sync.RWMutex
filesMap map[uint32]*log File
opt *Options
}
複製程式碼
logFile 檔案在 Pump 指定資料目錄下會以類似 “000001.log” 的命名儲存,其中的 “000001” 即為表示 logFile 檔案編號的 Fid。valueLog
中的 maxFid
為檔案中最大的 Fid,valueLog
也只會把 binlog 寫到 maxFid 的 logFile。 filesMap 中會儲存所有的 Fid 編號所對應的 logFile 物件。logFile 包含了單個 logFile 的一些屬性和方法,主要包含在 pump/storage/log.go 中。
valueLog 作為持久化 Binlog Event 到 logFiles 的元件,包含了一系列對 logFiles 進行的操作。下面我們來看看其中幾個比較重要的方法。
1. readValue
該函式的作用是使用上一篇文章中提到的 valuePointer
在磁碟的 logFiles 中定位到對應的 Binlog Event。該函式會在 Pump 向 Drainer 發 Binlogs 和向 TiKV 查詢 Binlog 的提交狀態時被用到。
2. write
顧名思義,主要作用是處理 寫 binlog 請求,在上一篇文章中提到的 writeToValueLog 被用到,不是併發安全的。為了提高寫入效率,write
函式在處理一組寫 binlog request 時,會先使用 encodeRecord 函式把將要寫入的 binlog event 編碼後存入 bufReqs
陣列,隨後再通過 toDisk
函式寫入 logFile 檔案。如果要寫入的目標 logFile 檔案已經很大,則新建並切換到新的 log 檔案,同時增大 maxFid。
一個完整的 binlog 檔案的編碼格式在 log.go 開頭註釋 中:
/*
log file := records + log file footer
record :=
magic: uint32 // magic number of a record start
length: uint64 // payload 長度
checksum: uint32 // checksum of payload
payload: uint8[length] // binlog 資料
footer :=
maxTS: uint64 // the max ts of all binlog in this log file,so we can check if we can safe delete the file when gc according to ts
fileEndMagic: uint32 // check if the file has a footer
*/
複製程式碼
一個 binlog 檔案中往往包含了多條 record。一條 record 中開頭的 16 個位元組為 record 頭:其中前 4 個位元組為表示 record 資料開始的 magic 碼;中間 8 個位元組儲存了該條 record 的長度;最後 4 個位元組為 checksum,用於校驗。record 頭後面緊跟的是單個 binlog event 的二進位制編碼。這樣編碼的一大好處是 valueLog
只需要 Offset 引數就能得到 binlog 編碼段。
完整的 log 檔案尾部還有一個 footer。valueLog 不會向已經有 footer 的 log 檔案寫入新的 binlog event。footer 的前 8 個位元組為該 logFile 中所有 Binlog 的 maxTS,該值可用於後面介紹到的 GC 操作。後 4 個位元組為表示檔案已結束的 magic 碼。
3. openOrCreateFiles
在 Pump Storage 啟動時會使用該函式啟動 valueLog
元件,初始化 valueLog
的配置資訊,讀取磁碟的 log 檔案並將檔案資訊匯入到 filesMap
中。
在 valueLog
啟動時,如果要寫入的 logFile 沒有 footer,則該函式會使用 scan
方法掃描該 logFile 的所有 binlog,求出 maxTS
更新至記憶體。因此在關閉 valueLog
時,如果當前檔案已經較大,則將檔案加上 footer,將記憶體中的 maxTS
持久化到 footer 以節省下次啟動 valueLog
時進行 scan
查詢的時間。
4. scan
與 scanRequests
掃描某個 valuePointer
之後的所有在 logFiles 中的 binlog event,並將讀到的 binlog event 通過 fn
函式進行對應的處理。Pump Storage 在重啟時會使用該函式讀取持久化到 vlog 但還沒將索引寫到 kv 的 binlog event 並 交給 kv 元件處理。為提高效率,scan 只在讀取檔案列表時加檔案鎖,讀取完畢開始掃描後如果有併發寫入的 logFile 則不會被 scan 掃到。
5. gcTS
在 Storage 進行 GC 時使用,前面 write 中提到的 maxTS
即在這裡使用。該函式會直接刪掉磁碟目錄下所有 maxTS
小於 gcTS
的 logFile 以節約磁碟空間。
slowChaser
slowChaser
元件的程式碼主要位於 pump/storage/chaser.go 中。其結構體定義如下所示:
type slowChaser struct {
on int32
vlog valLogScanner
lastUnreadPtr *valuePointer
recoveryTimeout time.Duration
lastRecoverAttempt time.Time
output chan *request
WriteLock sync.Mutex
}
複製程式碼
看到這裡,相信大家也一定有個疑問:既然 Pump 已經有了正常寫 binlogs 的鏈路,為什麼我們還要再引入 slowChaser
元件呢?
在上篇文章中我們提到,當 Pump Server 收到 binlog 後,會按照 vlog -> kv -> sorter 的順序傳遞 binlog,每一條 binlog 都會在上一步寫入完成後傳送給下一步元件的輸入 channel。在 寫 kv 時,GoLevelDB 可能會因為執行 compaction 導致寫入變慢甚至出現 write paused 現象。此時,當 vlog -> kv channel 裝滿後,則需要 slowChaser
來處理後續的 binlog 到 kv。
slowChaser 的初始化與啟動
slowChaser
會在呼叫 writeValueLog
函式的一開始就被例項化,並同時開啟執行緒執行 slowChaser.Run()
。但此時 slowChaser
並未開始掃描,只是開始監視 Pump 寫 kv 的速度。
開啟 slowChaser
的程式碼位於 writeValueLog
。當我們發現向 buffer channel 中寫入 request 等待的時間超過 1 秒,slowChaser
便會被開啟。同時從該 binlog 開始之後在 writeValueLog
中寫入磁碟的 binlog 均不會再再傳遞進 vlog -> kv 之間的 buffer channel,直到 slowChaser
被關閉為止。
因為 slowChaser
是可能被多次啟停的,因此在 slowChaser
的 Run
函式中我們使用 waitUntilTurnedOn
函式每隔 0.5 秒就檢查 slowChaser
的啟動狀態。
slowChaser 的掃描操作:catchUp
slowChaser
在被啟動後會使用 catchUp
函式去掃描磁碟目錄,從 lastUnreadPtr
即第一個沒有被寫 kv 的 binlog 的 valuePointer
開始。該值會在啟動 slowChaser
時設定為當時的 binlog 對應的 valuePointer
,之後會在每次成功寫入 kv 後就更新。
有了起始 valuePointer
以後,slowChaser
會使用前文提到的 valueLog
的 scanRequests
方法進行一次掃描。掃描時 chaser 會把掃出的每條 binlog 逐一發給 toKV channel。
slowChaser 的執行與關閉
在前面介紹了 slowChaser
的作用,但我們應當注意的是 slowChaser
畢竟是一個 “slow” 的元件,是針對寫 kv 緩慢的無奈之舉,從硬碟中掃描讀取 binlog 再寫 kv 的操作是必然慢於直接從記憶體寫 kv 的。因此 slowChaser
啟動掃描後,我們就應該觀察寫 kv 的速度是否已經恢復正常,以及在磁碟中的 binlog 是否已經全部寫到 kv,從而適時關掉 slowChaser
以提高執行速度。基於此,下面我們將介紹 slowChaser
的 catchUp
與關閉操作,主要涉及 slowChaser.Run()
的 for 迴圈裡的程式碼。
slowChaser
在每輪執行時會進行至多兩次 catchUp
操作:
-
第一次
catchUp
操作不會使用寫鎖禁止valueLog
元件寫 logFile 到磁碟。在正常掃描完磁碟中的 binlog 後,chaser 會同時計算本次catchUp
所花費的時間,如果花費時間較短,說明這可能是個恢復正常運轉的好時機。這時slowChaser
會進入第二次catchUp
操作,嘗試掃完所有 binlog 並關閉slowChaser
。如果本次catchUp
花費時間過長或者在 1 分鐘內進行過第二次的catchUp
操作則會跳過第二次catchUp
直接進入下一輪。 -
第二次
catchUp
會在操作開始前記錄本次恢復開始的時間,同時上鎖阻止 vlog 寫 binlog 到磁碟。如果catchUp
在 1 秒內完成,此時磁碟中所有 binlog 都已經寫到 kv , 則slowChaser
可以安全地被關閉。如果catchUp
超時,為避免長時間持鎖阻止 vlog 寫 binlog 影響效能,slowChaser
將繼續進行下一輪的catchUp
。第二次 catchUp 操作結束時不論成敗互斥鎖都將被釋放。
slowChaser
在成功 catch up 之後會被關閉,但不會完全停止執行,只是進入了 “睡眠” 狀態,繼續不斷監視 Pump 寫 kv 的速度。一旦 writeValueLog
中再次出現了寫 kv 慢的現象,slowChaser.TurnOn
被呼叫,slowChaser
又會重新啟動,開始新的輪次的 catchUp
操作。只有當 writeValueLog
函式退出時,slowChaser
才會真正隨之退出並完全停止執行。
小結
本文介紹了 Pump Storage 的兩個重要元件 valueLog
,slowChaser
的主要功能與具體實現,希望能幫助大家更好地理解 Pump 部分的原始碼。
至此 TiDB Binlog 原始碼的 Pump 部分的程式碼已基本介紹完畢,在下一篇文章中我們將開始介紹 Drainer Server 模組,幫助大家理解 Drainer 是如何啟動,維護狀態與獲取全域性 binlog 資料與 Schema 資訊的。