TiDB 資料一致性校驗實現:Sync-diff-inspector 優化方案

PingCAP發表於2021-12-01

簡介

在資料同步的場景下,上下游資料的一致性校驗是非常重要的一個環節,缺少資料校驗,可能會對商業決策產生非常負面的影響。。Sync-diff-inspector 是 Data Platform 團隊開發的一款一致性校驗工具,它能對多種資料同步場景的上下游資料進行一致性校驗,如多資料來源到單一目的(mysql 中分庫分表到 TiDB 中)、單一源到單一目的( TiDB 表 到 TiDB 表)等,在資料校驗過程中,其效率和正確性是至關重要的。首先我們看下 Sync-diff-inspector 的架構圖,對 Sync-diff-inspector 的作用和實現原理有一個大致的認知。


Sync-diff-inspector 2.0 架構圖

Why Sync-diff-inspector 2.0?

在 1.0 版本中,我們遇到客戶反饋的一些問題,包括:

  • 針對大表進行一致性校驗時出現 TiDB 端發生記憶體溢位。
  • 不支援 Float 型別資料校驗的問題。
  • 結果輸出對使用者不友好,需要對校驗結果進行精簡。
  • 檢驗過程中發生 GC,導致校驗失敗。

造成以上問題的原因與原版的實現方式有關

  • 採用單執行緒劃分 Chunk,該表中所有已被劃分的 Chunk 需要等待該表中所有 Chunk 全部被劃分才會開始進行比對,這會導致這段時間內,TiKV 的使用率降低
  • Checkpoint 功能將校驗過的每個 Chunk 的狀態寫入資料庫,所以寫入資料庫的 IO 成為校驗過程的瓶頸。
  • 當 chunk 範圍內的 checksum 不同時,直接進行按行比對,消耗大量 IO 資源。
  • 缺少自適應 GC 的功能,導致正在校驗的 Snapshot 被 GC,使得校驗失敗。
  • ...

Sync-diff-inspector 2.0 新特性

Chunk 劃分

對於比較兩個表資料是否相同,可以通過分別計算兩個表的 checksum 來判斷,但是確定哪一行出現了不同則需要逐行比對。為了縮小 checksum 不一致時需要進行逐行比對的行數, Sync-diff-inspector 採用了折衷的方案:將表按照索引的順序劃分成若干塊(chunk),再對每個 chunk 進行上下游資料比對。

chunk 的劃分沿用了之前的方法。TiDB 統計資訊會以索引作為範圍將表劃分為若干個桶,再對這些桶根據 chunk 的大小進行合併或切分。切分過程則選擇隨機行作為範圍。

原版 Sync-diff-inspector 採用單執行緒劃分 chunk,已被劃分的 chunk 需要等待該表劃分完所有 chunk 才會開始比對,這裡採用非同步劃分 chunk 的方法來提高這段時間的資源利用率。這裡有兩種降低資源利用率的情況:

  1. chunk 劃分過程中可能由於 chunk 的預定大小小於一個桶的大小,需要切分這個桶為若干個 chunk,這是個相對比較慢的過程,因此消費端也就是 chunk 的比對執行緒會出現等待的情況,資源利用率會降低。這裡採用兩種處理方法:採用多個桶非同步劃分來提高資源利用率;有些表沒有桶的資訊,因此只能把整個表當作一個桶來切分,採用多表劃分來提高總體的非同步劃分桶數。
  2. chunk 的劃分也會佔用一定的資源,chunk 劃分過快會一定程度減慢 chunk 比對的速度,因此這裡在消費端通過 channel 來限制多表劃分chunk的速度。

總結來說,優化後的 Sync-diff-inspector 對 chunk 的劃分由三部分組成。如下圖所示,這裡指定存在 3 個 chunk_iter,每個 chunk_iter 劃分一個表,這裡通過全域性的 channel 調整 chunks_iter 劃分的進度。注意這裡只按表限流,每個 chunk_iter 開始劃分時,會非同步劃分所有 chunk,當全域性的 channel 的 buffer 滿了,chunk_iter 會阻塞。當 chunk_iter 的所有 chunk 都進入全域性 channel 的 buffer 後,該 chunk_iter 會開始劃分下一個表。

Checkpoint 和修復 SQL

Sync-diff-inspector 支援在斷點處繼續進行校驗的功能。Diff 程式每十秒鐘會記錄一次斷點資訊,當校驗程式在某個時刻發生異常退出的時候,再次執行 Sync-diff-inspector 會從最近儲存的斷點處繼續進行校驗。如果在下一次執行時,Sync-diff-inspector 的配置檔案發生改變,那麼 Sync-diff-inspector 會拋棄斷點資訊,重新進行校驗。

