1. 程式人生 > 程式設計 >TiDB Binlog 原始碼閱讀系列文章(六)Pump Storage 介紹(下)

TiDB Binlog 原始碼閱讀系列文章(六)Pump Storage 介紹(下)

作者:Chunzhu Li

上篇文章 中,我們主要介紹了 Pump Storage 是如何對 binlog 進行持久化儲存、排序、配對的。在文中我們提到 binlog 的持久化鍵值儲存主要是由 valueLog 元件完成的。同時,大家如果在上文點開 writeToValueLog 程式碼閱讀的話會發現在其中還會使用一個 slowChaser 元件。slowChaser 元件主要用於避免在寫 kv 環節中 GoLevelDB 寫入太慢甚至出現 write paused 時影響 Pump Storage 的執行效率的問題。

接下來,本篇文章重點介紹 valueLogslowChaser 這兩個元件。

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. scanscanRequests

掃描某個 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 元件呢?

image

在上篇文章中我們提到,當 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 是可能被多次啟停的,因此在 slowChaserRun 函式中我們使用 waitUntilTurnedOn 函式每隔 0.5 秒就檢查 slowChaser 的啟動狀態。

slowChaser 的掃描操作:catchUp

slowChaser 在被啟動後會使用 catchUp 函式去掃描磁碟目錄,從 lastUnreadPtr 即第一個沒有被寫 kv 的 binlog 的 valuePointer 開始。該值會在啟動 slowChaser 時設定為當時的 binlog 對應的 valuePointer,之後會在每次成功寫入 kv 後就更新。

有了起始 valuePointer 以後,slowChaser 會使用前文提到的 valueLogscanRequests 方法進行一次掃描。掃描時 chaser 會把掃出的每條 binlog 逐一發給 toKV channel。

slowChaser 的執行與關閉

在前面介紹了 slowChaser 的作用,但我們應當注意的是 slowChaser 畢竟是一個 “slow” 的元件,是針對寫 kv 緩慢的無奈之舉,從硬碟中掃描讀取 binlog 再寫 kv 的操作是必然慢於直接從記憶體寫 kv 的。因此 slowChaser 啟動掃描後,我們就應該觀察寫 kv 的速度是否已經恢復正常,以及在磁碟中的 binlog 是否已經全部寫到 kv,從而適時關掉 slowChaser 以提高執行速度。基於此,下面我們將介紹 slowChasercatchUp 與關閉操作,主要涉及 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 的兩個重要元件 valueLogslowChaser 的主要功能與具體實現,希望能幫助大家更好地理解 Pump 部分的原始碼。

至此 TiDB Binlog 原始碼的 Pump 部分的程式碼已基本介紹完畢,在下一篇文章中我們將開始介紹 Drainer Server 模組,幫助大家理解 Drainer 是如何啟動,維護狀態與獲取全域性 binlog 資料與 Schema 資訊的。

原文閱讀pingcap.com/blog-cn/tid…