TiFlash 原始碼閱讀(四)TiFlash DDL 模組設計及實現分析

語言: CN / TW / HK

TiFlash 是 TiDB 的分析引擎,是 TiDB HTAP 形態的關鍵元件。TiFlash 原始碼閱讀系列文章將從原始碼層面介紹 TiFlash 的內部實現。在上一期原始碼閱讀中,我們介紹了 TiFlash 的儲存引擎,本文將介紹 TiFlash DDL 模組的相關內容,包括 DDL 模組的設計思路, 以及具體程式碼實現的方式。

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

Overview

本章節,我們會先對 DDL 模組做一個 overview 的介紹,介紹 DDL 在 TiFlash 中相關的場景,以及 TiFlash 中 DDL 模組整體的設計思想

這邊的 DDL 模組指的是對應負責處理 add column, drop column, drop table recover table 等這一系列 DDL 語句的模組,也是負責跟各資料庫和表的 schema 資訊打交道的模組。

DDL 模組在 TiFlash 中的相關場景

圖一 TiFlash 架構示意圖

圖一 是 TiFlash 的架構示意圖,上方是 TiDB/TiSpark 的計算層節點,虛線的左邊是四個 TiKV 的節點,右邊就是兩個 TiFlash 節點。這張圖體現的是TiFlash 一個重要的設計理念:通過利用 Raft 的共識演算法,TiFlash 會作為 Raft 的 Learner 節點加入 Raft group 來進行資料的非同步複製。Raft Group 指的是 TiKV 中由多個 region 副本組成的 raft leader 以及 raft follower 組成的 group。從 TiKV 同步到 TiFlash 的資料,在 TiFlash 中同樣是按照 region 劃分的,但是在內部會通過列存的方式來存到 TiFlash 的列式儲存引擎中。

圖二 TiFlash 架構示意圖(含 Schema)

圖二是一個概覽的架構設計,掩蓋了許多細節部分。其中圖中這兩個紅圈對應的部分,就是本文要討論的主角 DDL模組

下方的紅圈是關於 TiFlash 的寫操作。TiFlash 節點是以 learner 角色加入到了 TiKV 中一個個 region 對應的 raft group 中,通過 raft leader 不斷髮送 raft log 或者 raft snapshot 來給 learner 節點同步資料。 但是因為 TiKV 中資料都是行存的格式,而我們 TiFlash 中需要的資料則是列存的格式,所以 TiFlash 節點在接收到 TiKV 傳送過來的這個行存格式的資料以後,需要把他進行一個行轉列的轉換,轉換成需要的列存的格式。而這個轉換,就需要依賴對應表的 schema 資訊來完成。同樣,上方的紅圈指的是在 TiDB/TiSpark 來 TiFlash 中讀取資料的過程,這個讀資料的過程同樣也是依賴 schema 來進行參與解析的。因此,TiFlash 的讀寫操作都是需要強依賴 schema 的,schema 在 TiFlash 中亦是有重要的作用的

DDL 模組整體設計思想

在具體瞭解 TiFlash DDL 模組的整體設計思想之前,我們先來了解一下 DDL 模組在 TiDB 和 TiKV 中的對應情況,因為 TiFlash 接收到的 schema 的變更資訊亦是從 TiKV 節點發送的。

TiDB 中 DDL 模組基本情況

TiDB 的 DDL 模組是借鑑 Google F1 來實現的在分散式場景下,無鎖並且線上的 schema 變更。具體的實現可以參考 TiDB 原始碼閱讀系列文章(十七)DDL 原始碼解析 | PingCAP。TiDB 的 DDL 機制提供了兩大特點: 1. DDL 操作會盡可能避免發生 data reorg(data reorg 指的是在表中進行資料的增刪改)。

  • 圖三這個 add column 的例子裡面,原表有 a b 兩列以及兩行資料。當我們進行 add column 這個 DDL 操作時,我們不會在原有兩行中給新增的 c 列填上預設值。如果後續有讀操作會讀到這兩行的資料,我們則會在讀的結果中給 c 列填上預設值。通過這樣的方式,我們來避免在 DDL 操作的時候發生 data reorg。諸如 add column, drop column,以及整數型別的擴列操作,都不需要觸發 data reorg 的。