該功能的完整性和正確性依賴於在 Chunk 劃分過程中定義的全域性有序性和連續性。相比於原版,Sync-diff-inspector 2.0 實現的 checkpoint 不需要記錄每個 chunk 的狀態,只需要記錄連續的、最近校驗完成的 chunk 的狀態,大大減少了需要記錄的資料量。chunk 的全域性有序特性由一個結構體組成,結構體包含了該 chunk 屬於第幾個表,屬於該表的第幾個桶到第幾個桶(如果該 chunk 由兩個或多個桶合併而成,則記錄桶的首末),這個桶被切分成多少個 chunk,這個 chunk 是切分後的 chunk 的第幾個。同時這種特性也可以判斷兩個 chunk 是不是連續的。每次斷點時鐘觸發時,會選擇已完成比對的連續的 chunk 的最後一個 chunk 作為檢查點,寫入該 chunk 的資訊到本地檔案。

當校驗出不同行時,Sync-diff-inspector 會生成修復 SQL 並儲存在本地檔案中。因為檢驗的 chunk 是亂序且並行的,所以這裡為每個 chunk 建立(若該 chunk 存在不同行)一個檔案來儲存修復 SQL,檔名是該 chunk 的全域性有序的結構體。修復 SQL 和 checkpoint 的記錄肯定存在先後順序:

  1. 如果先寫入修復 SQL 的記錄,那麼此時程式異常退出,這個被寫入修復 SQL 但沒被 checkpoint 記錄的 chunk 會在下一次生成,一般情況下,這個修復 SQL 檔案會被重新覆蓋。但是由於桶的切分是隨機分的,因此儘管切分後的 chunk 個數固定,上一次檢查出的不同行在切分後 chunks 的第三個,這次可能跑到了第四個chunk 的範圍內。這樣就會存在重複的修復 SQL。
  2. 如果先寫入 checkpoint,那麼此時程式異常退出,下一次執行會從該 checkpoint 記錄的 chunk 的後面範圍開始檢驗,如果該 chunk 存在修復 SQL 但還沒有被記錄,那麼這個修復 SQL 資訊就丟失了。

這裡採用了先寫入修復 SQL 記錄,下一次執行時會將排在 checkpoint 記錄的 chunk 後的所有修復 SQL 檔案(檔案是以該 chunk 的全域性有序結構體命名,因此可以很容易判斷兩個 chunk 的先後順序)都移到 trash 資料夾中,以此避免出現重複的修復 SQL

二分校驗和自適應 chunkSize

大表做 checksum 和切分成 chunks 做 checksum 的效能損耗在於每次做 checksum 都會有一些額外消耗(包括一次會話建立傳輸的時間),如果把 chunk 劃分的很小,那麼這些額外消耗在一次 checksum 花費的時間佔比會變大。通常需要把 chunk 的預定大小 chunkSize 設定大一些,但是 chunkSize 設定的過大,當上下游資料庫對 chunk 做 checksum 的結果不同時,如果對這個大 chunk 直接進行按行對比,那麼開銷也會變得很大。

在資料同步過程中,一般只會出現少量的資料不一致,基於這個假定,當校驗過程中,發現某個 chunk 的上下游的 checksum 不一致,可以通過二分法將原來的 chunk 劃分成大小接近的兩個子 chunk,對子 chunk 進行 checksum 對比,進一步縮小不一致行的可能範圍。這個優化的好處在於,checksum 對比所消耗的時間和記憶體資源遠小於逐行進行資料比對的消耗,通過 checksum 對比不斷的縮小不一致行的可能範圍,可以減少需要進行逐行對比的資料行,加快對比速度,減少記憶體損耗。並且由於每次計算 checksum 都相當於遍歷一次二分後的子 chunk,理論上不考慮多次額外消耗,二分檢驗的開銷相當於只對原 chunk 多做兩次 checksum。

由於做一次 checksum 相當於遍歷範圍內的所有行,可以在這個過程中順便計算這段範圍的行數。這樣做是因為 checksum 的原理是對一行的資料進行 crc32 運算,再對每一行的結果計算異或和,這種 checksum 的無法校驗出三行重複的錯誤,在索引列不是 unique 屬性的情況下是存在這種錯誤的。同時計算出每個 chunk 的行數,可以使用 limit 語法定位到該 chunk 的中間一行資料的索引,這是二分方法使用的前提。

