TiFlash 原始碼閱讀(三)TiFlash DeltaTree 儲存引擎設計及實現分析 - Part 1

PingCAP發表於2022-06-07

TiFlash 是 TiDB 的分析引擎,是 TiDB HTAP 形態的關鍵元件。TiFlash 原始碼閱讀系列文章將從原始碼層面介紹 TiFlash 的內部實現。希望讀者在閱讀這一系列文章後,能夠對 TiFlash 內部原理有一個清晰的理解,更熟悉 TiFlash 各個流程及概念,甚至能對 TiFlash 進行原始碼級別的程式設計開發。在  上一期原始碼閱讀 中,我們介紹了 TiFlash 的計算層。從本文開始,我們將對 TiFlash 各個元件的設計及實現進行詳細分析。

本文作者:施聞軒,TiFlash 資深研發工程師

背景

PingCAP 自研的 DeltaTree 列式儲存引擎是讓 TiFlash 站在 Clickhouse 巨人肩膀上得以實現可更新列存的關鍵。本文分為兩部分, 主要介紹 DeltaTree 儲存引擎的設計細節及對應的程式碼實現。Part 1 部分主要涉及寫入流程,Part 2 主要涉及讀取流程。

本文基於寫作時最新的 TiFlash v6.1.0 設計及原始碼進行分析。隨著時間推移,新版本中部分設計可能會發生變更,使得本文部分內容失效,請讀者注意甄別。TiFlash v6.1.0 的程式碼可在 TiFlash 的 git repo 中切換到 v6.1.0 tag 進行檢視。

前置知識

TiFlash 關鍵的底層抽象都複用了 Clickhouse 已有的抽象概念,而非完全用 TiDB 抽象概念進行替代。本節首先介紹讀者通常接觸到的 TiDB 抽象概念在 TiFlash 中的形態及對應關係,以便讀者在進一步深入 TiFlash 程式碼後不會產生混淆。

TiDB 邏輯表、物理表、TiFlash 表

在 TiDB、TiKV 及 TiFlash 程式碼中,我們將在 TiDB 中通過  CREATE TABLE SQL  語句建立出來的表稱為「邏輯表」。例如,以下語句將會建立一個「邏輯表」:

CREATE TABLE foo(c INT);

對應地, 我們將實際儲存資料的表稱為「物理表」。對於非分割槽表,物理表與邏輯表相同。對於分割槽表,各個分割槽才是這張邏輯表的物理表。TiKV 及 TiFlash 由於主要涉及資料存取,因此它們絕大多數時候都在與物理表打交道、不關注邏輯表。

以下會建立一個邏輯表,且具有 4 張物理表、在這 4 張物理表上儲存了實際資料:

CREATE TABLE bar (
    id INT NOT NULL,
    store_id INT NOT NULL)PARTITION BY RANGE (store_id) (    PARTITION p0 VALUES LESS THAN (6),    PARTITION p1 VALUES LESS THAN (11),    PARTITION p2 VALUES LESS THAN (16),    PARTITION p3 VALUES LESS THAN (21)
);

小知識

可以通過  TiDB HTTP API 檢視內部表結構。例如,對於前文示例中建立的  foo表及  bar表,查詢出來的表結構如下:

curl { "id": 65, "name": {  "O": "foo",  "L": "foo"
 }, "cols": [...], ...}❯ curl { "id": 67, "name": {  "O": "bar",  "L": "bar"
 }, "cols": [...], "partition": {  ...,  "definitions": [
   {    "id": 68,    "name": {     "O": "p0",     "L": "p0"
    },    ...
   },
   {    "id": 69,    "name": {     "O": "p1",     "L": "p1"
    },    ...
   },
   {    "id": 70,    "name": {     "O": "p2",     "L": "p2"
    },    ...
   },
   {    "id": 71,    "name": {     "O": "p3",     "L": "p3"
    },    ...
   }
  ],
 }, ...}

通過上述查詢結果可知, foo表的邏輯表 ID 為 65,由於沒有分割槽,因此它的物理表 ID 也是 65。 bar表的邏輯表 ID 為 67,它具有四個物理表,ID 分別是 68、69、70、71,這四個分割槽對應的物理表儲存了  bar表中的資料。

