概述
在《DDL的一生(上)》中,我們以添加全局二級索引爲例,從DDL開發者的視角介紹瞭如何在DDL引擎框架下實現一個邏輯DDL。在本篇,作者將從DDL引擎的視角出發,向讀者介紹DDL引擎的架構、實現,以及DDL引擎與DDL Job的交互邏輯。
在閱讀本文之前,建議讀者先閱讀
DDL引擎相關概念
DDL Job
DDL Job是DDL引擎中的概念,它用於描述一個邏輯DDL。DDL引擎中,一個DDL Job對應一個邏輯DDL,DDL Job內部包含了執行一個邏輯DDL需要的一系列動作,因此在DDL引擎框架下,開發者新支持一條邏輯DDL,實質就是定義一個新的DDL Job。
DDL開發者定義的是靜態的DDL Job,然而,DDL Job在運行時,還擁有狀態屬性。這一屬性主要由DDL引擎負責管理。當然,用戶也可以執行有限的DDL運維指令以管理DDL Job的狀態,實現對DDL執行過程的管理。下圖是DDL Job的狀態轉移圖,圖中黑色加粗線框代表DDL Job執行的初態和終態,每個DDL Job狀態之間的連線上的標註了可以執行的運維指令。
DDL Task
DDL Task是對DDL Job內部一系列行爲的封裝,如讀寫metaDb、在內存中計算、進程通信、向DN下發需執行的物理DDL等,這些行爲都會被分別封裝爲DDL Task。因此一個DDL Job是由若干DDL Task構成的,這些Task需要按一定順序被DDL引擎調度執行,DDL開發者可以使用Polardb-X的DDL引擎提供的DAG圖框架描述Task之間的依賴關係和執行順序。在DDL引擎框架下,開發者定義一個新的DDL Job,實質就是定義若干DDL Task,然後用DAG圖把它們組合起來。
DDL Task是DDL引擎實現DDL近似原子性的重要工具,而DDL原子性是DDL引擎追求的目標。執行一條邏輯DDL涉及到一系列操作,原子性要求這些操作要麼都全部生效,要麼全都不生效。具體來說,DDL引擎要求每個DDL Task都是冪等的,每個Task必須有對應的反向冪等方法(此方法在回滾Task時被DDL引擎調用)。DDL引擎執行DDL之前,會爲該DDL生成由DDL Task組成的DAG圖,並將其持久化到MetaDb,這相當於保證DDL原子性的undo Log。DDL引擎按照DAG圖依次執行Task直到整個DDL Job執行成功或者徹底回滾。
Worker和Leader
在DDL引擎的視角下,CN節點被分爲Worker節點和Leader節點(在集羣中唯一)。Worker節點負責接收用戶發來的DDL請求,它將收到的請求進行簡單的本地校驗,然後把DDL轉換成DDL Job並推送至MetaDb,最後通知Leader節點從MetaDb拉取DDL任務。
Leader節點負責DDL的執行,它從MetaDb拉取到DDL Job後,恢復成DAG圖的形式,並對Job中的Task進行拓撲排序,然後按照一定的並行度進行調度、執行Task。
DDL 引擎源碼目錄
爲了方面下文描述,本文先向讀者說明DDL引擎源碼的目錄。PolarDB-X的DDL引擎的源碼位於com.alibaba.polardbx.executor.ddl.newengine,各模塊說明如下:
子目錄或關鍵類 | 功能 |
job | job和task對象的定義 |
dag | 通用DAG及拓撲排序的實現,包括節點和圖的定義、拓撲排序、DAG的維護和更新 |
meta | 讀寫GMS中的持久化對象的接口,持久化對象包括job和task的狀態、系統資源(持久化讀寫鎖) |
sync | 提供sync接口實現Leader節點和Follower節點之間的信息同步 |
utils | 線程、線程間通信及線程池的封裝 |
serializable | job和task對象的序列化接口 |
DdlEngineDagExecutor | job的執行器,包含Task調度、Task狀態監測、異常處理的主要邏輯 |
DdlEngineScheduler | job的調度器,將job置入執行隊列並調用job的執行器 |
DdlEngineRequester | ddl引擎處理ddl請求的入口,持久化ddl job並通知Leader節點處理ddl請求。 |
例子
下面,本文從DDL引擎的視角出發,向讀者展示一條邏輯DDL是如何被DDL引擎調度並執行的。
DDL 任務調度
一條DDL語句由用戶端的Mysql Client發出後,Worker節點接收到該DDL語句,經過簡單的優化器解析後得到LogicalPlan,然後把該LogicalPlan分派到對應的DDL Handler,這個DDL Handler負責生成DDL Job。然後DDL Handler的公共基類的接口com.alibaba.polardbx.executor.handler.ddl.LogicalCommonDdlHandler#handleDdlRequest
處理這個DDL請求,該函數調用com.alibaba.polardbx.executor.ddl.newengine.DdlEngineRequester#execute
方法將之前生成的DDL Job及執行DDL所需的上下文寫入MetaDB,並通知Leader節點處理。至此,Worker節點完成了自己的工作,如果該DDL是阻塞型的,Worker節點會等待Leader執行完DDL後,返回Response給用戶端;如果該DDL是非阻塞型的,Worker節點會直接返回。
Leader節點上運行着com.alibaba.polardbx.executor.ddl.newengine.DdlEngineScheduler#ddlDispatcherThread
和com.alibaba.polardbx.executor.ddl.newengine.DdlEngineScheduler#ddlSchedulerThread
兩個線程,它們分別對應着實例級別的DdlJobDispatcher和Schema級別的DdlJobScheduler。其中DdlJobDispatcher從全局唯一的Ddl Request 隊列中取出Ddl Request,然後將其分配到Schema級別的Ddl Job隊列。DdlJobScheduler是Schema級別的,它負責從Schema級別的Ddl Job隊列中不斷消費Ddl Job,這個過程中,DdlJobScheduler利用Schema級別的信號量對並行消費Ddl Job的並行度進行控制(同一Schema上的最大線程數爲10)。DdlJobScheduler消費Ddl Job,實質上是從Schema級別的Ddl Job隊列中取出Ddl Job,然後分派給DdlJobExecutor(Job級別),DdlJobExecutor負責將DDL Job轉交給DdlEngineDagExecutor。至此,DDL Job正式進入DDL引擎中的執行器DdlEngineDagExecutor,由後者接管DDL Job的執行。
需要補充說明的是,從上文可以看出DDL引擎支持多個DDL併發執行,爲保證需要相同資源的DDL之間互斥執行,DDL引擎提供了持久化的讀寫鎖機制。作爲DDL開發者,只需要在定義DDL Job的時候,提前聲明該DDL所需的Schema、Table資源。當執行DDL的時候,DDL引擎會在com.alibaba.polardbx.executor.ddl.newengine.DdlEngineRequester#execute
生成DDL Job並保存至MetaDB之前,先根據該DDL Job所需的資源進行讀寫鎖的acquire。
DDL 任務執行
DdlEngineDagExecutor負責DDL任務的執行,它會調用restoreAndRun
方法,從MetaDb中拉取並恢復DDL Job爲DAG形式。然後調用run方法,根據DDL Job的當前狀態執行相應的回調方法。
public class DdlEngineDagExecutor {
public static void restoreAndRun(String schemaName, Long jobId, ExecutionContext executionContext){
boolean restoreSuccess = DdlEngineDagExecutorMap.restore(schemaName, jobId, executionContext);
DdlEngineDagExecutor dag = DdlEngineDagExecutorMap.get(schemaName, jobId);
dag.run();
}
private void run() {
// Start the job state machine.
if (ddlContext.getState() == DdlState.QUEUED) {
onQueued();
}
if (ddlContext.getState() == DdlState.RUNNING) {
onRunning();
}
if (ddlContext.getState() == DdlState.ROLLBACK_RUNNING) {
onRollingBack();
}
// Handle the terminated states.
switch (ddlContext.getState()) {
case ROLLBACK_PAUSED:
case PAUSED:
onTerminated();
break;
case ROLLBACK_COMPLETED:
case COMPLETED:
onFinished();
break;
default:
break;
}
}
}
com.alibaba.polardbx.executor.ddl.newengine.DdlEngineDagExecutor#run
會根據DDL Job當前的狀態,執行對應的回調方法,這本質上是一個在DDL Job的狀態轉移圖上游走的過程。
DDL Job的初始狀態一般爲QUEUED,它表示當前被DDL引擎新調度到Schema級別隊列。此時run方法會依據此狀態調用onQueued()方法。onQueued()方法的作用是將DDL Job的狀態修改爲RUNNING。
當DDL Job當前的狀態是RUNNING時,run
方法就會調用onRunning回調方法,按照DAG圖的依賴關係執行DDL Job內部的Task。
private void onRunning() {
while (true) {
if (hasFailureOnState(DdlState.RUNNING)) {
if (waitForAllTasksToStop(50L, TimeUnit.MILLISECONDS)) {
LOGGER.info(String.format("JobId:[%s], all tasks stopped", ddlContext.getJobId()));
return;
} else {
continue;
}
}
if (executingTaskScheduler.isAllTaskDone()) {
updateDdlState(DdlState.RUNNING, DdlState.COMPLETED);
return;
}
if (executingTaskScheduler.hasMoreExecutable()) {
// fetch & execute next batch
submitDdlTask(executingTaskScheduler.pollBatch(), true, executingTaskScheduler);
continue;
}
//get some rest
sleep(50L);
}
onRunning的流程如下:
- 先檢查當前DDL Job的狀態是否爲RUNNING,如果不是則直接返回。
- 檢查當前DAG圖上是否還有待執行的Task節點,如果沒有,則更新Job狀態爲COMPLETED,然後返回。
- 如果當前DAG圖上存在可以執行的Task,則用拓撲排序的方式,從DAG圖上取出所有可執行的Task,按照並行度的限制,調用
submitDdlTask
方法併發執行。注意,Task並不一定能執行成功,如果有Task執行失敗,submitDdlTask
方法會按照Task的開發者預先定義的失敗策略,修改當前DDL Job的狀態。最典型的,當有Task失敗時,修改當前DDL Job狀態爲 PAUSED 或 ROLLBACK_RUNNING。詳細的錯誤處理與恢復機制,將在下一小節介紹。
如果有DDL Job的狀態爲ROLLBACK_RUNNING,run方法就會調用onRollingBack()回調方法,實現DDL的回滾。相關代碼如下
private void onRollingBack() {
if (!allowRollback()) {
updateDdlState(DdlState.ROLLBACK_RUNNING, DdlState.ROLLBACK_PAUSED);
return;
}
reverseTaskDagForRollback();
// Rollback the tasks.
while (true) {
if (hasFailureOnState(DdlState.ROLLBACK_RUNNING)) {
if (waitForAllTasksToStop(50L, TimeUnit.MILLISECONDS)) {
LOGGER.info(String.format("JobId:[%s], all tasks stoped", ddlContext.getJobId()));
return;
} else {
continue;
}
}
if (reveredTaskScheduler.isAllTaskDone()) {
updateDdlState(DdlState.ROLLBACK_RUNNING, DdlState.ROLLBACK_COMPLETED);
return;
}
if (reveredTaskScheduler.hasMoreExecutable()) {
// fetch & execute next batch
submitDdlTask(reveredTaskScheduler.pollBatch(), false, reveredTaskScheduler);
continue;
}
//get some rest
sleep(50L);
}
}
onRollingBack的流程如下:
- 首先檢查,在當前DAG圖的執行進度下,是否允許回滾(一旦越過了fail point task,則不允許回滾)。如果不可回滾,則標記當前DDL Job的狀態爲PAUSED,然後退出。
- 當DDL Job的狀態爲ROLLBACK_RUNNING時,可能還存在其他正在執行中的Task。此時DDL引擎將不再允許新的Task開始執行,並且會等待正在執行中的Task成功或失敗,此時該DDL Job就到達了一個一致性的狀態。
- 達了一致性狀態後可以開始回滾流程,首先逆轉DAG圖的所有有向邊,使整個DDL Job的執行流程反過來。然後按照逆轉後的DAG圖進行拓撲排序,取出之前執行完畢或執行過但未完成的Task,執行它們的反向冪等方法。
- 當DAG圖中沒有可執行的Task節點時,標記DDL Job狀態爲ROLLBACK_COMPLETED,回滾成功。
其餘狀態的回調函數邏輯較爲簡單,這裏不再贅述,請感興趣的讀者自行閱讀代碼。
錯誤處理與恢復
DDL引擎追求的目標之一是DDL的原子性,如果在執行DDL的過程中部分Task失敗,DDL引擎需要採取適當措施讓DDL Job變成完全未執行或執行成功的狀態(即狀態轉移圖中的終態)。DDL引擎採取的辦法是給Task添加DdlExceptionAction屬性,該屬性用於指示DDL引擎執行Task出現異常時如何處置。DDL開發者可以在定義DDL Task的時候設置該屬性。
DdlExceptionAction一共有4種取值
- TRY_RECOVERY_THEN_PAUSE:執行該Task出現異常後,重試3次,如果仍失敗,則將Task對應的DDL Job狀態設置爲PAUSED。
- ROLLBACK:執行Task出現異常後,將該Task所在DDL Job狀態設置爲ROLLBACK_RUNNING,隨後DDL引擎會根據該狀態進行回滾DDL。
- TRY_RECOVERY_THEN_ROLLBACK:執行該Task出現異常後,重試3次,如果仍失敗,將該Task所在DDL Job狀態設置爲ROLLBACK_RUNNING,隨後由DDL引擎回滾該DDL。
- PAUSE:執行該Task出現異常後,將Task對應的DDL Job狀態設置爲PAUSED。
一般來說,PAUSED狀態意味着該DDL Job沒有達到終態,需要開發者介入處理,這常用於出現異常後無法恢復的Task,或者對外界產生了影響以致無法回滾的Task。前者舉例,如drop table指令,一旦執行了刪除元信息或刪除物理表的Task,就無法再恢復到刪除前的狀態了,這時如果某Task失敗且重試3次後仍失敗,就會導致該DDL Job進入PAUSED狀態;後者舉例,如Polardb-X中大部分DDL Job都含有一個CDC打標的Task,用於對外生成bin log,該Task執行完成意味着外界已經可以獲取相應DDL的bin log,因此無法回滾。
總結
本文從DDL引擎的視角,向讀者介紹了DDL引擎的架構、實現,以及DDL引擎與DDL Job的交互邏輯。瞭解更多關於Polardb-X源碼的解析,請持續關注我們後續發佈的文章。
本文爲阿里雲原創內容,未經允許不得轉載。