圖三 add column 樣例

  • 但是對於有損變更的 DDL 操作(例如:縮短列長度(後續簡稱縮列)的操作,可能會導致使用者資料截斷的 DDL變更),我們不可避免會發生 data reorg。但是在有損變更的場景下,我們也不會在表的原始列上進行資料修改重寫的操作,而是通過新增列,在新增列上進行轉換,最後刪除原列,對新增列更名的方式來完成 DDL 操作。圖四這個縮列 (modify column) 的例子中,我們原表中有 a, b 兩列 ,此次 DDL 操作需要把 a 列從 int 型別縮成 tiny int 型別。整個 DDL 操作的過程為:

    • 先新增一列隱藏列 _col_a_0。
    • 把原始 a 列中的數值進行轉換寫到隱藏列 _col_a_0 上。
    • 轉換完成後,將原始的 a 列刪除,並且將 _col_a_0 列重新命名為 a 列。(這邊提到的刪除 a 列也並非物理上把 a 列的數值刪除,是通過修改 meta 資訊的方式來實現的)*

圖四 modify column 樣例

另外對於縮列這個 DDL 操作本身,我們要求縮列過程中不會發生資料的丟失。比如要從 int 縮成 tinyint時,就要求原有列的值都是在 tinyint 範圍內的,而不支援出現本身超出 tinyint 的值轉換成 tinyint 型別。對於後者的情況,會直接報錯 overflow,縮列操作失敗。

  1. 相對資料更新的 schema 永遠可以解析舊的資料。這一條結論亦是我們後面 TiFlash DDL 模組依賴的一條重要的保證。這個保證是依賴我們行存資料的格式來實現的。在存資料的時候,我們是將column id 和 column value 一起儲存的,而非column name和column value一起儲存。另外我們的行存格式可以簡化的理解為是一個 column_id → data 的一個 map 方式(實際上我們的行存並非一個 map,而是用二進位制編碼的方式來儲存的,具體可以參考 Proposal: A new storage row format for efficient decoding)。

  2. 我們可以通過圖五這個例子,來更好的理解一下這條特性。左邊是一個兩列的原表,通過 DDL 操作,我們刪除了 a 列,新增了 c 列,轉換為右邊的 schema 狀態。這時,我們需要用新的 schema 資訊去解析原有的老資料,根據新 schema 中的每個 column id,我們去老資料中找到每個 column id 對應的值,其中 id_2 可以找到對應的值,但 id_3 並沒有找到對應的值,因此,就給 id_3 補上該列的預設值。而對於資料中多個 id_1 對應的值, 就選擇直接捨棄。通過這樣的方式,我們就正確的解析了原來的資料。

圖五 新 schema 解析舊資料樣例

TiKV 中 DDL 模組基本情況

TiKV 這個行存的儲存層,本身是沒有在節點中儲存各個資料表對應的 schema 資訊的,因為 TiKV 本身的讀寫過程都不需要依賴自身提供的 schema 資訊。

  1. TiKV 的寫操作本身是不需要 shcema ,因為寫入 TiKV 的資料是上層已經完成轉換的行存的格式的資料(也就是 kv 中的 v)。
  2. 對於 TiKV 的讀操作
  3. 如果讀操作只需要直接把 kv 讀出,則也不需要 schema 資訊。
  4. 如果是需要在 TiKV 中的 coprocesser 上處理一些 TiDB 下發給 TiKV 承擔的下推計算任務的時候,TiKV 會需要 schema 的資訊。但是這個 schema 資訊,會在 TiDB 傳送來的請求中包含,所以 TiKV 可以是直接拿 TiDB 傳送的請求中的 schema 資訊來進行資料的解析,以及做一些異常處理(如果解析失敗的話)。因此 TiKV 這一類讀操作也不會需要自身提供 schema 相關的資訊。

TiFlash 中 DDL 模組設計思想

TiFlash 中 DDL 模組的設計思想主要包含了以下三點: 1. TiFlash 節點上會儲存自己的 schema copy。一部分是因為 TiFlash 對 schema 具有強依賴性,需要 schema 來幫助解析行轉列的資料以及需要讀取的資料。另一方面也因為 TiFlash 是基於 Clickhouse 實現的,所以很多設計也是在 Clickhouse 原有的設計上進行演進的,Clickhouse 本身設計中就是保持了一份 schema copy。 2. 對於 TiFlash 節點上儲存的 schema copy,我們選擇通過定期從 TiKV 中拉取最新的 schema(本質其實是拿到 TiDB 中最新的 schema 資訊)來進行更新,因為不斷持續地更新 schema 的開銷是非常大的,所以我們是選擇了定期更新。 3. 讀寫操作,會依賴節點上的 schema copy 來進行解析。如果節點上的 schema copy 不滿足當下讀寫的需求,我們會去拉最新的schema資訊,來保證schema 比資料新,這樣就可以正確成功解析了(這個就是前面提到的 TiDB DDL 機制提供的保證)。具體讀寫時對 schema copy 的需求,會在後面的部分具體給大家介紹。