但是 chunkSize 也不能設定的過大,當一次二分後兩邊的子 chunk 都存在不同行,那麼會停止二分,進行行比對。過大的 chunk 就更有可能同時包含多個不同行,二分校驗的作用也會減小。這裡設定每張表預設的 chunkSize 為 50000 行,每張表最多劃分出 10000 個 chunk。

索引處理

上下游資料庫的表可能會出現 schema 不同,例如下游表只擁有一部分上游的索引。不恰當的索引的選擇會造成一方資料庫耗時加大。在做表結構校驗時,只保留上下游都有的索引(若不存在這種索引,則保留所有索引)。另一方面,某些索引包含的列並不是 unique 屬性的,可能會有大量的行擁有相同的索引值,這樣 chunk 會劃分的不均勻。Sync-diff-inspector 在選擇索引時,會優先選擇 primary key 或者 unique 的索引,其次是選擇重複率最低的索引

where 處理

假設存在一張表 create table t (a int, b int, c int, primary key (a, b, c));

並且一個劃分後的 chunk 範圍是 ((1, 2, 3), (1, 2, 4)]

原版 Sync-diff-inspector 會生成 where 語句:

  • ((a > 1) OR (a = 1 AND b > 2) OR (a = 1 AND b = 2 AND c > 3))
  • ((a < 1) OR (a = 1 AND b < 2) OR (a = 1 AND b = 2 AND c <= 4))

可以優化為 (a = 1) AND (b = 2) AND ((c > 3) AND (c <= 4))

自適應 GC

在原版 Sync-diff-inspector 中,校驗過程中可能會出現大量表被 GC 導致校驗失敗。Sync-diff-inspector 工具支援自適應 GC 的功能,在 Diff 程式初始化階段啟動一個後臺 goroutine,在檢驗過程中不斷的更新 GC safepoint TTL 引數,使得對應的 snapshot 不會被 GC,保證校驗過程的順利進行。

處理 Float 列

根據 float 型別的特性,有效精度只有6位,因此在 checksum SQL 中對 float 型別的列使用 round(%s, 5-floor(log10(abs(column)))) 取 6 位有效數字作為 checksum string 的一部分,當 column 取特殊值為 0 時,該結果為 NULL,但是 ISNULL(NULL) 也作為 checksum string 的一部分,此時不為 true,這樣可以把 0 和 NULL 區分開來。

使用者互動優化

Sync-diff-inspector 顯示如下資訊:

  • 將日誌寫入到日誌檔案中。
  • 在前臺顯示進度條,並提示正在比較的表。
  • 記錄每個表校驗相關結果,包括整體對比時間、對比資料量、平均速度、每張表對比結果和每張表的配置資訊。
  • 生成的修復 SQL 資訊。
  • 一定時間間隔記錄的 checkpoint 資訊。

其效果如下圖:

具體細節可參考 overview

效能提升

基於以上的優化手段,我們進行了效能測試,在 Sysbench中, 構造 668.4GB 資料,共 190 張表,每張表一千萬行資料,測試結果如下:

從測試結果可以看出,Sync-diff-inspector 2.0 相比於原版, 校驗速度有明顯提升,同時在TiDB 端記憶體佔用顯著減少

未來展望

開放性的架構

在 Sync-diff-inspector 中我們定義了 Source 抽象,目前只支援 TiDB 端到 TiDB 端,MySQL 端到 MySQL 端以及 MySQL 端到 TiDB 端的資料一致性校驗,但是在未來,通過實現 Source 對應的方法,可以適配多種其他資料庫進行資料一致性校驗,例如 Oracle, Aurora 等。

支援更多型別

由於部分列型別特殊,目前 sync-diff-inspector 暫不支援(例如 json,bit,binary,blob )。需要在 checksum SQL 語句中對它們特殊處理,例如對於 json 型別的列,需要通過 json_extract 提取出現在 json 中的每一個 key 的值。

更激進的二分 checksum

新版 sync-diff-inspector 採用二分 checksum 方法來減小逐行比對的資料量,但是在發現二分後的兩個 chunk 都存在不一致資料時就停止繼續二分,進行逐行比對。這種方法比較悲觀,認為此刻 chunk 可能存在多個不一致的地方。但是根據實際情況,sync-diff-inspector 的應用場景一般是隻存在少量不一致的情況,更加激進的做法是,繼續二分,最後得到的是一組擁有最小行數(預設 3000 行)的且存在不一致資料的 chunk 陣列,再對這些陣列分別進行逐行比對。

相關文章