TiFlash 中我們維持了 Clickhouse 的表抽象概念。 每一張  TiDB  中的物理表都會對應地在 TiFlash 中建立出一張 Clickhouse 表來儲存資料,並指定儲存引擎為 DeltaTree,關係如下所示:

1.png

例如,一個 ID = 13 的物理表會在 TiFlash 中對應  t_13表。

每張 DeltaTree 引擎的 TiFlash 表內部都對應了一個  StorageDeltaMerge例項(參見  StorageDeltaMerge.h):

2.png

備註 1:DeltaMerge 是 DeltaTree 的前稱。由於 DeltaMerge 與 TiDB 的 Data Migration 產品有一樣的縮寫 DM,因此 DeltaMerge 目前已統一改稱 DeltaTree。程式碼中還未完全清理乾淨,歡迎感興趣的小夥伴參與貢獻。
備註 2:PageStorage 是一個 TiFlash 的抽象儲存層,DeltaTree 引擎的一部分資料通過 PageStorage 模組進行儲存。本文不對 PageStorage 模組做詳細分析,這將由原始碼解讀系列的其他文章進一步展開。

TiDB Region 與 TiFlash 表

熟悉 TiDB 的讀者可能會對 TiDB Region 這個概念比較熟悉。Region 是 TiDB  資料分片(Sharding)的基本單位,一張物理表的資料將會切分到一個或多個 Region 中,從而實現資料分片儲存及計算。在 TiFlash 儲存引擎層面,由於 Region 的存在,因此 每個 TiFlash 表實際上會儲存對應 TiDB 物理表的一部分資料

以下圖為例,假設部署了兩個 TiFlash 節點。若設定了 employee 表的 TiFlash 副本數為 1,則這兩個 TiFlash 節點各將儲存 employee 表的約 50% 資料:

3.png

同樣的,假設 job 表設定的 TiFlash 副本數也為 1,由於它只有一個 Region,因此 job 表的資料會落在其中一個 TiFlash 節點上,其餘 TiFlash 節點上沒有資料。

Handle

在 TiDB 產品(TiDB、TiKV 及 TiFlash)程式碼中會頻繁出現 Handle 一詞。為了相容 MySQL 語法,在 TiDB 產品中通過 SQL 語句指定的主鍵不一定是物理資料中的主鍵。程式碼中將 SQL 語句指定的主鍵稱為 Primary Key,而 物理資料對應的「真正的」、物理主鍵稱為 Handle

TiDB 產品中有以下幾種不同的 Handle:

1.CommonHandle(自 v5.0+ 版本引入)

建立表時若指定主鍵為  聚簇索引(Clustered Index) ,且主鍵不是 INT 型別,則該主鍵對應於 CommonHandle,例如:

-- 指定 VARCHAR 型別的聚簇索引主鍵CREATE TABLE …(id VARCHAR PRIMARY KEY CLUSTERED);-- 指定聚簇索引聯合主鍵CREATE TABLE …(… ,  PRIMARY KEY (a, b) CLUSTERED);

各模組程式碼中往往會採用  is_common_handle == true代表這種情況。

2.IntHandle

建立表時若指定為 INT 或 UNSIGNED INT 型別(INT 的不同種類如 BIGINT、TINYINT 等也包括在內)的主鍵,則這個主鍵對應於 IntHandle,例如:

-- 指定 INT 型別主鍵CREATE TABLE …(id INT PRIMARY KEY);-- 指定 UNSIGNED INT 型別主鍵CREATE TABLE …(id INT UNSIGNED PRIMARY KEY);

各模組程式碼中往往會採用  is_common_handle == false && pk_is_handle == true代表這種情況。

3.TiDB 隱式主鍵

若建立表時沒有指定主鍵,或沒有開啟聚簇索引,則 TiDB 內部會建立一個名為  _tidb_rowid的隱式主鍵,並自動管理該隱式主鍵的值:

-- 指定 VARCHAR 型別非聚簇索引主鍵CREATE TABLE …(id VARCHAR PRIMARY KEY);-- 指定 INT 型別非聚簇索引主鍵CREATE TABLE …(id INT PRIMARY KEY NONCLUSTERED);-- 不指定主鍵CREATE TABLE …(name VARCHAR);