DDL Core Process

本章節中,我們將介紹 TiFlash DDL 模組核心的工作流程。

圖六 DDL Core Process

圖六左邊是各個節點的一個縮略展示,右邊放大顯示了TiFlash 中跟 DDL 相關的核心流程,分別為:

  1. Local Schema Copy 指的是 TiFlash 節點上存的 schema copy 的資訊。
  2. Schema Syncer 模組負責從 TiKV 拉取 最新的 Schema 資訊,依此來更新 Local Schema Copy。
  3. Bootstrap 指的是 TiFlash Server 啟動的時候,會直接呼叫一次 Schema Syncer,獲得目前所有的 schema 資訊。
  4. Background Sync Thread 是負責定期呼叫 Schema Syncer 來更新 Local Schema Copy 模組。
  5. Read 和 Write 兩個模組就是 TiFlash 中的讀寫操作,讀寫操作都會去依賴 Local Schema Copy,也會在有需要的時候來呼叫 Schema Syncer 進行更新。

下面我們就逐一來看每個部分是怎麼實現的。

Local Schema Copy

TiFlash 中 schema 資訊最主要的是跟各個資料表相關的資訊。在 TiFlash 的儲存層中,每一個物理的表,都會對應一個 StorageDeltaMerge 的例項物件,在這個物件中有兩個變數,是負責來儲存跟schema 相關的資訊的。

圖七 Schema Copy 儲存示意圖

  1. tidb_table_info 這個變數存的是 table 中各種 schema 資訊,包括 table id,table name,columns infos,schema version等等。並且 tidb_table_info 的儲存結構跟 TiDB / TiKV 中儲存 table schema 的結構是完全一致的。
  2. decoding_schema_snapshot 則是根據 tidb_table_info 以及 StorageDeltaMerge 中的一些資訊生成的一個物件。decoding_schema_snapshot 是為了優化寫入過程中行轉列的效能而提出的。因為我們在做行轉列轉換的時候,如果依賴 tidb_table_info 獲取對應需要的 schema 資訊,需要做一系列的轉換操作來進行適配。考慮到 schema 本身也不會頻繁更新,為了避免每次行轉列解析都需要重複做這些操作,我們就用 decoding_schema_snapshot 這個變數來儲存轉換好的結果,並且在行轉列過程中依賴 decoding_schema_snapshot 來進行解析。

Schema Syncer

Schema Syncer 這個模組是由 TiDBSchemaSyncer 這個類來負責的。它通過 RPC 去 TiKV 中獲取最新的 schema 的更新內容。對於獲取到的 schema diffs,會找到每個 schema diff 對應的 table,在 table 對應的 StorageDeltaMerge 物件中來更新 schema 資訊以及對應儲存層相關的內容。

Schemas 流程圖

整個過程是通過 TiDBSchemaSyncer 函式 syncSchema來實現的,具體的過程可以參考圖八: 1. 通過 tryLoadSchemaDiffs, TiKV 中拿到這一輪新的 schema 變更資訊。 2. 隨後遍歷所有的 diffs 來一個個進行 applyDiff。 3. 對每個 diff,我們會找到他對應的 table,進行 applyAlterPhysicalTable。 4. 在這其中,我們會 detect 到這輪更新中,跟這個表相關的所有 schema 變更,然後呼叫 StorageDeltaMerge::alterFromTiDB 來對這張表對應的 StorageDeltaMerge 物件進行變更。 5. 具體變更中,我們會修改 tidb_table_info , 相關的 columns 和主鍵的資訊。 6. 另外我們還會更新這張表的建表語句,因為表本身發生了變化,所以他的建表語句也需要對應改變,這樣後續做 recover 等操作的時候才能正確工作。

