摘要:需要把的元信息從中解析出來,做一些檢查,然后把的元信息持久化保存到中。主要做件事修改的元信息,把加入到的元信息中去。從隊(duì)列中取出,根據(jù)的類型調(diào)用函數(shù)。的狀態(tài)變?yōu)橹?,?huì)調(diào)用將的元信息從上刪除。
DDL 是數(shù)據(jù)庫非常核心的組件,其正確性和穩(wěn)定性是整個(gè) SQL 引擎的基石,在分布式數(shù)據(jù)庫中,如何在保證數(shù)據(jù)一致性的前提下實(shí)現(xiàn)無鎖的 DDL 操作是一件有挑戰(zhàn)的事情。本文首先會(huì)介紹 TiDB DDL 組件的總體設(shè)計(jì),介紹如何在分布式場景下支持無鎖 shema 變更,描述這套算法的大致流程,然后詳細(xì)介紹一些常見的 DDL 語句的源碼實(shí)現(xiàn),包括 create table、add index、drop column、drop table 這四種。
DDL in TiDBTiDB 的 DDL 通過實(shí)現(xiàn) Google F1 的在線異步 schema 變更算法,來完成在分布式場景下的無鎖,在線 schema 變更。為了簡化設(shè)計(jì),TiDB 在同一時(shí)刻,只允許一個(gè)節(jié)點(diǎn)執(zhí)行 DDL 操作。用戶可以把多個(gè) DDL 請求發(fā)給任何 TiDB 節(jié)點(diǎn),但是所有的 DDL 請求在 TiDB 內(nèi)部是由 owner 節(jié)點(diǎn)的 worker 串行執(zhí)行的。
worker:每個(gè)節(jié)點(diǎn)都有一個(gè) worker 用來處理 DDL 操作。
owner:整個(gè)集群中只有一個(gè)節(jié)點(diǎn)能當(dāng)選 owner,每個(gè)節(jié)點(diǎn)都可能當(dāng)選這個(gè)角色。當(dāng)選 owner 后的節(jié)點(diǎn) worker 才有處理 DDL 操作的權(quán)利。owner 節(jié)點(diǎn)的產(chǎn)生是用 Etcd 的選舉功能從多個(gè) TiDB 節(jié)點(diǎn)選舉出 owner 節(jié)點(diǎn)。owner 是有任期的,owner 會(huì)主動(dòng)維護(hù)自己的任期,即續(xù)約。當(dāng) owner 節(jié)點(diǎn)宕機(jī)后,其他節(jié)點(diǎn)可以通過 Etcd 感知到并且選舉出新的 owner。
這里只是簡單概述了 TiDB 的 DDL 設(shè)計(jì),下兩篇文章詳細(xì)介紹了 TiDB DDL 的設(shè)計(jì)實(shí)現(xiàn)以及優(yōu)化,推薦閱讀:
TiDB 的異步 schema 變更實(shí)現(xiàn) ?
TiDB 的異步 schema 變更優(yōu)化
下圖描述了一個(gè) DDL 請求在 TiDB 中的簡單處理流程:
TiDB 的 DDL 組件相關(guān)代碼存放在源碼目錄的 ddl 目錄下。
ddl owner 相關(guān)的代碼多帶帶放在 owner 目錄下,實(shí)現(xiàn)了 owner 選舉等功能。
另外,ddl job queue 和 history ddl job queue 這兩個(gè)隊(duì)列都是持久化到 TiKV 中的。structure 目錄下有 list,hash 等數(shù)據(jù)結(jié)構(gòu)在 TiKV 上的實(shí)現(xiàn)。
本文接下來按照 TiDB 源碼的 origin/source-code 分支講解,最新的 master 分支和 source-code 分支代碼會(huì)稍有一些差異。
Create tablecreate table 需要把 table 的元信息(TableInfo)從 SQL 中解析出來,做一些檢查,然后把 table 的元信息持久化保存到 TiKV 中。具體流程如下:
語法解析:ParseSQL 解析成抽象語法樹 CreateTableStmt。
編譯生成 Plan:Compile 生成 DDL plan , 并 check 權(quán)限等。
生成執(zhí)行器:buildExecutor 生成 ?DDLExec 執(zhí)行器。TiDB 的執(zhí)行器是火山模型。
執(zhí)行器調(diào)用 e.Next 開始執(zhí)行,即 DDLExec.Next 方法,判斷 DDL 類型后執(zhí)行 executeCreateTable , 其實(shí)質(zhì)是調(diào)用 ddl_api.go 的 CreateTable 函數(shù)。
CreateTable 方法是主要流程如下:
會(huì)先 check 一些限制,比如 table name 是否已經(jīng)存在,table 名是否太長,是否有重復(fù)定義的列等等限制。
buildTableInfo 獲取 global table ID,生成 tableInfo , 即 table 的元信息,然后封裝成一個(gè) DDL job,這個(gè) job 包含了 table ID 和 tableInfo,并將這個(gè) job 的 type 標(biāo)記為 ActionCreateTable。
d.doDDLJob(ctx, job) 函數(shù)中的 d.addDDLJob(ctx, job) 會(huì)先給 job 獲取一個(gè) global job ID 然后放到 job queue 中去。
DDL 組件啟動(dòng)后,在 start 函數(shù)中會(huì)啟動(dòng)一個(gè) ddl_worker 協(xié)程運(yùn)行 onDDLWorker 函數(shù)(最新 Master 分支函數(shù)名已重命名為 start),每隔一段時(shí)間調(diào)用 handleDDLJobQueu 函數(shù)去嘗試處理 DDL job 隊(duì)列里的 job,ddl_worker 會(huì)先 check 自己是不是 owner,如果不是 owner,就什么也不做,然后返回;如果是 owner,就調(diào)用 getFirstDDLJob 函數(shù)獲取 DDL 隊(duì)列中的第一個(gè) job,然后調(diào) runDDLJob 函數(shù)執(zhí)行 job。
runDDLJob 函數(shù)里面會(huì)根據(jù) job 的類型,然后調(diào)用對應(yīng)的執(zhí)行函數(shù),對于 create table 類型的 job,會(huì)調(diào)用 onCreateTable 函數(shù),然后做一些 check 后,會(huì)調(diào)用 t.CreateTable 函數(shù),將 db_ID 和?table_ID 映射為 key,tableInfo 作為 value 存到 TiKV 里面去,并更新 job 的狀態(tài)。
finishDDLJob 函數(shù)將 job 從 DDL job 隊(duì)列中移除,然后加入 history ddl job 隊(duì)列中去。
doDDLJob 函數(shù)中檢測到 history DDL job 隊(duì)列中有對應(yīng)的 job 后,返回。
Add indexadd index 主要做 2 件事:
修改 table 的元信息,把 indexInfo 加入到 table 的元信息中去。
把 table 中已有了的數(shù)據(jù)行,把 index columns 的值全部回填到 index record 中去。
具體執(zhí)行流程的前部分的 SQL 解析、Compile 等流程,和 create table 一樣,可以直接從 DDLExec.Next 開始看,然后調(diào)用 alter 語句的 e.executeAlterTable(x) 函數(shù),其實(shí)質(zhì)調(diào) ddl 的 AlterTable 函數(shù),然后調(diào)用 CreateIndex 函數(shù),開始執(zhí)行 add index 的主要工作,具體流程如下:
Check 一些限制,比如 table 是否存在,索引是否已經(jīng)存在,索引名是否太長等。
封裝成一個(gè) job,包含了索引名,索引列等,并將 job 的 type 標(biāo)記為 ActionAddIndex。
給 job 獲取一個(gè) global job ID 然后放到 DDL job 隊(duì)列中去。
owner ddl worker 從 DDL job 隊(duì)列中取出 job,根據(jù) job 的類型調(diào)用 onCreateIndex 函數(shù)。
buildIndexInfo 生成 indexInfo,然后更新 tableInfo 中的 Indices,持久化到 TiKV 中去。
這里引入了 online schema change 的幾個(gè)步驟,需要留意 indexInfo 的狀態(tài)變化:none -> delete only -> write only -> reorganization -> ?public。在 reorganization -> public 時(shí),首先調(diào)用 getReorgInfo 獲取 reorgInfo,主要包含需要 reorganization 的 range,即從表的第一行一直到最后一行數(shù)據(jù)都需要回填到 index record 中。然后調(diào)用 runReorgJob , addTableIndex 函數(shù)開始填充數(shù)據(jù)到 index record中去。runReorgJob 函數(shù)會(huì)定期保存回填數(shù)據(jù)的進(jìn)度到 TiKV。addTableIndex 的流程如下:
啟動(dòng)多個(gè) worker 用于并發(fā)回填數(shù)據(jù)到 index record。
把 reorgInfo 中需要 reorganization 分裂成多個(gè) range。掃描的默認(rèn)范圍是 [startHandle , endHandle],然后默認(rèn)以 128 為間隔分裂成多個(gè) range,之后并行掃描對應(yīng)數(shù)據(jù)行。在 master 分支中,range 范圍信息是從 PD 中獲取。
把 range 包裝成多個(gè) task,發(fā)給 worker 并行回填 index record。
等待所有 worker 完成后,更新 reorg 進(jìn)度,然后持續(xù)第 3 步直到所有的 task 都做完。
后續(xù)執(zhí)行 finishDDLJob,檢測 history ddl job?流程和 create table 類似。
Drop Columndrop Column 只要修改 table 的元信息,把 table 元信息中對應(yīng)的要?jiǎng)h除的 column 刪除。drop Column 不會(huì)刪除原有 table 數(shù)據(jù)行中的對應(yīng)的 Column 數(shù)據(jù),在 decode 一行數(shù)據(jù)時(shí),會(huì)根據(jù) table 的元信息來 decode。
具體執(zhí)行流程的前部分都類似,直接跳到 DropColumn 函數(shù)開始,具體執(zhí)行流程如下:
Check table 是否存在,要 drop 的 column 是否存在等。
封裝成一個(gè) job, 將 job 類型標(biāo)記為 ActionDropColumn,然后放到 DDL job 隊(duì)列中去
owner ddl worker 從 DDL job 隊(duì)列中取出 job,根據(jù) job 的類型調(diào)用 onDropColumn 函數(shù):
這里 column info 的狀態(tài)變化和 add index 時(shí)的變化幾乎相反:public -> write only -> delete only -> reorganization -> absent。
updateVersionAndTableInfo 更新 table 元信息中的 Columns。
后續(xù)執(zhí)行 finishDDLJob,檢測 history ddl job 流程和 create table 類似。
Drop tabledrop table 需要?jiǎng)h除 table 的元信息和 table 中的數(shù)據(jù)。
具體執(zhí)行流程的前部分都類似,owner ddl worker 從 DDL job 隊(duì)列中取出 job 后執(zhí)行 onDropTable 函數(shù):
tableInfo 的狀態(tài)變化是:public -> write only -> delete only -> none。
tableInfo 的狀態(tài)變?yōu)?none 之后,會(huì)調(diào)用 ?DropTable 將 table 的元信息從 TiKV 上刪除。
至于刪除 table 中的數(shù)據(jù),后面在調(diào)用 finishDDLJob 函數(shù)將 job 從 job queue 中移除,加入 history ddl job queue 前,會(huì)調(diào)用 delRangeManager.addDelRangeJob(job),將要?jiǎng)h除的 table 數(shù)據(jù)范圍插入到表 gc_delete_range 中,然后由 GC worker 根據(jù) gc_delete_range 中的信息在 GC 過程中做真正的刪除數(shù)據(jù)操作。
New Parallel DDL目前 TiDB 最新的 Master 分支的 DDL 引入了并行 DDL,用來加速多個(gè) DDL 語句的執(zhí)行速度。因?yàn)榇袌?zhí)行 DDL 時(shí),add index 操作需要把 table 中已有的數(shù)據(jù)回填到 index record 中,如果 table 中的數(shù)據(jù)較多,回填數(shù)據(jù)的耗時(shí)較長,就會(huì)阻塞后面 DDL 的操作。目前并行 DDL 的設(shè)計(jì)是將 add index job 放到新增的 add index job queue 中去,其它類型的 DDL job 還是放在原來的 job queue。相應(yīng)的,也增加一個(gè) add index worker 來處理 add index job queue 中的 job。
并行 DDL 同時(shí)也引入了 job 依賴的問題。job 依賴是指同一 table 的 DDL job,job ID 小的需要先執(zhí)行。因?yàn)閷τ谕粋€(gè) table 的 DDL 操作必須是順序執(zhí)行的。比如說,add column a,然后 add index on column a, 如果 add index 先執(zhí)行,而 add column 的 DDL 假設(shè)還在排隊(duì)未執(zhí)行,這時(shí) add index on column a 就會(huì)報(bào)錯(cuò)說找不到 column a。所以當(dāng) add index job queue 中的 job2 執(zhí)行前,需要檢測 job queue 是否有同一 table 的 job1 還未執(zhí)行,通過對比 job 的 job ID 大小來判斷。執(zhí)行 job queue 中的 job 時(shí)也需要檢查 add index job queue 中是否有依賴的 job 還未執(zhí)行。
EndTiDB 目前一共支持 十多種 DDL,具體以及和 MySQL 兼容性對比可以看 這里。剩余其它類型的 DDL 源碼實(shí)現(xiàn)讀者可以自行閱讀,流程和上述幾種 DDL 類似。
作者:陳霜
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/17776.html
摘要:內(nèi)容概要源碼閱讀系列將會(huì)從兩條線進(jìn)行展開,一條是圍繞的系統(tǒng)架構(gòu)和重要模塊進(jìn)行分析,另一條線圍繞內(nèi)部的同步機(jī)制展開分析。更多的代碼閱讀內(nèi)容會(huì)在后面的章節(jié)中逐步展開,敬請期待。 作者:楊非 前言 TiDB-DM 是由 PingCAP 開發(fā)的一體化數(shù)據(jù)同步任務(wù)管理平臺,支持從 MySQL 或 MariaDB 到 TiDB 的全量數(shù)據(jù)遷移和增量數(shù)據(jù)同步,在 TiDB DevCon 2019 正...
閱讀 2689·2021-11-18 10:02
閱讀 3417·2021-09-28 09:35
閱讀 2598·2021-09-22 15:12
閱讀 757·2021-09-22 15:08
閱讀 3123·2021-09-07 09:58
閱讀 3479·2021-08-23 09:42
閱讀 738·2019-08-30 12:53
閱讀 2087·2019-08-29 13:51