各模組程式碼中往往會採用  is_common_handle == false && pk_is_handle == false代表這種情況。

小知識 1

通過  TiDB HTTP API 檢視內部表結構時可以瞭解這張表的主鍵型別:

mysql> CREATE TABLE yo(id INT PRIMARY KEY);
❯ curl 
{
 "id": 73,
 "name": {
  "O": "yo",
  "L": "yo"
 },
 "pk_is_handle": true,
 "is_common_handle": false,
 ...
}

小知識 2
可以直接通過 SQL 語句查詢出 TiDB 隱式主鍵的值,甚至可以參與運算(如置於 WHERE 子句中):

mysql> CREATE TABLE characters (name VARCHAR(32));
Query OK, 0 rows affected (0.06 sec)
mysql> INSERT INTO characters VALUES ("Klee"), ("Kazuha");
Query OK, 2 rows affected (0.00 sec)
Records: 2  Duplicates: 0  Warnings: 0mysql> SELECT *, _tidb_rowid FROM characters;+--------+-------------+| name   | _tidb_rowid |+--------+-------------+| Klee   |           1 || Kazuha |           2 |+--------+-------------+2 rows in set (0.00 sec)
mysql> select * from characters where _tidb_rowid=2;+--------+| name   |+--------+| Kazuha |+--------+1 row in set (0.00 sec)

儲存引擎基本介面

TiFlash 的 DeltaTree 引擎實現了 Clickhouse 資料表的標準儲存引擎介面  IStorage,允許直接通過 Clickhouse SQL 進行訪問,這樣即可在不引入 TiDB 及 TiKV 的情況下直接對錶上的資料進行簡單的讀寫,對整合測試和除錯都提供了很大的便利。Clickhouse 儲存引擎上標準的讀寫是通過  BlockInputStream及  BlockOutputStream實現的,分別對應寫入和讀取,DeltaTree 也不例外。寫入和讀取的基本單位是  Block(請參見  Block.h)。  Block 以列為單位組織資料,這些列合起來構成了若干行資料。

當然,DeltaTree 引擎本身也需要服務於從 TiKV Raft 協議同步而來的資料寫入,及來自 TiFlash MPP 引擎的資料讀取。

4.png

StorageDeltaMerge是 DeltaTree 儲存引擎的最外層包裝(參見  StorageDeltaMerge.h),它提供了以下介面來實現上述兩類分別來自 TiDB 和 Clickhouse Client 的讀寫需求:

  • 來自 TiDB 的讀請求  StorageDeltaMerge::read() → BlockInputStream

  • 來自 TiDB 的寫請求

    • 從 Raft Log 增量同步: StorageDeltaMerge::write(Block)
  • 從 Raft Snapshot 全量寫入: StorageDeltaMerge::ingestFiles()。並不是所有資料都需要通過 Raft Log 進行增量同步,例如在追加新副本時,往往就通過直接傳遞副本上全量資料(Raft Snapshot)的方式進行副本資料寫入。

  • 除錯及測試目的來自 Clickhouse SQL 讀請求  StorageDeltaMerge::read() → BlockInputStream

  • 除錯及測試目的來自 Clickhouse SQL 寫請求  StorageDeltaMerge::write() → BlockOutputStream

DeltaTree 結構

Segment

DeltaTree 引擎由一組 Segment 構成,Segment 會按需進行分裂及合併。 DeltaTree 儲存的所有資料都按 Handle 列(物理主鍵)進行值域切分,切分為不同的 Segment(參見  Segment.h)。

5.png

Segment 形式上與 Region 有些類似,都是在依據 Handle 進行值域切分。TiFlash 的 Segment 單位較大,以便能夠一次性對比較大的 Column 資料進行批量處理。一個 Segment 往往可以達到 500MB(可通過  dt_segment_limit_size及  dt_segment_limit_rows引數控制),相對應地,Region 一般則不超過 96MB。

注意,Segment 本身與 Region 沒有直接的對齊關係。例如一個 Segment 可以包含一個完整的 Region,或包含很多個 Region,也可能包含了一個 Region 的一部分。