在整個 syncSchema 的過程中,我們是不會更新 decoding_schema_snapshot的。decoding_schema_snapshot採用的是採用的是懶惰更新的方式,只有在具體的資料要發生寫入操作了,需要呼叫到 decoding_schema_snapshot,它才會去檢測自己目前是不是最新的 schema 對應的狀態,如果不是,就會根據最新的 tidb_table_info 相關的資訊來更新。也是通過這樣的方式,我們可以減少很多不必要的轉換。比如如果一張表頻繁發生了很多 schema change,但是沒有做任何的寫操作, 那麼就可以避免 tidb_table_infodecoding_schema_snapshot 之間的諸多計算轉換操作。

DDL Process

對於周圍涉及到呼叫 Schema Syncer 的模組,Read,Write,BootStrap 這三個模組都是直接的呼叫 TiDBSchemaSyncer::syncSchema。而 Background Sync Thread 則是通過 SchemaSyncService 來負責,在 TiFlash Server 啟動的最開始階段,把 syncSchema 這個函式塞到 background thread pool裡面去,保持大概每隔10s呼叫一次,來實現定期更新。

Schema on Data Write

我們先來了解一下,寫的過程本身需要處理的情況。我們有一個要寫入的行格式的資料,需要把他每一列內容進行解析處理,寫入列存引擎中。另外我們節點中有 local schema copy 來幫助解析。但是,這行要寫入的資料和我們的 schema copy 在時間上的先後順序是不確定的。因為我們的資料是通過 raft log / raft snapshot 的形式傳送過來的,是一個非同步的過程。schema copy 則是定期來進行更新的,也可以看作是一個非同步的過程,所以對應的 schema 版本和 這行寫入的資料 在 TiDB 上發生的先後順序我們是不知道的。寫操作就是要在這樣的場景下,正確的解析資料進行寫入。

圖十 寫入資料

對於這樣的場景,會有個非常直接的處理思路:我們可以在做行轉列解析前,先拉取最新的 schema,從而保證我們的 schema 一定比要寫入的資料更新,這樣一定是可以成功解析的。但是一方面 schema 不是頻繁變更的,另外每次寫都要拉取 schema 是非常大的開銷,所以我們寫操作最終選擇的做法是,我們先直接用現有的 schema copy 來解析這行資料,如果解析成功了就結束,解析失敗了,我們再去拉取最新的 schema 來重新解析

在做第一輪解析時,除了正確解析完成以外,我們還可能遇到以下三種情況: 1. 第一種情況 Unknown Column, 即待寫入的資料比 schema 多了一列 e。發生這種情況的可能有下面兩種可能。

圖十一 unknown column 場景

  • 第一種可能,如圖十一(左)所示,待寫入的資料比 schema 新。在 TiDB 的時間線上,先新增了一列 e,隨後再插入了 (a,b,c,d,e) 這行資料。但是插入的資料先到到了 TiFlash ,add column e 的 schema 變更還沒到 TiFlash 側,所以就出現了資料比 schema 多一列的情況。
  • 第二種可能,如圖十一(右)所示,待寫入的資料比 schema 舊。在 TiDB 的時間線上,先插入了這行資料 (a,b,c,d,e),然後 drop column e。但是 drop column e 的 schema 變更先到達 TiFlash 側, 插入的資料後到達,也會出現了資料比 schema 多一列的情況。 在這種情況下,我們也沒有辦法判斷到底屬於上述是哪一種情況,也沒有一個共用的方法能處理,所以就只能返回解析失敗,去觸發拉取最新的 schema 進行第二輪解析。
  • 第二種情況 Missing Column,即待寫入的資料比 schema 少了一列 e。同樣,也有兩種產生的可能性。

圖十二 missing column 場景

  • 第一種可能,如圖十二(左)所示,待寫入的資料比 schema 新。在 TiDB 時間線上,先 drop column e,再插入資料(a,b,c,d)。
  • 第二種可能,如圖十二(右)所示,待寫入的資料比 schema 舊。 在 TiDB 時間線上,先插入了資料 (a,b,c,d),然後再插入了 e 列。

同樣我們這時候也沒有辦法判斷是屬於哪種情況,按照前面的做法,我們還是應該解析失敗返回重新拉取在解析了。但是在這種情況下,如果多出來的 e 列 是有預設值的或者是支援填 NULL 的,我們可以直接給 e 列填上預設值或者 NULL 來返回解析成功。我們分別看一下在兩種可能性下,我們這種填預設值或者 NULL 的處理會有什麼樣的影響。

