TiDB 應用 | TiDB Online DDL 在 TiCDC 中的應用
TiCDC 作為 TiDB 的數(shù)據(jù)同步組件,負責直接從 TiKV 獲取數(shù)據(jù)變更信息并同步到下游。其中比較核心的問題是數(shù)據(jù)解析正確性問題,具體而言就是如何使用正確的 schema 解析 TiKV 傳遞過來的 Key-Value 數(shù)據(jù),從而還原成正確的 SQL 或者其他下游支持的形式。本文主要通過對 TiDB Online DDL 機制原理和實現(xiàn)的分析,引出對當前 TiCDC 數(shù)據(jù)解析實現(xiàn)的討論。
背景和問題
F1 Online Schema Change 機制

這里我們定義數(shù)據(jù)不一致問題為數(shù)據(jù)多余(orphan data anomaly)和數(shù)據(jù)缺失(integrity anomaly),Schema 變更結(jié)束后出現(xiàn)數(shù)據(jù)多余和數(shù)據(jù)缺失我們就認為數(shù)據(jù)不一致了。這類系統(tǒng)的 schema 變更問題特點可以總結(jié)成以下 3 點:

2. 引入確定的隔離時間區(qū)間,保證無法共存的 schema 不會同時出現(xiàn);
具體來講:
引入共存的中間 schema 狀態(tài)
因為直接從 schema S1 變更到 schema S2 會導致數(shù)據(jù)不一致的問題,所以引入了 delete-only 和 write-only 中間狀態(tài),從 S1 -> S2 過程變成 S1 -> S2+delete-only -> S2+write-only -> S2 過程,同時使用 lease 機制保證同時最多有 2 個狀態(tài)共存。這時只需要證明每相臨的兩個狀態(tài)都是可以共存的,保證數(shù)據(jù)一致性,就能推導出 S1 到 S2 變更過程中數(shù)據(jù)是一致的。
引入確定的隔離時間區(qū)間
定義 schema lease,超過 lease 時長后節(jié)點需要重新加載 schema,加載時超過 lease 之后沒法獲取 new schema 的節(jié)點直接下線,不提供服務。所以可以明確定義 2 倍 lease 時間之后,所有節(jié)點都會更新到下一個的 schema。
引入共存的中間狀態(tài)
Delete-only 狀態(tài)


假設我們已經(jīng)引入了明確的隔離時間區(qū)間(下一個小節(jié)會細講),能保證同一時刻最多只出現(xiàn) 2 個 schema 狀態(tài)。所以當我們引入 delete-only 狀態(tài)之后,需要考慮的場景就變成:
1. old schema + new schema(delete-only)
2. new schema(delete-only) + new schema
對于場景 1,所有的服務層節(jié)點要么處于 old schema 狀態(tài),要么處于 new schema(delete-only) 狀態(tài)。由于 index 只能在 delete 的時候被操作,所以根本沒有 index 生成,就不會出現(xiàn)前面說的遺留沒有指向的索引問題,也不會有數(shù)據(jù)缺失問題,此時數(shù)據(jù)是一致的。我們可以說 old schema 和 new schema(delete-only) 是可以共存的。
對于場景 2,所有的服務層節(jié)點要么處于 new schema(delete-only) 狀態(tài),要么處于 new schema 狀態(tài)。處于 new schema 狀態(tài)的節(jié)點可以正常插入刪除數(shù)據(jù)和索引,處于 new schema( delete-only) 狀態(tài)的節(jié)點只能插入數(shù)據(jù),但是可以刪除數(shù)據(jù)和索引,此時存在部分數(shù)據(jù)缺少索引問題,數(shù)據(jù)是不一致的。
引入 delete-only 狀態(tài)之后,已經(jīng)解決了之前提到的索引多余的問題,但是可以發(fā)現(xiàn),處于 new schema( delete-only) 狀態(tài)的節(jié)點只能插入數(shù)據(jù),導致新插入的數(shù)據(jù)和存量歷史數(shù)據(jù)都缺少索引信息,仍然存在數(shù)據(jù)缺失的數(shù)據(jù)不一致問題。
Write-only 狀態(tài)


對于場景 2',所有的服務層節(jié)點要么處于 new schema(delete-only) 狀態(tài),要么處于 new schema(write-only) 。處于 new schema(delete-only) 狀態(tài)的服務層節(jié)點只能插入數(shù)據(jù),但是可以刪除數(shù)據(jù)和索引,處于 new schema(write-only) 可以正常插入和刪除數(shù)據(jù)和索引。此時仍然存在索引缺失的問題,但是由于 delete-only 和 write-only 狀態(tài)下,索引對于用戶都是不可見的,所以在用戶的視角上,只存在完整的數(shù)據(jù),不存在任何索引,所以內(nèi)部的索引缺失對用戶而言還是滿足數(shù)據(jù)一致性的。
對于場景 3,所有的服務層節(jié)點要么處于 new schema(write-only) 狀態(tài),要么處于 new schema。此時 new insert 的數(shù)據(jù)都能正常維護索引,而存量歷史數(shù)據(jù)仍然存在缺失索引的問題。但是存量歷史數(shù)據(jù)是確定且有限的,我們只需要在所有節(jié)點過渡到 write-only 之后,進行歷史數(shù)據(jù)索引補全,再過渡到 new schema 狀態(tài),就可以保證數(shù)據(jù)和索引都是完整的。此時處于 write-only 狀態(tài)的節(jié)點只能看到完整的數(shù)據(jù),而 new schema 狀態(tài)的節(jié)點能看到完整的數(shù)據(jù)和索引,所以對于用戶而言數(shù)據(jù)都是一致的。
小節(jié)總結(jié)

