TiDB 源碼閱讀系列文章(十七)DDL 源碼解析

DDL 是數據庫非常核心的組件,其正確性和穩定性是整個 SQL 引擎的基石,在分佈式數據庫中,如何在保證數據一致性的前提下實現無鎖的 DDL 操作是一件有挑戰的事情。本文首先會介紹 TiDB DDL 組件的總體設計,介紹如何在分佈式場景下支持無鎖 shema 變更,描述這套算法的大致流程,然後詳細介紹一些常見的 DDL 語句的源碼實現,包括 create tableadd indexdrop columndrop 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 中的簡單處理流程:

TiDB 中 DDL SQL 的處理流程

<center>圖 1:TiDB 中 DDL SQL 的處理流程</center>

TiDB 的 DDL 組件相關代碼存放在源碼目錄的 ddl 目錄下。

表.png

ddl owner 相關的代碼單獨放在 owner 目錄下,實現了 owner 選舉等功能。

另外,ddl job queuehistory ddl job queue 這兩個隊列都是持久化到 TiKV 中的。structure 目錄下有 list,hash 等數據結構在 TiKV 上的實現。

本文接下來按照 TiDB 源碼的 origin/source-code 分支講解,最新的 master 分支和 source-code 分支代碼會稍有一些差異。

Create table

create table 需要把 table 的元信息(TableInfo)從 SQL 中解析出來,做一些檢查,然後把 table 的元信息持久化保存到 TiKV 中。具體流程如下:

  1. 語法解析:ParseSQL 解析成抽象語法樹 CreateTableStmt
  2. 編譯生成 Plan:Compile 生成 DDL plan , 並 check 權限等。
  3. 生成執行器:buildExecutor 生成  DDLExec 執行器。TiDB 的執行器是火山模型。
  4. 執行器調用 e.Next 開始執行,即 DDLExec.Next 方法,判斷 DDL 類型後執行 executeCreateTable , 其實質是調用 ddl_api.goCreateTable 函數。
  5. CreateTable 方法是主要流程如下:

    • 會先 check 一些限制,比如 table name 是否已經存在,table 名是否太長,是否有重複定義的列等等限制。
    • buildTableInfo 獲取 global table ID,生成 tableInfo , 即 table 的元信息,然後封裝成一個 DDL job,這個 job 包含了 table IDtableInfo,並將這個 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 映射爲 keytableInfo 作爲 value 存到 TiKV 裏面去,並更新 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 的主要工作,具體流程如下:

  1. Check 一些限制,比如 table 是否存在,索引是否已經存在,索引名是否太長等。
  2. 封裝成一個 job,包含了索引名,索引列等,並將 job 的 type 標記爲 ActionAddIndex
  3. 給 job 獲取一個 global job ID 然後放到 DDL job 隊列中去。
  4. 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 都做完。
  5. 後續執行 finishDDLJob,檢測 history ddl job 流程和 create table 類似。

Drop Column

drop Column 只要修改 table 的元信息,把 table 元信息中對應的要刪除的 column 刪除。drop Column 不會刪除原有 table 數據行中的對應的 Column 數據,在 decode 一行數據時,會根據 table 的元信息來 decode。

具體執行流程的前部分都類似,直接跳到 DropColumn 函數開始,具體執行流程如下:

  1. Check table 是否存在,要 drop 的 column 是否存在等。
  2. 封裝成一個 job, 將 job 類型標記爲 ActionDropColumn,然後放到 DDL job 隊列中去
  3. owner ddl worker 從 DDL job 隊列中取出 job,根據 job 的類型調用 onDropColumn 函數:

    • 這裏 column info 的狀態變化和 add index 時的變化幾乎相反:public -> write only -> delete only -> reorganization -> absent
    • updateVersionAndTableInfo 更新 table 元信息中的 Columns。
  4. 後續執行 finishDDLJob,檢測 history ddl job 流程和 create table 類似。

Drop table

drop table 需要刪除 table 的元信息和 table 中的數據。

具體執行流程的前部分都類似,owner ddl worker 從 DDL job 隊列中取出 job 後執行 onDropTable 函數:

  1. tableInfo 的狀態變化是:public -> write only -> delete only -> none
  2. 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。

圖 2:並行 DDL 處理流程

<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 類似。

作者:陳霜
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章