背景
本系列會聚焦在 TiFlash 自身,讀者需要有一些對 TiDB 基本的知識。可以通過這三篇文章瞭解 TiDB 體系裡的一些概念《 說儲存 》、《 說計算 》、《 談排程 》。
今天的主角 -- TiFlash 是 TiDB HTAP 形態的關鍵元件,它是 TiKV 的列存擴充套件,通過 Raft Learner 協議非同步複製,但提供與 TiKV 一樣的快照隔離支援。我們用這個架構解決了 HTAP 場景的隔離性以及列存同步的問題。自 5.0 引入 MPP 後,也進一步增強了 TiDB 在實時分析場景下的計算加速能力。
上圖描述了 TiFlash 整體邏輯模組的劃分,通過 Raft Learner Proxy 接入到 TiDB 的 multi-raft 體系中。我們可以對照著 TiKV 來看:計算層的 MPP 能夠在 TiFlash 之間做資料交換,擁有更強的分析計算能力;作為列存引擎,我們有一個 schema 的模組負責與 TiDB 的表結構進行同步,將 TiKV 同步過來的資料轉換為列的形式,並寫入到列存引擎中;最下面的一塊,是稍後會介紹的列存引擎,我們將它命名為 DeltaTree 引擎。
有持續關注 TiDB 的使用者可能之前閱讀過 《TiDB 的列式儲存引擎是如何實現的?》 這篇文章,近期隨著 TiFlash 開源 ,也有新的使用者想更多地瞭解 TiFlash 的內部實現。這篇文章會從更接近程式碼層面,來介紹 TiFlash 內部實現的一些細節。
這裡是 TiFlash 內一些重要的模組劃分以及它們對應在程式碼中的位置。在今天的分享和後續的系列裡,會逐漸對裡面的模組開展介紹。
TiFlash 模組對應的程式碼位置
dbms/
└── src
├── AggregateFunctions, Functions, DataStreams # 函式、運算元
├── DataTypes, Columns, Core # 型別、列、Block
├── IO, Common, Encryption # IO、輔助類
├── Debug # TiFlash Debug 輔助函式
├── Flash # Coprocessor、MPP 邏輯
├── Server # 程式啟動入口
├── Storages
│ ├── IStorage.h # Storage 抽象
│ ├── StorageDeltaMerge.h # DeltaTree 入口
│ ├── DeltaMerge # DeltaTree 內部各個元件
│ ├── Page # PageStorage
│ └── Transaction # Raft 接入、Scehma 同步等。 待重構 https://github.com/pingcap/tiflash/issues/4646
└── TestUtils # Unittest 輔助類
TiFlash 中的一些基本元素抽象
TiFlash 這款引擎的程式碼是 18 年從 ClickHouse fork。ClickHouse 為 TiFlash 提供了一套效能十分強勁的向量化執行引擎,我們將其當做 TiFlash 的單機的計算引擎使用。在此基礎上,我們增加了針對 TiDB 前端的對接,MySQL 相容,Raft 協議和叢集模式,實時更新列存引擎,MPP 架構等等。雖然和原本的 Clickhouse 已經完全不是一回事,但程式碼自然地 TiFlash 程式碼繼承自 ClickHouse,也沿用著 CH 的一些抽象。比如:
IColumn 代表記憶體裡面以列方式組織的資料。IDataType 是資料型別的抽象。Block 則是由多個 IColumn 組成的資料塊,它是執行過程中,資料處理的基本單位。
在執行過程中,Block 會被組織為流的形式,以 BlockInputStream 的方式,從儲存層 “流入” 計算層。而 BlockOutputStream,則一般從執行引擎往儲存層或其他節點 “寫出” 資料。
IStorage 則是對儲存層的抽象,定義了資料寫入、讀取、DDL 操作、表鎖等基本操作。
DeltaTree 引擎
雖然 TiFlash 基本沿用了 CH 的向量化計算引擎,但是儲存層最終沒有沿用 CH 的 MergeTree 引擎,而是重新研發了一套更適合 HTAP 場景的列存引擎,我們稱為 DeltaTree,對應程式碼中的 " StorageDeltaMerge "。
DeltaTree 引擎解決的是什麼問題
A. 原生支援高頻率資料寫入,適合對接 TP 系統,更好地支援 HTAP 場景下的分析工作。
B. 支援列存實時更新的前提下更好的讀效能。它的設計目標是優先考慮 Scan 讀效能,相對於 CH 原生的 MergeTree 可能部分犧牲寫效能
C. 符合 TiDB 的事務模型,支援 MVCC 過濾
D. 資料被分片管理,可以更方便的提供一些列存特性,從而更好的支援分析場景,比如支援 rough set index
為什麼我們說 DeltaTree 引擎具備上面特性呢? ?回答這個疑問之前,我們先回顧下 CH 原生的 MergeTree 引擎存在什麼問題。MergeTree 引擎可以理解為經典的 LSM Tree(Log Structured Merge Tree)的一種列存實現,它的每個 "part 資料夾" 對應 SSTFile(Sorted Strings Table File)。最開始,MergeTree 引擎是沒有 WAL 的,每次寫入,即使只有 1 條資料,也會將資料需要生成一個 part。因此如果使用 MergeTree 引擎承接高頻寫入的資料,磁碟上會形成大量碎片的檔案。這個時候,MergeTree 引擎的寫入效能和讀取效能都會出現嚴重的波動。這個問題直到 2020 年,CH 給 MergeTree 引擎引入了 WAL,才部分緩解這個壓力 ClickHouse/8290 。
那麼是不是有了 WAL,MergeTree 引擎就可以很好地承載 TiDB 的資料了呢?還不足夠。因為 TiDB 是一個通過 MVCC 實現了 Snapshot Isolation 級別事務的關係型資料庫。這就決定了 TiFlash 承載的負載會有比較多的資料更新操作,而承載的讀請求,都會需要通過 MVCC 版本過濾,篩選出需要讀的資料。而以 LSM Tree 形式組織資料的話,在處理 Scan 操作的時候,會需要從 L0 的所有檔案,以及其他層中 與查詢的 key-range 有 overlap 的所有檔案,以堆排序的形式合併、過濾資料。在合併資料的這個入堆、出堆的過程中, CPU 的分支經常會 miss,cache 命中也會很低。測試結果表明,在處理 Scan 請求的時候,大量的 CPU 都消耗在這個堆排序的過程中。
另外,採用 LSM Tree 結構,對於過期資料的清理,通常在 level compaction 的過程中,才能被清理掉(即 Lk-1 層與 Lk 層 overlap 的檔案進行 compaction)。而 level compaction 的過程造成的寫放大會比較嚴重。當後臺 compaction 流量比較大的時候,會影響到前臺的寫入和資料讀取的效能,造成效能不穩定。
MergeTree 引擎上面的三點:寫入碎片、Scan 時 CPU cache miss 嚴重、以及清理過期資料時的 compaction ,造成基於 MergeTree 引擎構建的帶事務的儲存引擎,在有資料更新的 HTAP 場景下,讀、寫效能都會有較大的波動。
DeltaTree 的解決思路以及模組劃分
在看實現之前,我們來看看 DeltaTree 的療效如何。上圖是 Delta Tree 與基於 MergeTree 實現的帶事務支援的列存引擎在不同資料量(Tuple number)以及不同更新 TPS (Transactions per second) 下的讀 (Scan) 耗時對比。可以看到 DeltaTree 在這個場景下的讀效能基本能達到後者的兩倍。
那麼 DeltaTree 具體面對上述問題,是如何設計的呢?
首先,我們在表內,把資料按照 handle 列的 key-range,橫向分割進行資料管理,每個分片稱為 Segment。這樣在 compaction 的時候,不同 Segment 間的資料就獨立地進行資料整理,能夠減少寫放大。這方面與 PebblesDB[1] 的思路有點類似。
另外,在每個 Segment 中,我們採用了 delta-stable 的形式,即最新的修改資料寫入的時候,被組織在一個寫優化的結構的末尾( DeltaValueSpace.h ),定期被合併到一個為讀優化的結構中( StableValueSpace.h )。Stable Layer 存放相對老的,資料量較大的資料,它不能被修改,只能被 replace。當 Delta Layer 寫滿之後,與 Stable Layer 做一次 Merge(這個動作稱為 Delta Merge),從而得到新的 Stable Layer,並優化讀效能。很多支援更新的列存,都是採用類似 delta-stable 這種形式來組織資料,比如 Apache Kudu[2]。有興趣的讀者還可以看看《Fast scans on key-value stores》[3] 的論文,其中對於如何組織資料,MVCC 資料的組織、對過期資料 GC 等方面的優劣取捨都做了分析,最終作者也是選擇了 delta-main 加列存這樣的形式。
Delta Layer 的資料,我們通過一個 PageStorage 的結構來儲存資料,Stable Layer 我們主要通過 DTFile 來儲存資料、通過 PageStorage 來管理生命週期。另外還有 Segment、DeltaValueSpace、StableValueSpace 的元資訊,我們也是通過 PageStorage 來儲存。上面三者分別對應 DeltaTree 中 StoragePool 這一資料結構的 log, data 以及 meta。
PageStorage 模組
上面提到, Delta Layer 的資料和 DeltaTree 儲存引擎的一些後設資料,這類較小的資料塊,在序列化為位元組串之後,作為 "Page" 寫入到 PageStorage 來進行儲存。PageStorage 是 TiFlash 中的一個儲存的抽象元件,類似物件儲存。它主要設計面向的場景是 Delta Layer 的高頻讀取:比如在 snapshot 上,以 PageID (或多個 PageID) 做點查的場景;以及相對於 Stable Layer 較高頻的寫入。PageStorage 層的 "Page" 資料塊典型大小為數 KiB~MiB。
PageStorage 是一個比較複雜的元件,今天先不介紹它內部的構造。讀者可以先理解 PageStorage 至少提供以下 3 點功能:
提供 WriteBatch 介面,保證寫入 WriteBatch 的原子性
提供 Snapshot 功能,可以獲取一個不阻塞寫的只讀 view
提供讀取 Page 內部分資料的能力(只讀選擇的列資料)
讀索引 DeltaTree Index
前面提到,在 LSM-Tree 上做多路歸併比較耗 CPU,那我們是否可以避免每次讀都要重新做一次呢?答案是可以的。事實上有一些記憶體資料庫已經實踐了類似的思路。具體的思路是,第一次 Scan 完成後,我們把多路歸併演算法產生的資訊想辦法存下來,從而使下一次 Scan 可以重複利用。這份可以被重複利用的資訊我們稱為 Delta Index,它由一棵 B+ Tree 實現。利用 Delta Index,把 Delta Layer 和 Stable Layer 合併到一起,輸出一個排好序的 Stream。Delta Index 幫助我們把 CPU bound、而且存在很多 cache miss 的 merge 操作,轉化為大部分情況下一些連續記憶體塊的 copy 操作,進而優化 Scan 的效能。
Rough Set Index
很多資料庫都會在資料塊上加統計資訊,以便查詢時可以過濾資料塊,減少不必要的 IO 操作。有的將這個輔助的結構稱為 KnowledgeNode、有的叫 ZoneMaps。TiFlash 參考了 InfoBright [4] 的開源實現,採用了 Rough Set Index 這個名字,中文叫粗粒度索引。
TiFlash 給 SelectQueryInfo 結構中新增了一個 MvccQueryInfo 的結構,裡面會帶上查詢的 key-ranges 資訊。DeltaTree 在處理的時候,首先會根據 key-ranges 做 segment 級別的過濾。另外,也會從 DAGRequest 中將查詢的 Filter 轉化為 RSFilter 的結構,並且在讀取資料時,利用 RSFilter,做 ColumnFile 中資料塊級別的過濾。
在 TiFlash 內做 Rough Set Filter,跟一般的 AP 資料庫不同點,主要在還需要考慮粗粒度索引對 MVCC 正確性的影響。比如表有三列 a、b 以及寫入的版本 tso,其中 a 是主鍵。在 t0 時刻寫入了一行 Insert (x, 100, t0),它在 Stable VS 的資料塊中。在 t1 時刻寫入了一個刪除標記 Delete(x, 0, t1),這個標記存在 Delta Layer 中。這時候來一個查詢 select * from T where b = 100,很顯然如果我們在 Stable Layer 和 Delta Layer 中都做索引過濾,那麼 Stable 的資料塊可以被選中,而 Delta 的資料塊被過濾掉。這時候就會造成 (x, 100, t0) 這一行被錯誤地返回給上層,因為它的刪除標記被我們丟棄了。
因此 TiFlash Delta layer 的資料塊,只會應用 handle 列的索引。非 handle 列上的 Rough Set Index 主要應用於 Stable 資料塊的過濾。一般情況下 Stable 資料量佔 90%+,因此整體的過濾效果還不錯。
程式碼模組
下面是 DeltaTree 引擎內各個模組對應的程式碼位置,讀者可以回憶一下前文,它們分別對應前文的哪一部分 ;)
DeltaTree 引擎內各模組對應的程式碼位置
dbms/src/Storages/
├── Page # PageStorage
└── DeltaMerge
├── DeltaMergeStore.h # DeltaTree 引擎的定義
├── Segment.h # Segment
├── StableValueSpace.h # Stable Layer
├── Delta # Delta Layer
├── DeltaMerge.h # Stable 與 Delta merge 過程
├── File # Stable Layer 的儲存格式
├── DeltaTree.h, DeltaIndex.h # Delta Index
├── Index, Filter, FilterParser # Rough Set Filter
└── DMVersionFilterBlockInputStream.h # MVCC Filtering
小結
本篇文章主要介紹了 TiFlash 整體的模組分層,以及在 TiDB 的 HTAP 場景下,儲存層 DeltaTree 引擎如何進行優化的思路。簡單介紹了 DeltaTree 內元件的構成和作用,但是略去了一些細節,比如 PageStorage 的內部實現,DeltaIndex 如何構建、應對更新,TiFlash 是如何接入 multi-Raft 等問題。更多的程式碼閱讀內容會在後面的章節中逐步展開,敬請期待。
體驗全新的一棧式實時 HTAP 資料庫,即刻註冊 TiDB Cloud,免費試用 TiDB Developer Tier 一年。
相關文章
[1] SOSP'17: PebblesDB: Building Key-Value Stores using Fragmented Log-Structured Merge Trees
[2] Kudu: Storage for Fast Analytics on Fast Data
[3] VLDB'17: Fast scans on key-value stores
[4] Brighthouse: an analytic data warehouse for ad-hoc queries