DDL 是數據庫非常核心的組件,其正確性和穩定性是整個 SQL 引擎的基石,在分佈式數據庫中,如何在保證數據一致性的前提下實現無鎖的 DDL 操作是一件有挑戰的事情。本文首先會介紹 TiDB DDL 組件的總體設計,介紹如何在分佈式場景下支持無鎖 shema 變更,描述這套算法的大致流程,然後詳細介紹一些常見的 DDL 語句的源碼實現,包括 create table
、add index
、drop column
、drop table
這四種。
DDL in TiDB
TiDB 的 DDL 通過實現 Google F1 的在線異步 schema 變更算法,來完成在分佈式場景下的無鎖,在線 schema 變更。爲了簡化設計,TiDB 在同一時刻,只允許一個節點執行 DDL 操作。用戶可以把多個 DDL 請求發給任何 TiDB 節點,但是所有的 DDL 請求在 TiDB 內部是由 owner 節點的 worker 串行執行的。
- worker:每個節點都有一個 worker 用來處理 DDL 操作。
- owner:整個集羣中只有一個節點能當選 owner,每個節點都可能當選這個角色。當選 owner 後的節點 worker 纔有處理 DDL 操作的權利。owner 節點的產生是用 Etcd 的選舉功能從多個 TiDB 節點選舉出 owner 節點。owner 是有任期的,owner 會主動維護自己的任期,即續約。當 owner 節點宕機後,其他節點可以通過 Etcd 感知到並且選舉出新的 owner。
這裏只是簡單概述了 TiDB 的 DDL 設計,下兩篇文章詳細介紹了 TiDB DDL 的設計實現以及優化,推薦閱讀:
下圖描述了一個 DDL 請求在 TiDB 中的簡單處理流程:
<center>圖 1:TiDB 中 DDL SQL 的處理流程</center>
TiDB 的 DDL 組件相關代碼存放在源碼目錄的 ddl
目錄下。
ddl owner
相關的代碼單獨放在 owner
目錄下,實現了 owner 選舉等功能。
另外,ddl job queue
和 history ddl job queue
這兩個隊列都是持久化到 TiKV 中的。structure
目錄下有 list,hash
等數據結構在 TiKV 上的實現。
本文接下來按照 TiDB 源碼的 origin/source-code 分支講解,最新的 master 分支和 source-code 分支代碼會稍有一些差異。
Create table
create table
需要把 table 的元信息(TableInfo)從 SQL 中解析出來,做一些檢查,然後把 table 的元信息持久化保存到 TiKV 中。具體流程如下:
- 語法解析:ParseSQL 解析成抽象語法樹 CreateTableStmt。
- 編譯生成 Plan:Compile 生成 DDL plan , 並 check 權限等。
- 生成執行器:buildExecutor 生成 DDLExec 執行器。TiDB 的執行器是火山模型。
- 執行器調用 e.Next 開始執行,即 DDLExec.Next 方法,判斷 DDL 類型後執行 executeCreateTable , 其實質是調用
ddl_api.go
的 CreateTable 函數。 -
CreateTable 方法是主要流程如下:
- 會先 check 一些限制,比如 table name 是否已經存在,table 名是否太長,是否有重複定義的列等等限制。
-
buildTableInfo 獲取 global table ID,生成
tableInfo
, 即 table 的元信息,然後封裝成一個 DDL job,這個 job 包含了table ID
和tableInfo
,並將這個 job 的 type 標記爲ActionCreateTable
。 - d.doDDLJob(ctx, job) 函數中的 d.addDDLJob(ctx, job) 會先給 job 獲取一個 global job ID 然後放到 job queue 中去。
-
DDL 組件啓動後,在 start 函數中會啓動一個
ddl_worker
協程運行 onDDLWorker 函數(最新 Master 分支函數名已重命名爲 start),每隔一段時間調用 handleDDLJobQueu 函數去嘗試處理 DDL job 隊列裏的 job,ddl_worker
會先 check 自己是不是 owner,如果不是 owner,就什麼也不做,然後返回;如果是 owner,就調用 getFirstDDLJob 函數獲取 DDL 隊列中的第一個 job,然後調 runDDLJob 函數執行 job。-
runDDLJob 函數裏面會根據 job 的類型,然後調用對應的執行函數,對於
create table
類型的 job,會調用 onCreateTable 函數,然後做一些 check 後,會調用 t.CreateTable 函數,將db_ID
和table_ID
映射爲key
,tableInfo
作爲 value 存到 TiKV 裏面去,並更新 job 的狀態。
-
runDDLJob 函數裏面會根據 job 的類型,然後調用對應的執行函數,對於
- finishDDLJob 函數將 job 從 DDL job 隊列中移除,然後加入 history ddl job 隊列中去。
- doDDLJob 函數中檢測到 history DDL job 隊列中有對應的 job 後,返回。
Add index
add index
主要做 2 件事:
- 修改 table 的元信息,把
indexInfo
加入到 table 的元信息中去。 - 把 table 中已有了的數據行,把
index columns
的值全部回填到index record
中去。
具體執行流程的前部分的 SQL 解析、Compile 等流程,和 create table
一樣,可以直接從 DDLExec.Next 開始看,然後調用 alter
語句的 e.executeAlterTable(x) 函數,其實質調 ddl 的 AlterTable 函數,然後調用 CreateIndex 函數,開始執行 add index 的主要工作,具體流程如下:
- Check 一些限制,比如 table 是否存在,索引是否已經存在,索引名是否太長等。
- 封裝成一個 job,包含了索引名,索引列等,並將 job 的 type 標記爲
ActionAddIndex
。 - 給 job 獲取一個 global job ID 然後放到 DDL job 隊列中去。
-
owner ddl worker
從 DDL job 隊列中取出 job,根據 job 的類型調用 onCreateIndex 函數。-
buildIndexInfo
生成indexInfo
,然後更新tableInfo
中的Indices
,持久化到 TiKV 中去。 -
這裏引入了 online schema change 的幾個步驟,需要留意 indexInfo 的狀態變化:
none -> delete only -> write only -> reorganization -> public
。在reorganization -> public
時,首先調用 getReorgInfo 獲取reorgInfo
,主要包含需要reorganization
的 range,即從表的第一行一直到最後一行數據都需要回填到index record
中。然後調用 runReorgJob , addTableIndex 函數開始填充數據到index record
中去。runReorgJob 函數會定期保存回填數據的進度到 TiKV。addTableIndex 的流程如下:- 啓動多個
worker
用於併發回填數據到index record
。 - 把
reorgInfo
中需要reorganization
分裂成多個 range。掃描的默認範圍是[startHandle , endHandle]
,然後默認以 128 爲間隔分裂成多個 range,之後並行掃描對應數據行。在 master 分支中,range 範圍信息是從 PD 中獲取。 - 把 range 包裝成多個 task,發給
worker
並行回填index record
。 - 等待所有
worker
完成後,更新reorg
進度,然後持續第 3 步直到所有的 task 都做完。
- 啓動多個
-
- 後續執行 finishDDLJob,檢測 history ddl job 流程和
create table
類似。
Drop Column
drop Column
只要修改 table 的元信息,把 table 元信息中對應的要刪除的 column 刪除。drop Column
不會刪除原有 table 數據行中的對應的 Column 數據,在 decode 一行數據時,會根據 table 的元信息來 decode。
具體執行流程的前部分都類似,直接跳到 DropColumn 函數開始,具體執行流程如下:
- Check table 是否存在,要 drop 的 column 是否存在等。
- 封裝成一個 job, 將 job 類型標記爲
ActionDropColumn
,然後放到 DDL job 隊列中去 -
owner ddl worker
從 DDL job 隊列中取出 job,根據 job 的類型調用 onDropColumn 函數:- 這裏
column info
的狀態變化和add index
時的變化幾乎相反:public -> write only -> delete only -> reorganization -> absent
。 - updateVersionAndTableInfo 更新 table 元信息中的 Columns。
- 這裏
- 後續執行 finishDDLJob,檢測 history ddl job 流程和
create table
類似。
Drop table
drop table
需要刪除 table 的元信息和 table 中的數據。
具體執行流程的前部分都類似,owner ddl worker
從 DDL job 隊列中取出 job 後執行 onDropTable 函數:
-
tableInfo
的狀態變化是:public -> write only -> delete only -> none
。 -
tableInfo
的狀態變爲none
之後,會調用 DropTable 將 table 的元信息從 TiKV 上刪除。
至於刪除 table 中的數據,後面在調用 finishDDLJob 函數將 job 從 job queue 中移除,加入 history ddl job queue 前,會調用 delRangeManager.addDelRangeJob(job),將要刪除的 table 數據範圍插入到表 gc_delete_range
中,然後由 GC worker 根據 gc_delete_range
中的信息在 GC 過程中做真正的刪除數據操作。
New Parallel DDL
目前 TiDB 最新的 Master 分支的 DDL 引入了並行 DDL,用來加速多個 DDL 語句的執行速度。因爲串行執行 DDL 時,add index
操作需要把 table 中已有的數據回填到 index record
中,如果 table 中的數據較多,回填數據的耗時較長,就會阻塞後面 DDL 的操作。目前並行 DDL 的設計是將 add index job
放到新增的 add index job queue
中去,其它類型的 DDL job 還是放在原來的 job queue。相應的,也增加一個 add index worker
來處理 add index job queue
中的 job。
<center>圖 2:並行 DDL 處理流程</center>
並行 DDL 同時也引入了 job 依賴的問題。job 依賴是指同一 table 的 DDL job,job ID 小的需要先執行。因爲對於同一個 table 的 DDL 操作必須是順序執行的。比如說,add column a
,然後 add index on column a
, 如果 add index
先執行,而 add column
的 DDL 假設還在排隊未執行,這時 add index on column a
就會報錯說找不到 column a
。所以當 add index job queue
中的 job2 執行前,需要檢測 job queue 是否有同一 table 的 job1 還未執行,通過對比 job 的 job ID 大小來判斷。執行 job queue 中的 job 時也需要檢查 add index job queue
中是否有依賴的 job 還未執行。
End
TiDB 目前一共支持 十多種 DDL,具體以及和 MySQL 兼容性對比可以看 這裏。剩餘其它類型的 DDL 源碼實現讀者可以自行閱讀,流程和上述幾種 DDL 類似。
作者:陳霜