PolarDB-X源碼解讀:DDL的一生(下)

概述

在《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#ddlDispatcherThreadcom.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源碼的解析,請持續關注我們後續發佈的文章。

原文鏈接

本文爲阿里雲原創內容,未經允許不得轉載。

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