在記憶體中,我們簡單地使用一棵紅黑樹記載所有 Segment: Map<EndHandle, SegmentPtr>,Map 的 Key 為該 Segment 的 EndHandleKey。這使得我們能非常輕易地基於 Handle 找到它對應的 Segment。

Delta Layer, Stable Layer

單個 Segment 內部進一步按時域分為兩層,一層是 Delta Layer(參見  DeltaValueSpace.h,一層是 Stable Layer(參見  StableValueSpace.h)。可以簡單地想象成是一個兩層的 LSM Tree:

6.png

Delta Layer 及 Stable Layer 在值域上是重疊的,它們都會包含整個 Segment 值域空間中的資料。 新寫入或更新的資料儲存在 Delta Layer 中,定期 Compaction 形成 Stable Layer。其中單個 Segment 內的 Delta Layer 一般佔 Segment 內資料的 5% 左右、剩餘在 Stable Layer 中。

由於 Delta Layer 主要儲存新寫入的資料,與寫入密切相關,而絕大多數需要讀取的資料又在 Stable Layer 中,因此這種雙層設計給予了我們分別進行優化的空間,這兩層我們採用了不同的儲存結構。Delta Layer 主要面向寫入場景進行優化,而 Stable Layer 則主要面向讀取場景進行優化。

MVCC

為了與 TiDB 的 MVCC 相容,除了使用者在建立 TiDB 表指定的列以外,DeltaTree 實際還會額外儲存以下兩列資料:

MVCC 版本列

該列儲存了   TiKV  同步而來的行資料中記載的 commit_ts 的值,即  MVCC  版本號。通過讀取的時候按照該列進行過濾,TiDB 就能在訪問 TiKV 及 TiFlash 時獲得一致的快照隔離級別資料。若對同一行資料進行了多次更新,那麼它們將產生不同的 MVCC 版本號。不同版本的相同行的資料將在 GC 的時候被清理。

刪除標記列(Delete mark)

該列為 1 時代表對應行的資料被刪除。例如在 TiDB 中執行 DELETE 語句後,每一個刪除的行在同步到 TiFlash 上後都成為了 Delete mark = 1 的列資料。這些資料會儲存在表中,以便在讀的時候對其進行過濾。這些資料會在 GC 的時候被清理。

寫入

寫入相關流程

與寫入有關的流程大致如下:

1.寫入時接受 Block 為單位的資料,資料置於記憶體中,對應結構為  MemTableSet(參見  MemTableSet.h

2.DeltaTree 後臺定期將記憶體中的 MemTableSet 寫入到磁碟上(這個過程稱為  Flush),形成磁碟上持久化了的 Delta 層資料

實際上,Delta 層資料並非是直接操作檔案、儲存在檔案系統中,而是 通過 PageStorage 模組進行儲存。 PageStorage 是一層簡單的物件儲存層,提供了諸如快照、回滾、合併小 IO 等功能,針對 Delta 層資料高頻 IO 等特性進行了優化。PageStorage 模組的詳細設計分析將在原始碼閱讀的後續文章中做詳細介紹,本文不做展開。

3.DeltaTree 後臺定期將磁碟上 Delta 層的資料與磁碟上 Stable 層的資料進行合併(這個過程稱為  Merge Delta,也稱為 Major Compaction),並寫入磁碟,形成新的 Stable 層資料

7.png

該流程與標準的 LSM Tree 比較相似。

ColumnFile

DeltaTree 引擎對 Delta Layer 及 Stable Layer 採用了不同的結構,分別針對寫入和讀取場景進行鍼對性優化。在 Delta Layer 中,資料的粒度是 ColumnFile。

  • 接受 Block 寫入資料時,Block 會被包裹成 ColumnFileInMemory,追加到記憶體的 MemTableSet 中。ColumnFileInMemory 代表它包含了在記憶體中的、尚未被持久化的 Block 資料。
  • Flush 時,ColumnFileInMemory 中的資料會被寫入到磁碟中(通過 PageStorage 儲存),相應地,記憶體中結構會被替換成 ColumnFileTiny(繼承自 ColumnFilePersisted),代表它內部的 Block 資料已經被持久化在磁碟上了、記憶體中僅有它的 metadata 資訊,存放在 ColumnFilePersistedSet 中。

8.png

除了上述兩種 ColumnFile 以外,還有其他 ColumnFile 也比較重要,以下是一個 ColumnFile 的總體列表:

ColumnFileInMemory

該結構包含 Block 資料, 資料在記憶體中、尚未被持久化。參見  ColumnFileInMemory.h

大多數對 DeltaTree 引擎的寫入操作都會封裝為 ColumnFileInMemory 進行後續處理。

ColumnFilePersisted

它僅僅是一個虛類,代表了所有繼承自它的 ColumnFile 的資料都已經持久化在了磁碟中。參見  ColumnFilePersisted.h

ColumnFileBig

繼承自 ColumnFilePersisted。它指向一個已經儲存於磁碟上的 DMFile 資料,參見  ColumnFileBig.h。DMFile 是 Stable 層資料的基本格式,後邊將進行詳細解釋。

在接受來自 Raft 層的全量資料快照(Raft Snapshot)時,構建的就是 ColumnFileBig 而非 ColumnFileInMemory。除此以外,Major Compaction 過程也會構建 ColumnFileBig。

ColumnFileTiny

繼承自 ColumnFilePersisted。如前文所述,它指向一個已經儲存在了 PageStorage 中的 Delta 層 Block 資料,參見  ColumnFileTiny.h

在 Flush 過程中,ColumnFileInMemory 會在將資料持久化後將自己轉化為 ColumnFileTiny 來標記自己的資料已經被持久化了。除此以外,若寫入過程收到的資料塊較大,也會直接構造出 ColumnFileTiny,從而節約記憶體使用。

ColumnFileDeleteRange

繼承自 ColumnFilePersisted。它代表在一個 Handle  範圍內所有資料都被清除了,參見  ColumnFileDeleteRange.h。例如,在加入新 TiFlash 節點後,其他 TiFlash 節點上副本的資料會被重新排程、以達到分佈均勻的狀態。此時會有 Region 副本在某些 TiFlash 節點上被擦除。這種範圍內無差別的資料擦除便是通過 ColumnFileDeleteRange 來實現的,避免了普通的資料刪除過程中需要先讀取、再寫入刪除標記這種低效率的方式。

前臺寫入步驟

9.png

DeltaTree 對外提供的寫入介面中會做這些事情:

1.對收到的 Block 進行排序。

排序方式是 (Handle, Version)。這個排序方式與 TiKV 一致,使得 TiFlash 能保持和 TiKV 一樣的資料先後順序。

2.對 Block 按照 Segment 值域進行切分,並寫入到各個 Segment 的 MemTableSet 中。

在寫入的過程中,若當前 Segment 已積壓的資料過多了,寫入會被阻塞(Write Stall)並等待 Segment 完成更新。例如,可能使用者猛烈地寫入了大批資料,積壓了大量資料來不及進行 Flush 或進行 Compaction。

若不需要 Write Stall,則 Block 資料會被寫入到一個已有的、位於 MemTableSet 的 ColumnFileInMemory 中,或 Block 資料比較大的話,則寫入到一個 ColumnFileTiny 中、再加入 MemTableSet。

3.嘗試對 Segment 進行更新。

例如,嘗試觸發 Flush、Compaction、Segment 的合併和分裂等。

此時,單次寫入操作便已完成。詳情可參見  DeltaMergeStore::write(Block)函式了解詳細實現。

需要注意的是,在前臺寫入路徑上, 資料寫入到記憶體  **MemTableSet** 中就寫入完畢、可以返回了,後續涉及磁碟 IO 的 Flush 及 Merge Delta 操作都是後臺操作,不會對寫入延遲產生直接影響。另外,由於 IO 發生在 Flush 階段,而非寫入階段,因此這也起到了對於高頻寫入減少 IO 的效果。

既然寫入返回時資料還沒寫入到磁碟上,那麼此時掉電了怎麼辦?實際上由於 TiFlash 從 TiKV Raft log 同步資料,因此  Raft log 即為 TiFlash 資料的 WAL。在掉電後,從上次已經完成 Flush 操作的 Raft Apply 位置恢復資料即可。

Flush 步驟

10.png

通過 Flush 過程,記憶體中的資料會被寫入到 Delta Layer 的持久化儲存(PageStorage)中,步驟如下:

  1. 對 DeltaValueSpace 上鎖並將所有的 MemTableSet 中的 ColumnFile 提取出來,構建出待 Flush 的任務列表。
  2. Prepare
    • 對每個 ColumnFile 再次按照 (Handle, Version) 進行排序。雖然每次寫入過程中,待寫入的 Block 本身會按照 (Handle, Version) 進行排序,但多次寫入的 Block 可能會被追加到相同的 ColumnFileInMemory 中,因此在 ColumnFileInMemory 並不保證有序,Flush 的時候會再次進行排序。
    • 將排序後的資料寫入 PageStorage,此時涉及磁碟 IO。
  3. 對 DeltaValueSpace 上鎖,並 Apply
    • 將每一個已經完成寫入的 ColumnFileInMemory 替換成 ColumnFilePersisted,放入 ColumnFilePersistedSet 記憶體結構。
    • 若這個過程失敗了,則對已經寫入 PageStorage 的資料進行回滾。

在上述過程中,有一個比較有意思的設計是, DeltaTree 會採用類似於樂觀鎖的方式,儘可能減少上鎖時間,並採用事後回退的方式處理衝突。例如,多個 Flush 可能同時發生——一個 Flush 在前臺寫入中觸發,一個 Flush 在後臺觸發。在這個情況下,只有一個 Flush 會完成併成功修改記憶體結構。通過這種設計,整個結構上鎖的時間內去除了可能有顯著延遲的 IO 等操作,從而縮短了整個結構的上鎖時間,提高了效能。讀者會在接下來的其他 DeltaTree 的步驟中頻繁地見到這種上鎖模式。

詳細可參見  DeltaValueSpace::flush函式及  ColumnFileFlushTask.h

Minor Compaction 步驟

11.png

ColumnFilePersistedSet 可能會包含比較多的零碎小資料塊,這些小資料塊直到觸發 Major Compaction(即 Merge Delta)時才會被清理、合併,這會對讀的過程帶來比較高的 IOPS 。為了節約讀 IOPS,DeltaTree 後臺會持續對零碎的、小的 ColumnFileTiny 進行合併,合成一個大的 ColumnFileTiny,這個過程稱為 Minor Compaction。

DeltaTree 的 Minor Compaction 過程會形成類似於 LSM Tree 的多層結構,與 LSM Tree 有些相似,但不完全一致。例如,在當前設計中,Delta Layer 每一層的每一個 ColumnFileTiny 都不保證有序(合併 ColumnFileTiny 時僅僅是簡單地資料頭尾相接),而且各層的 ColumnFileTiny 之間也會有值域重疊。因此,在發起讀請求的時候,事實上這些 ColumnFileTiny 實際上都有可能需要被讀取到。

Minor Compaction 過程如下,同樣也是 Lock + Prepare + Lock & Apply 的模式:

  1. 對 DeltaValueSpace 上鎖,並提取某一層中比較小的 ColumnFileTiny
  2. Prepare
    • 將這些 ColumnFileTiny 資料進行簡單的頭尾合併成一個新的 ColumnFileTiny,然後寫入到下一層
  3. 對 DeltaValueSpace 上鎖,並 Apply
    • 將 Prepare 過程中新生成的 ColumnFileTiny 及合併掉的 ColumnFileTiny 在記憶體結構中進行更新

詳細可參見  DeltaValueSpace::compact函式。

Major Compaction (Merge Delta) 步驟

12.png

Delta 層已持久化的增量更新資料與 Stable 層已持久化的、面向讀優化的大部分資料進行合併的過程稱為 Merge Delta,它也是整個 DeltaTree 儲存引擎最主要的資料整理操作(Major Compaction)。在這個過程中,該 Segment 的 整個 Stable 層資料會與整個 Delta 層資料進行合併,替換生成一個新的 Stable 層資料,步驟如下:

  1. 對整個 DeltaTree 儲存層上讀鎖,從而取得一個 Delta 層資料、Stable 層資料、當前表結構(Schema)的快照
  2. Prepare
    • 從 Delta 層級 Stable 層聯合讀取有序、去重的資料
    • 將資料寫入到一個 DMFile 作為新的 Stable 層
  3. 對整個 DeltaTree 儲存層上寫鎖,並 Apply
    • 清理現有 Delta 及 Stable 層資料,並將新的 Stable 層資料在記憶體結構中進行更新

詳細可參見  DeltaMergeStore::segmentMergeDelta函式。

Stable 層物理儲存結構

Stable 層的資料按照 (Handle, Version) 排序並切分了多個 Pack 作為 IO 粒度(每個 Pack 大約是 8192 行,通過  dt_segment_stable_pack_rows引數控制)。單一列內資料相鄰地儲存在一起,總體邏輯結構如下圖所示:

13.png

不同於 Delta 通過 PageStorage 在磁碟上儲存資料,Stable 層直接將上述結構及資料儲存在磁碟檔案上,該儲存格式被稱為  DMFile。雖然名字中有個 file,但  DMFile實際是一個資料夾,其內部包含的檔案如下所示:

  • dmf_/pack:

儲存了每個 Pack 的資訊,例如 pack 中實際有多少行等等。詳細可參見  PackStats結構。

  • dmf_/meta.txt:

記錄了 DMFile 的格式(例如 V1、V2)等。

  • dmf_/config:

記錄了該 DMF 的一些配置資訊,目前主要包含各個資料檔案的 Checksum 方式等配置。詳細可參見  DMChecksumConfig結構。

  • dmf_/.dat

壓縮儲存了 col_id 列的資料。預設情況下壓縮方式是 LZ4,可通過  dt_compression_method引數進行配置。

  • dmf_/.mrk

標記檔案,儲存了各個 Pack 在 .dat 檔案中的 offset。在讀取資料內容時,可以通過這個標記檔案中記錄的偏移資訊,跳過並只讀取特定 Pack 的資料。詳細可參見  MarkInCompressedFile結構。

  • dmf_/.idx

索引檔案,目前 DeltaTree 只支援 Min Max 索引,該檔案會儲存 col_id 列在各個 Pack 區間上的最大最小值。在查詢時,一些列上的查詢條件可通過這裡的 Min Max 索引跳過不需要的 Pack,從而減少 IO。詳細可參見  MinMaxIndex結構。

動手實踐!

對一個系統加深理解的最好方法莫過於動手實踐了。由於 TiFlash 保留了 Clickhouse Client 相容的 SQL 查詢介面,因此可以通過這個內部介面來對本文中描述的各種概念進行實驗。

啟動包含 TiFlash 的 TiDB 叢集后,可以通過  tiup tiflash client快捷地通過 Clickhouse SQL 介面連入 TiFlash:

# Start Server$ tiup playground nightly# Run TiFlash Client$ tiup tiflash client --host 127.0.0.1

連入後,你可以執行大部分 Clickhouse SQL 語句(推薦僅進行查詢語句),例如  SELECTSHOW TABLES等,也可以執行 TiFlash 特有的 Clickhouse SQL 語句,如:

14.png

除了  SELRAW語句以外, DBGInvoke也是一個常用的內部語句,本文不作詳細展開,讀者可在 TiFlash 原始碼中搜尋  > DBGInvoke查詢到在各個測試檔案中是如何呼叫  DBGInvoke語句查詢或操作內部結構的。

結語

本文主要針對 DeltaTree 引擎寫入過程中涉及到的各個模組及其設計進行了分析。由於篇幅原因,從 DeltaTree 引擎中讀資料的過程及相應優化將在下一篇中進行分析,讀者可關注 TiFlash 原始碼解讀的後續更新。另外,本文也僅僅是呈現了一個 TiFlash 給出的「答案」,即儲存引擎設計成什麼樣可以支撐可更新、可高頻寫入、可進行高效能 OLAP 分析這些需求。至於這個「答案」本身是如何的得出來的、背後的設計思路及取捨並沒有涵蓋。我們將在下一期 TiFlash 原始碼閱讀中給出詳細的介紹。

體驗全新的一棧式實時 HTAP 資料庫,即刻註冊 TiDB Cloud,線上申請 PoC 並獲得專業技術支援。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69994146/viewspace-2899173/,如需轉載,請註明出處,否則將追究法律責任。

相關文章