第一種可能的情況下,因為我們已經 drop 了 column e,所以後續所有的讀操作都不會讀到 column e 的操作,所以其實給 e 列填任何值,都不會影響正確性。而對於第二種可能的情況,本身 (a,b,c,d) 這行資料就是缺失 e 的值的,需要在讀的時候給這行資料填 e 的預設值 或者 NULL 的,所以在這個情況下,我直接先給這行資料的 column e 填了預設值或者 NULL,也是完全可以正常工作的。所以這兩種情況下,我們給 e 列填預設值或者 NULL 都是可以正確工作的,因此我們就不需要返回解析失敗了。但是如果多出的 e 列並不支援填預設值或者 NULL,那就只能返回解析失敗,去觸發拉取最新的 schema 進行第二輪解析。

  1. 第三種情況 Overflow Column,即我們待寫入的資料中有一列數值大於了我們 schema 中這一列的資料範圍的。

圖十三 overflow column 場景

對於這種情況,只有圖十三(左)這種情況,即先進行了擴列的操作,然後插入了新的資料,但是資料先於 schema 到達了 TiFlash。我們可以看一下圖十三(右)來理解為什麼不可能是先插入資料再觸發縮列的情況。如果我們先插入了資料(a,b,c,d,E),然後對 e 列做了縮列操作,將 e 列從 int 型別縮成 tinyint 型別。而因為插入的這個 E 超過了 tinyint 的範圍,所以這個 DDL 操作會報 overflow 的錯誤的,操作失敗,因此無法導致 overflow column 這種現象。

因此出現 overflow的場景,只可能是圖十三(左)的這種情況。但是因為 schema change 還沒有到達 TiFlash,我們並不知道新的列具體的資料範圍是怎麼樣的,所以沒有辦法把這個 overflow 的值 E 寫入 TiFlash 儲存引擎,所以我們也只能返回解析失敗,去觸發拉取最新的 schema 進行第二輪解析。

瞭解完再第一次解析的時候可能會遇到的三種異常情況,我們再來了解一下在第一次解析失敗下,重新拉取最新的 schema 以後,再進行第二輪解析下會出現的情況。同樣的,除了在第二輪正常的完成解析以外,我們還可能遇到前面的三種情況,但不一樣的是,在第二輪解析時,可以保證我們的 schema 比待寫入的資料更新了

圖十四 第二輪解析異常場景

  1. 第一種情況 Unknown Column。因為 schema 比 待寫入的資料新,所以我們可以肯定是因為在這行資料後,又發生了 drop column e 的操作,但是這個 schema change 先到達了 TiFlash 側,所以導致了 Unknown Column 的場景。因此我們只需要直接把 e 列資料直接刪除即可。
  2. 第二種情況 Missing Column。這種情況則是由於在這行資料後進行了 add column e 的操作造成的,因此我們直接給多餘的列填上預設值即可。
  3. 第三種情況 Overflow Column。因為目前我們的 schema 已經比待寫入的資料新了,所以再次出現 overflow column 的情況,一定是發生了異常,因此我們直接丟擲異常。

以上就是寫資料過程的整體的思路,如果想了解具體的程式碼細節,可以搜尋一下 writeRegionDataToStorage 這個函式。另外我們的行轉列的過程是依賴 RegionBlockReader 這個類來實現的,這個類依賴的 schema 資訊就是我們前面提到的 decoding_schema_snapshot。在行轉列的過程中,RegionBlockReader 在拿 decoding_schema_snapshot 的時候會先檢查 decoding_schema_snapshot 是否跟最新的 tidb_table_info 版本是對齊的,如果沒對齊,就會觸發 decoding_schema_snapshot 的更新,具體邏輯可以參考 getSchemaSnapshotAndBlockForDecoding 這個函式。

Schema on Data Read

和寫不太一樣的是,在開始內部的讀流程之前,我們需要先校驗 schema version。我們上層傳送的請求中,會帶有 schema version 資訊(Query_Version)。讀請求校驗需要滿足的要求則是,待讀的表本地的 schema 資訊和讀請求裡面的 schema version 對應的資訊保持一致的

圖十五 讀資料

TiFlash 負責拉取 schema 的 TiDBSchemaSyncer 會記錄整體的 schema version,我們這邊稱它為Local_Version。因此讀操作的要求即 Query_Version = Local_Version。如果 Query_Version 大於 Local_Version,我們會認為本地 schema 版本落後了,因此觸發 sync schema ,拉取最新的 schema,再重新進行校驗。如果 Query_Version 小於 Local_Version,我們就會認為 query 的 schema 版本太老,因此會拒絕讀請求,讓上層節點更新 schema version 後重新發送請求。

