TiDB Online DDL 在 TiCDC 中的應用丨TiDB 工具分享

語言: CN / TW / HK

引言

TiCDC 作為 TiDB 的資料同步元件,負責直接從 TiKV 感知資料變更同步到下游。其中比較核心的問題是資料解析正確性問題,具體而言就是如何使用正確的 schema 解析 TiKV 傳遞過來的 Key-Value 資料,從而還原成正確的 SQL 或者其他下游支援的形式。本文主要通過對 TiDB Online DDL 機制原理和實現的分析,引出對當前 TiCDC 資料解析實現的討論。

背景和問題

資料同步元件是資料庫生態中不可或缺的生態工具,比較知名的開源單機資料庫 MySQL 就將資料同步作為 Server 能力的一部分,並基於 MySQL binlog 實現非同步/半同步/同步的主從複製。由於 MySQL 悲觀事務模型和表元資料鎖的存在,我們總是可以認為 MySQL binlog 中存在因果關係的 data 和 schema 符合時間先後順序的,即:

New data commitTs > New schema commitTs

但是對於 TiDB 這種儲存計算分離的架構而言,schema 的變更在儲存層持久化,服務層節點作為多快取節點,總是存在一個 schema 狀態不一致的時間段。為了保證資料一致性和實現線上 DDL 變更,現有的分散式資料庫大都採用或者借鑑了Online, Asynchronous Schema Change in F1 機制。所以我們要回答的問題變成了,在 TiDB Online DDL 機制下,TiCDC 如何正確處理 data 和 schema 的對應關係,存在因果關係的 data 和 schema 是否仍然滿足:

​ New data commitTs > New schema commitTs

為了回答這個問題,我們首先需要先闡述原始的 F1 Online Schema Change 機制的核心原理,然後描述當前 TiDB Online DDL 實現,最後我們討論在當前 TiCDC 實現下,data 和 schema 的處理關係和可能出現的不同的異常場景。

1. F1 Online Schema Change 機制

F1 Online Schema Change 機制要解決的核心問題是,在單儲存多快取節點的架構下,如何實現滿足資料一致性的 Online Schema 變更,如圖 1 所示:

img

圖 1: 單儲存多快取節點的架構下的 schema 變更

這裡我們定義資料不一致問題為資料多餘(orphan data anomaly)和資料缺失(integrity anomaly),Schema 變更結束後出現數據多餘和資料缺失我們就認為資料不一致了。這類系統的 schema 變更問題特點可以總結成以下 3 點:

  1. 一份 schema 儲存,多份 schema 快取

  2. 部分 new schema 和 old schema 無法共存

  3. 直接從 old schema 變更到 new schema 時,總是存在一個時間區間兩者同時存在

特點 1 和特點 3 是系統架構導致的,比較容易理解。特點 2 的一個典型例子是 add index,載入了 new schema 的服務層節點插入資料時會同時插入索引,而載入了 old schema 的服務層節點執行刪除操作只會刪除資料,導致出現了沒有指向的索引, 出現數據多餘。

Schema 變更問題的特點 2 和特點 3 看起來是互相矛盾的死結,new schema 和 old schema 無法共存,但又必然共存。而 F1 Online Schema 機制提供的解決方案也很巧妙,改變不了結果就改變條件。所以該論文的解決思路上主要有 2 點,如圖 2 所示:

img

圖 2: F1 Online DDL 解決方案

1. 引入共存的中間 schema 狀態,比如 S1->S2’->S2, S1 和 S2’ 可以共存,S2’ 和 S2 可以共存;

2. 引入確定的隔離時間區間,保證無法共存的 schema 不會同時出現;

具體來講:

  • 引入共存的中間 schema 狀態

因為直接從 schema S1 變更到 schema S2 會導致資料不一致的問題,所以引入了 delete-only 和 write-only 中間狀態,從 S1 -> S2 過程變成 S1 -> S2+delete-only -> S2+write-only -> S2 過程,同時使用 lease 機制保證同時最多有 2 個狀態共存。這時只需要證明每相臨的兩個狀態都是可以共存的,保證資料一致性,就能推匯出 S1 到 S2 變更過程中資料是一致的。

  • 引入確定的隔離時間區間

定義 schema lease,超過 lease 時長後節點需要重新載入 schema,載入時超過 lease 之後沒法獲取 new schema 的節點直接下線,不提供服務。所以可以明確定義 2 倍 lease 時間之後,所有節點都會更新到下一個的 schema。

1.1 引入共存的中間狀態

