作者:Kenny Chan
簡介
TiDB-Lightning Toolset 是一套快速全量匯入 SQL dump 檔案到 TiDB 叢集的工具集,自 2.1.0 版本起隨 TiDB 釋出,速度可達到傳統執行 SQL 匯入方式的至少 3 倍、大約每小時 100 GB,適合在上線前用作遷移現有的大型資料庫到全新的 TiDB 叢集。
設計
TiDB 從 2017 年開始提供全量匯入工具 Loader,它以多執行緒操作、錯誤重試、斷點續傳以及修改一些 TiDB 專屬配置來提升資料匯入速度。
然而,當我們全新初始化一個 TiDB 叢集時,Loader 這種逐條 INSERT 指令線上上執行的方式從根本上是無法盡用效能的。原因在於 SQL 層的操作有太強的保證了。在整個匯入過程中,TiDB 需要:
-
保證 ACID 特性,需要執行完整的事務流程。
-
保證各個 TiKV 伺服器資料量平衡及有足夠的副本,在資料增長的時候需要不斷的分裂、排程 Regions。
這些動作確保 TiDB 整段匯入的期間是穩定的,但在匯入完畢前我們根本不會對外提供服務,這些保證就變成多此一舉了。此外,多執行緒的線上匯入也代表資料是亂序插入的,新的資料範圍會與舊的重疊。TiKV 要求儲存的資料是有序的,大量的亂序寫入會令 TiKV 要不斷地移動原有的資料(這稱為 Compaction),這也會拖慢寫入過程。
TiKV 是使用 RocksDB 以 KV 對的形式儲存資料,這些資料會壓縮成一個個 SST 格式檔案。TiDB-Lightning Toolset使用新的思路,繞過SQL層,線上下將整個 SQL dump 轉化為 KV 對、生成排好序的 SST 檔案,然後直接用 Ingestion 推送到 RocksDB 裡面。這樣批量處理的方法略過 ACID 和線上排序等耗時步驟,讓我們提升最終的速度。
架構
TiDB-Lightning Toolset 包含兩個元件:tidb-lightning 和 tikv-importer。Lightning 負責解析 SQL 成為 KV 對,而 Importer 負責將 KV 對排序與排程、上傳到 TiKV 伺服器。
為什麼要把一個流程拆分成兩個程式呢?
-
Importer 與 TiKV 密不可分、Lightning 與 TiDB 密不可分,Toolset 的兩者皆引用後者為庫,而這樣 Lightning 與 Importer 之間就出現語言衝突:TiKV 是使用 Rust 而 TiDB 是使用 Go 的。把它們拆分為獨立的程式更方便開發,而雙方都需要的 KV 對可以透過 gRPC 傳遞。
-
分開 Importer 和 Lightning 也使橫向擴充套件的方式更為靈活,例如可以執行多個 Lightning,傳送給同一個 Importer。
以下我們會詳細分析每個元件的操作原理。
Lightning
Lightning 現時只支援經 mydumper 匯出的 SQL 備份。mydumper 將每個表的內容分別儲存到不同的檔案,與 mysqldump 不同。這樣不用解析整個資料庫就能平行處理每個表。
首先,Lightning 會掃描 SQL 備份,區分出結構檔案(包含 CREATE TABLE 語句)和資料檔案(包含 INSERT 語句)。結構檔案的內容會直接傳送到 TiDB,用以建立資料庫構型。
然後 Lightning 就會併發處理每一張表的資料。這裡我們只集中看一張表的流程。每個資料檔案的內容都是規律的 INSERT 語句,像是:
INSERT INTO `tbl` VALUES (1, 2, 3), (4, 5, 6), (7, 8, 9);
INSERT INTO `tbl` VALUES (10, 11, 12), (13, 14, 15), (16, 17, 18);
INSERT INTO `tbl` VALUES (19, 20, 21), (22, 23, 24), (25, 26, 27);
複製程式碼
Lightning 會作初步分析,找出每行在檔案的位置並分配一個行號,使得沒有主鍵的表可以唯一的區分每一行。此外亦同時將檔案分割為大小差不多的區塊(預設 256 MiB)。這些區塊也會併發處理,讓資料量大的表也能快速匯入。以下的例子把檔案以 20 位元組為限分割成 5 塊:
Lightning 會直接使用 TiDB 例項來把 SQL 轉換為 KV 對,稱為「KV 編碼器」。與外部的 TiDB 叢集不同,KV 編碼器是寄存在 Lightning 程式內的,而且使用記憶體儲存,所以每執行完一個 INSERT 之後,Lightning 可以直接讀取記憶體獲取轉換後的 KV 對(這些 KV 對包含資料及索引)。
得到 KV 對之後便可以傳送到 Importer。
Importer
因非同步操作的緣故,Importer 得到的原始 KV 對註定是無序的。所以,Importer 要做的第一件事就是要排序。這需要給每個表劃定準備排序的儲存空間,我們稱之為 engine file。
對大資料排序是個解決了很多遍的問題,我們在此使用現有的答案:直接使用 RocksDB。一個 engine file 就相等於本地的 RocksDB,並設定為優化大量寫入操作。而「排序」就相等於將 KV 對全寫入到 engine file 裡,RocksDB 就會幫我們合併、排序,並得到 SST 格式的檔案。
這個 SST 檔案包含整個表的資料和索引,比起 TiKV 的儲存單位 Regions 實在太大了。所以接下來就是要切分成合適的大小(預設為 96 MiB)。Importer 會根據要匯入的資料範圍預先把 Region 分裂好,然後讓 PD 把這些分裂出來的 Region 分散排程到不同的 TiKV 例項上。
最後,Importer 將 SST 上傳到對應 Region 的每個副本上。然後通過 Leader 發起 Ingest 命令,把這個 SST 檔案匯入到 Raft group 裡,完成一個 Region 的匯入過程。
我們傳輸大量資料時,需要自動檢查資料完整,避免忽略掉錯誤。Lightning 會在整個表的 Region 全部匯入後,對比傳送到 Importer 之前這個表的 Checksum,以及在 TiKV 叢集裡面時的 Checksum。如果兩者一樣,我們就有信心說這個表的資料沒有問題。
一個表的 Checksum 是透過計算 KV 對的雜湊值(Hash)產生的。因為 KV 對分佈在不同的 TiKV 例項上,這個 Checksum 函式應該具備結合性;另外,Lightning 傳送 KV 對之前它們是無序的,所以 Checksum 也不應該考慮順序,即服從交換律。也就是說 Checksum 不是簡單的把整個 SST 檔案計算 SHA-256 這樣就了事。
我們的解決辦法是這樣的:先計算每個 KV 對的 CRC64,然後用 XOR 結合在一起,得出一個 64 位元的校驗數字。為減低 Checksum 值衝突的概率,我們目時會計算 KV 對的數量和大小。若速度允許,將來會加入更先進的 Checksum 方式。
總結和下一步計劃
從這篇文章大家可以看到,Lightning 因為跳過了一些複雜、耗時的步驟使得整個匯入程式更快,適合大資料量的初次匯入,接下來我們還會做進一步的改進。
提升匯入速度
現時 Lightning 會原封不動把整條 SQL 命令拋給 KV 編碼器。所以即使我們省去執行分散式 SQL 的開銷,但仍需要進行解析、規劃及優化語句這些不必要或未被專門化的步驟。Lightning 可以呼叫更底層的 TiDB API,縮短 SQL 轉 KV 的行程。
並行匯入
另一方面,儘管我們可以不斷的優化程式程式碼,單機的效能總是有限的。要突破這個界限就需要橫向擴充套件:增加機器來同時匯入。如前面所述,只要每套 TiDB-Lightning Toolset 操作不同的表,它們就能平行導進同一個叢集。可是,現在的版本只支援讀取本機檔案系統上的 SQL dump,設定成多機版就顯得比較麻煩了(要安裝一個共享的網路盤,並且手動分配哪臺機讀取哪張表)。我們計劃讓 Lightning 能從網路獲取 SQL dump(例如通過 S3 API),並提供一個工具自動分割資料庫,降低設定成本。
線上匯入
TiDB-Lightning 在匯入時會把叢集切換到一個專供 Lightning 寫入的模式。目前來說 Lightning 主要用於在進入生產環境之前匯入全量資料,所以在此期間暫停對外提供服務還可以接受。但我們希望支援更多的應用場景,例如回覆備份、儲存 OLAP 的大規模計算結果等等,這些都需要維持叢集線上上。所以接下來的一大方向是考慮怎樣降低 Lightning 對叢集的影響。