在這種設定下,如果我們有個表在非常頻繁的發生 DDL 操作,那麼他的 schema version 就會不斷更新。因此如果此時又需要對這個表進行讀操作,就很容易出現讀操作一直在 Query_Version > Local_Version 和 Query_Version < Local_Version 兩種狀態下交替來回的狀況。比如一開始讀請求的 schema version 更大,觸發 TiFlash sync schema,更新 local schema copy。更新後本地的 schema version 就比讀請求新,因此觸發拒絕讀請求。讀請求更新 schema version 後,我們又發現讀請求的 schema version 比 本地 schema copy 更新了,週而復始 .... 對於這種情況,我們目前是沒有做特殊處理的。我們會認為這種情況是非常非常罕見的,或者說不會發生的,所以如果不幸發生了這樣的特殊情況,那隻能等待他們達到一個平衡狀態,順利開始讀操作。

前面我們提到讀操作要求 Query_Version 和 Local_Version 完全相等,因此非常容易出現出現不相等的情況,從而造成諸多重新發起查詢或者重新拉取 schema 的情況。為了減少發生此種情況的次數,我們做了一個小的優化。

圖十六 version 關係示意圖

我們除了 TiFlash 整體有 schema version外,每張表也有自己的 schema version,我們稱為 Storage_Version,並且我們的 Storage_Version 永遠小於等於 Local_Version 的, 因為只有在最新的schema 變更的時候,確實修改了這張表,Storage_Version 才會恰好等於 Local_Version, 其他情況下,Storage_Version 都是小於 Local_Version 的。因此在 [Storage_Version, Local_Version] 這個區間中,我們這張表的 schema 資訊是沒有發生任何變化的。也就是 Query_Version只要在[Storage_Version, Local_Version] 這個區間內,讀請求要求的這張表的 schema 資訊和我們目前的 schema 版本就是完全一致的。所以我們就可以把 Query_Version < Local_Version 這個限定放鬆到 Query_Version < Storage_Version。在 Query_Version < Storage_Version 時,才需要更新讀請求的 schema 資訊。

在校驗結束後,負責讀的模組根據我們對應表的 tidb_table_info 去建立 stream 進行讀取。Schema 相關的流程,我們可以在 InterpreterSelectQuery.cppgetAndLockStorageWithSchemaVersion 以及 DAGStorageInterpreter.cppgetAndLockStorages 中進行進一步的瞭解。 InterpreterSelectQuery.cppDAGStorageInterpreter.cpp 都是來負責對 TiFlash 進行讀表的操作,前者是負責 clickhouse client 連線下讀取的流程,後者則是 TiDB 支路中讀取的流程。

Special Case

最後我們看一個例子,來了解一下 Drop Table 和 Recover Table 相關的情況。

圖十七 special case 示意圖一

圖十六中上方的線是 TiDB 的時間線,下方的線是 TiFlash 的時間線。在 t1 的時候,TiDB 進行了 insert 的操作,然後在 t2 的時候又進行了 drop table 的操作。t1' 的時候,TiFlash 收到了 insert 這條操作的 raft log,但是還沒進行到解析和寫入的步驟,然後在 t2' 的時候,TiFlash 同步到了 drop table 這條 schema DDL 操作,進行了 schema 的更新。等到 t2'' 的時候,TiFlash 開始 解析前面那條新插入的資料了,但是這時候因為對應的表已經被刪除了,所以我們就會扔掉這條資料。到目前為止還沒有任何的問題。

圖十八 special case 示意圖二

但是如果 t3 的時候我們又進行了 recover 的操作,將這張表恢復了,那最後插入的這條 row 資料就丟失了。資料丟失是我們不能接受的結果。因此 TiFlash 對於 drop table 這類的 DDL,會對這張表設上 tombstone,具體的物理回收延後到做 gc 操作的時候再發生。對於 drop table 後這張表上還存在的寫操作,我們會繼續進行解析和寫入,這樣在後續做 recover 的時候,我們也不會發生資料的丟失。

小結

本篇文章主要介紹了 TiFlash 中 DDL 模組的設計思想,具體實現和核心的相關流程。更多的程式碼閱讀內容會在後面的章節中逐步展開,敬請期待。