我們需要引入什麼樣的中間狀態呢?那要看我們需要解決什麼問題。這裡我們仍然使用 add index 這個 DDL 作為例子,其他 DDL 細節可以查閱 Online, Asynchronous Schema Change in F1

Delete-only 狀態

我們可以看到 old schema 是無法看到索引資訊的,所以會導致出現刪除資料,遺留沒有指向的索引這種資料多餘的異常場景,所以我們要引入的第一個中間狀態是 delete-only 狀態,賦予 schema 刪除索引的能力。在 delete-only 狀態下,schema 只能在 delete 操作的時候對索引進行刪除,在 insert/select 操作的時候無法操作索引,如圖 3 所示:

img

圖 3: 引入 delete-only 中間狀態

原始論文對於 delete-only 的定義如下:

img

假設我們已經引入了明確的隔離時間區間(下一個小節會細講),能保證同一時刻最多隻出現 2 個 schema 狀態。所以當我們引入 delete-only 狀態之後,需要考慮的場景就變成:

  1. old schema + new schema(delete-only)

  2. new schema(delete-only) + new schema

  3. 對於場景 1,所有的服務層節點要麼處於 old schema 狀態,要麼處於 new schema(delete-only) 狀態。由於 index 只能在 delete 的時候被操作,所以根本沒有 index 生成,就不會出現前面說的遺留沒有指向的索引問題,也不會有資料缺失問題,此時資料是一致的。我們可以說 old schema 和 new schema(delete-only) 是可以共存的。

  4. 對於場景 2,所有的服務層節點要麼處於 new schema(delete-only) 狀態,要麼處於 new schema 狀態。處於 new schema 狀態的節點可以正常插入刪除資料和索引,處於 new schema( delete-only) 狀態的節點只能插入資料,但是可以刪除資料和索引,此時存在部分資料缺少索引問題,資料是不一致的。

引入 delete-only 狀態之後,已經解決了之前提到的索引多餘的問題,但是可以發現,處於 new schema( delete-only) 狀態的節點只能插入資料,導致新插入的資料和存量歷史資料都缺少索引資訊,仍然存在資料缺失的資料不一致問題。

Write-only 狀態

在場景 2 中我們可以看到,對於 add index 這種場景,處於 new schema( delete-only) 狀態節點插入的資料和存量資料都存在索引缺失的問題。而存量資料本身數量是確定且有限的,總可以在有限的時間內根據資料生成索引,但是 new insert 的資料卻可能隨時間不斷增加。為了解決這個資料缺失的問題,我們還需要引入第二個中間狀態 write-only 狀態,賦予 schema insert/delete 索引的能力。處於 write-only 狀態的節點可以 insert/delete/update 索引,但是 select 無法看到索引,如圖 4 所示:

img

圖 4: 引入 write-only 狀態

原始論文中對於 write-only 狀態的定義如下:

img

引入 write-only 狀態之後,上述的場景 2 被切分成了場景 2‘ 和場景 3:

2’: new schema(delete-only) + new schema(write-only)

3: new schema(write-only) + new schema

  • 對於場景 2‘,所有的服務層節點要麼處於 new schema(delete-only) 狀態,要麼處於 new schema(write-only) 。處於 new schema(delete-only) 狀態的服務層節點只能插入資料,但是可以刪除資料和索引,處於 new schema(write-only) 可以正常插入和刪除資料和索引。此時仍然存在索引缺失的問題,但是由於 delete-only 和 write-only 狀態下,索引對於使用者都是不可見的,所以在使用者的視角上,只存在完整的資料,不存在任何索引,所以內部的索引缺失對使用者而言還是滿足資料一致性的。

  • 對於場景 3,所有的服務層節點要麼處於 new schema(write-only) 狀態,要麼處於 new schema。此時 new insert 的資料都能正常維護索引,而存量歷史資料仍然存在缺失索引的問題。但是存量歷史資料是確定且有限的,我們只需要在所有節點過渡到 write-only 之後,進行歷史資料索引補全,再過渡到 new schema 狀態,就可以保證資料和索引都是完整的。此時處於 write-only 狀態的節點只能看到完整的資料,而 new schema 狀態的節點能看到完整的資料和索引,所以對於使用者而言資料都是一致的。

小節總結

通過上面對 delete-only 和 write-only 這兩個中間狀態的表述,我們可以看到,在 F1 Online DDL 流程中,原來的單步 schema 變更被兩個中間狀態分隔開了。每兩個狀態之間都是可以共存的,每次狀態變更都能保證資料一致性,全流程的資料變更也能保證資料一致性。

img

1.2 引入確定的隔離時間區間