引入確定的隔離時間區(qū)間

中間狀態(tài)可見性



小圖 (1) 中,服務層節(jié)點已經(jīng)過渡到了場景 1,部分節(jié)點處于 old schema 狀態(tài),部分節(jié)點處于 new schema(delete-only) 狀態(tài)。此時 c2 對用戶是不可見的,不管是 insert < c1,c2> 還是 delete
的顯式指定 c2 都是失敗的。但是存儲層如果存在 [1,xxx] 這樣的數(shù)據(jù)是可以順利刪除的,只能插入 [7] 這樣的缺失 c2 的行數(shù)據(jù)。 小圖 (2) 中,服務層節(jié)點已經(jīng)過渡到了場景 2,部分節(jié)點處于 new schema(delete-only) 狀態(tài),部分節(jié)點處于 new schema(write-only) 狀態(tài),此時 c2 對用戶仍是不可見的,不管是 insert <c1,c2> 還是 delete 的顯式指定 c2 都是失敗的。但是處于 write-only 狀態(tài)的節(jié)點,insert [9] 在內(nèi)部會被默認值填充成 [9,0] 插入存儲層。處于 delete-only 狀態(tài)的節(jié)點,delete [9] 會被轉(zhuǎn)成 delete [9,0]。
小圖 (3) 中,服務層所有節(jié)點都過渡到 write-only 之后,c2 對用戶仍是不可見的。此時開始進行數(shù)據(jù)填充,將歷史數(shù)據(jù)中缺失 c2 的行進行填充(實現(xiàn)時可能只是在表的列信息中打上一個標記,取決于具體的實現(xiàn))。 小圖 (4) 中,開始過渡到場景 3,部分節(jié)點處于 new schema(write-only) 狀態(tài),部分節(jié)點處于 new schema 狀態(tài)。處于 new schema(write-only) 狀態(tài)的節(jié)點,c2 對用戶仍是不可見的。處于 new schema 狀態(tài)的節(jié)點,c2 對用戶可見。此時連接在不同服務層節(jié)點上的用戶,可以看到不同的的 select 結(jié)果,不過底層的數(shù)據(jù)是完整且一致的。
總結(jié)

TiDB Online DDL 實現(xiàn)

TiDB Server 節(jié)點收到 DDL 變更時,將 DDL SQL 包裝成 DDL job 提交到 TIKV job queue 中持久化; TiDB Server 節(jié)點選舉出 Owner 角色,從 TiKV job queue 中獲取 DDL job,負責具體執(zhí)行 DDL 的多階段變更; DDL 的每個中間狀態(tài)(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 層各節(jié)點間 schema state 同步,縮短 2*lease 的變更時間; DDL job 處于 done/sync 狀態(tài)之后,表示該 DDL 變更已經(jīng)結(jié)束,移動到 job history queue 中;
TiCDC 中 Data 和 Schema 處理關系

1 對應 old schema 狀態(tài) 此時 old schema data 和 old schema 是對應的*;* 4 對應 new schema public 及之后 此時 new schema data 和 new schema 是對應的; 3 對應 write-only ~ public 之間數(shù)據(jù)
add column:狀態(tài)變更 absent -> delete-only -> write-only -> write-reorg -> public。由于 new schema data 是 TiDB 節(jié)點在 write-only 狀態(tài)下填充的默認值,所以使用 old schema 解析后會被直接丟棄,下游執(zhí)行 new schema DDL 的時候會再次填充默認值。對于動態(tài)生成的數(shù)據(jù)類型,比如 auto_increment 和 current timestamp,可能會導致上下游數(shù)據(jù)不一致。 change column:有損狀態(tài)變更 absent -> delete-only -> write-only -> write-reorg -> public, 比如 int 轉(zhuǎn) double,編碼方式不同需要數(shù)據(jù)重做。在 TiDB 實現(xiàn)中,有損 modify column 會生成不可見 new column,中間狀態(tài)下會同時變更新舊 column。對于 TiCDC 而言,只會處理 old column 下發(fā),然后在下游執(zhí)行 change column,這個和 TiDB 的處理邏輯保持一致。 drop column:狀態(tài)變更 absent-> write-only -> delete-only -> delete-reorg -> public。write-only 狀態(tài)下新插入的數(shù)據(jù)已經(jīng)沒有了對應的 column,TiCDC 會填充默認值然后下發(fā)到下游,下游執(zhí)行 drop column 之后會丟棄掉該列。用戶可能看到預期外的默認值,但是數(shù)據(jù)能滿足最終一致性。 2 對應直接從 old schema -> new schema
說明這類 schema 變更下,old schema 和 new schema 是可以共存的,不需要中間狀態(tài),比如 truncate table DDL。TiDB 執(zhí)行 truncate table 成功后,服務層節(jié)點可能還沒有加載 new schema,還可以往表中插入數(shù)據(jù),這些數(shù)據(jù)會被 TiCDC 直接根據(jù) tableid 過濾掉,最終上下游都是沒有這個表存在的,滿足最終一致性。
總結(jié)