為了保證同一時刻最多隻能存在 2 種狀態,需要約定服務層節點載入 schema 的行為:

  1. 所有的服務層節點在 lease 之後都需要重新載入 schema;

  2. 如果在 lease 時間內無法獲取 new schema,則下線拒絕服務;

通過對服務層節點載入行為的約定,我們可以得到一個確定的時間邊界,在 2*lease 的時間週期之後,所有正常工作的服務層節點都能從 schema state1 過渡到 schema state2, 如圖 5 所示:

img

圖 5: 最多 2*lease 時長後所有的節點都能過渡到下一個狀態

1.3 中間狀態可見性

要正確理解原始論文的中間狀態,需要正確理解中間狀態的可見性問題。前面小節為了方便我們一直使用 add index 作為例子,然後表述 delete-only 和 write-only 狀態下索引對於使用者 select 是不可見的,但是 write-only 狀態下,delete/insert 都是可以操作索引的。如果 DDL 換成 add column,那節點處於 write-only 狀態時,使用者 insert 顯式指定新增列可以執行成功嗎?答案是不能。

總得來說,中間狀態的 delete/insert 可見性是內部可見性,具體而言是服務層節點對儲存層節點的可見性,而不是使用者可見性。對於 add column 這個 DDL,服務層節點在 delete-only 和 write-only 狀態下就能看到 new column,但是操作受到不同的限制。對使用者而言,只有到 new schema 狀態下才能看到 new column,才能顯式操作 new column,如圖 6 所示:

img

圖 6: 中間狀態可見性

為了清晰表述可見性,我們舉個例子,如圖 7 所示。原始的表列資訊為 , DDL 操作之後表列資訊為

img

img

圖 7: 中間狀態過渡

  • 小圖 (1) 中,服務層節點已經過渡到了場景 1,部分節點處於 old schema 狀態,部分節點處於 new schema(delete-only) 狀態。此時 c2 對使用者是不可見的,不管是 insert 還是 delete 的顯式指定 c2 都是失敗的。但是儲存層如果存在 [1,xxx] 這樣的資料是可以順利刪除的,只能插入 [7] 這樣的缺失 c2 的行資料。

  • 小圖 (2) 中,服務層節點已經過渡到了場景 2,部分節點處於 new schema(delete-only) 狀態,部分節點處於 new schema(write-only) 狀態,此時 c2 對使用者仍是不可見的,不管是 insert 還是 delete 的顯式指定 c2 都是失敗的。但是處於 write-only 狀態的節點,insert [9] 在內部會被預設值填充成 [9,0] 插入儲存層。處於 delete-only 狀態的節點,delete [9] 會被轉成 delete [9,0]。

  • 小圖 (3) 中,服務層所有節點都過渡到 write-only 之後,c2 對使用者仍是不可見的。此時開始進行資料填充,將歷史資料中缺失 c2 的行進行填充(實現時可能只是在表的列資訊中打上一個標記,取決於具體的實現)。

  • 小圖 (4) 中,開始過渡到場景 3,部分節點處於 new schema(write-only) 狀態,部分節點處於 new schema 狀態。處於 new schema(write-only) 狀態的節點,c2 對使用者仍是不可見的。處於 new schema 狀態的節點,c2 對使用者可見。此時連線在不同服務層節點上的使用者,可以看到不同的的 select 結果,不過底層的資料是完整且一致的。

總結

上面我們通過 3 個小節對 F1 online Schema 機制進行了簡要描述。原來單步 schema 變更被拆解成了多箇中間變更流程,從而保證資料一致性的前提下實現了線上 DDL 變更。

img

對於 add index 或者 add column DDL 是上述的狀態變更,對於 drop index 或者 drop column 則是完全相反的過程。比如 drop column 在 write-only 階段及之後對使用者都不可見了,內部可以正確 insert/delete,可見性和之前的論述完全一樣。

2. TiDB Online DDL 實現

TiDB Online DDL 是基於 F1 Online Schema 實現的,整體流程如圖 8 所示:

img

圖 8 TiDB Online DDL 流程

簡單描述如下:

  • TiDB Server 節點收到 DDL 變更時,將 DDL SQL 包裝成 DDL job 提交到 TIKV job queue 中持久化;

  • TiDB Server 節點選舉出 Owner 角色,從 TiKV job queue 中獲取 DDL job,負責具體執行 DDL 的多階段變更;

  • DDL 的每個中間狀態(delete-only/write-only/write-reorg)都是一次事務提交,持久化到 TiKV job queue 中;

  • Schema 變更成功之後,DDL job state 會變更成 done/sync,表示 new schema 正式被使用者看到,其他 job state 比如 cancelled/rollback done 等表示 schema 變更失敗;

  • Schema state 的變更過程中使用了 etcd 的訂閱通知機制,加快 server 層各節點間 schema state 同步,縮短 2*lease 的變更時間。

  • DDL job 處於 done/sync 狀態之後,表示該 DDL 變更已經結束,移動到 job history queue 中;

詳細的 TiDB 處理流程可以參見:schema-change-implement.mdTiDB ddl.html

3. TiCDC 中 Data 和 Schema 處理關係

前面我們分別描述了 TiDB Online DDL 機制的原理和實現,現在我們可以回到最一開始我們提出的問題:在 TiDB Online DDL 機制下,是否還能滿足:

New data commitTs > New schema commitTs

答案是否定的。在前面 F1 Online Schema 機制的描述中,我們可以看到在 add column DDL 的場景下,當服務層節點處於 write-only 狀態時,節點已經能夠插入 new column data 了,但是此時 new column 還沒有處於使用者可見的狀態,也就是出現了 New data commitTs < New schema commitTs,或者說上述結論變成了:

New data commitTs > New schema(write-only) commitTs

但是由於在 delete-only + write-only 過渡狀態下,TiCDC 直接使用 New schema(write-only) 作為解析的 schema,可能導致 delete-only 節點 insert 的資料無法找到對應的 column 元資訊或者元資訊型別不匹配,導致資料丟失。所以為了保證資料正確解析,可能需要根據不同的 DDL 型別和具體的 TiDB 內部實現,在內部維護複雜的 schema 策略。

在當前 TiCDC 實現中,選擇了比較簡單的 schema 策略,直接忽略了各個中間狀態,只使用變更完成之後的 schema 狀態。為了更好表述在 TIDB Online DDL 機制下,當前 TiCDC 需要處理的不同場景,我們使用象限圖進行進一步歸類描述。

| | Old schema | New schema | | ------------------- | -------------- | -------------- | | Old schema data | 1 | 2 | | New schema data | 3 | 4 |

  • 1 對應 old schema 狀態

此時 old schema data 和 old schema 是對應的*;*

  • 4 對應 new schema public 及之後

此時 new schema data 和 new schema 是對應的;

  • 3 對應 write-only ~ public 之間資料

此時 TiCDC 使用 old schema 解析資料,但是處於 write-only 狀態的 TiDB 節點已經可以基於 new schema insert/update/delete 部分資料,所以 TiCDC 會收到 new schema data。不同 DDL 處理效果不同,我們選取 3 個常見有代表性的 DDL 舉例。

  • add column: 狀態變更 absent -> delete-only -> write-only -> write-reorg -> public。由於 new schema data 是 TiDB 節點在 write-only 狀態下填充的預設值,所以使用 old schema 解析後會被直接丟棄,下游執行 new schema DDL 的時候會再次填充預設值。對於動態生成的資料型別,比如 auto_increment 和 current timestamp,可能會導致上下游資料不一致。

  • change column:有損狀態變更 absent -> delete-only -> write-only -> write-reorg -> public, 比如 int 轉 double,編碼方式不同需要資料重做。在 TiDB 實現中,有損 modify column 會生成不可見 new column,中間狀態下會同時變更新舊 column。對於 TiCDC 而言,只會處理 old column 下發,然後在下游執行 change column,這個和 TiDB 的處理邏輯保持一致。

  • drop column:狀態變更 absent-> write-only -> delete-only -> delete-reorg -> public。write-only 狀態下新插入的資料已經沒有了對應的 column,TiCDC 會填充預設值然後下發到下游,下游執行 drop column 之後會丟棄掉該列。使用者可能看到預期外的預設值,但是資料能滿足最終一致性。

  • 2 對應直接從 old schema -> new schema

說明這類 schema 變更下,old schema 和 new schema 是可以共存的,不需要中間狀態,比如 truncate table DDL。TiDB 執行 truncate table 成功後,服務層節點可能還沒有載入 new schema,還可以往表中插入資料,這些資料會被 TiCDC 直接根據 tableid 過濾掉,最終上下游都是沒有這個表存在的,滿足最終一致性。

總結

TiCDC 作為 TiDB 的資料同步元件,資料解析正確性問題是保證上下游資料一致性的核心問題。為了能充分理解 TiCDC 處理 data 和 schema 過程中遇到的各種異常場景,本文首先從 F1 Online Schema Change 原理出發,詳細描述在 schema 變更各個階段的資料行為,然後簡單描述了當前 TiDB Online DDL 的實現。最後引出在當前 TiCDC 實現下在 data 和 schema 處理關係上的討論。