开源分布式数据库PolarDB-X源码解读——PolarDB-X源码解读(十):DDL的一生(下)

本文涉及的产品
云原生数据库 PolarDB MySQL 版,通用型 2核8GB 50GB
云原生数据库 PolarDB PostgreSQL 版,标准版 2核4GB 50GB
简介: 开源分布式数据库PolarDB-X源码解读——PolarDB-X源码解读(十):DDL的一生(下)

一、概述


在《DDL的一生(上)》中,我们以添加全局二级索引为例,从DDL开发者的视角介绍了如何在DDL引擎框架下实现一个逻辑DDL。在本篇,作者将从DDL引擎的视角出发,向读者介绍DDL引擎的架构、实现,以及DDL引擎与DDL Job的交互逻辑。  


在阅读本文之前,建议读者先阅读:


《DDL的一生(上)》

《PolarDB-X DDL也要追求ACID?》


二、DDL引擎相关概念


1.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状态之间的连线上的标注了可以执行的运维指令。



           


2.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执行成功或者彻底回滚。


3.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

utils

线程、线程间通信及线程池的封装 
serializable job和task对象的序列化接口
DdlEngineDagExecutor  job的执行器,包含Task调度、Task状态监测、异常处理的主要逻辑
DdlEngineScheduler  job的调度器,将job置入执行队列并调用job的执行器 
DdlEngineRequester  ddl引擎处理ddl请求的入口,持久化ddl job并通知Leader节点处理ddl请求。

四、例子 


下面,本文从DDL引擎的视角出发,向读者展示一条逻辑DDL是如何被DDL引擎调度并执行的。


     


1.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。


2.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,回滚成功。


其余状态的回调函数逻辑较为简单,这里不再赘述,请感兴趣的读者自行阅读代码。


3.错误处理与恢复


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源码的解析,请持续关注我们后续发布的文章。



相关实践学习
体验PolarDB-X透明分布式
PolarDB-X具备从单机到分布式的平滑演进能力,支持通过DDL将一张大表动态调整为分布式的分区表,结合分布式事务、以及兼容MySQL Binlog的数据回流,可完成单机到分布式的快速改造。本体验从PolarDB-X的Auto Partition,以及兼容MySQL Binlog的CDC能力两个方面演示透明分布式能力。
相关文章
|
2月前
|
关系型数据库 分布式数据库 数据库
|
2月前
|
存储 关系型数据库 分布式数据库
喜报|阿里云PolarDB数据库(分布式版)荣获国内首台(套)产品奖项
阿里云PolarDB数据库管理软件(分布式版)荣获「2024年度国内首版次软件」称号,并跻身《2024年度浙江省首台(套)推广应用典型案例》。
|
4月前
|
关系型数据库 MySQL 数据库
MyEMS开源系统安装之数据库
本文详细讲解MyEMS的安装步骤,重点介绍数据库架构与脚本部署。MyEMS支持MySQL 8.0、MariaDB 10.5及SingleStore 7.0等数据库服务器。通过命令行或客户端工具执行SQL脚本完成安装,包括多个数据库(如myems_billing_db、myems_energy_db等)。此外,提供解决常见问题的方法,如“用户拒绝访问”、“COLLATE设置”和“MAX_ALLOWED_PACKET错误”。注意,不建议在生产环境中将数据库安装于Docker容器内。
93 1
|
3月前
|
存储 监控 关系型数据库
突破IO瓶颈:PolarDB分布式并行查询(Parallel Query)深度调优手册
在海量数据处理中,I/O瓶颈严重制约数据库性能。本文基于PolarDB MySQL 8.0.32版本,深入解析分布式并行查询技术如何提升CPU利用率至86.7%、IO吞吐达8.5GB/s,并结合20+实战案例,系统讲解并行架构、执行计划优化、资源调优与故障排查方法,助力实现高性能数据分析。
103 6
|
4月前
|
存储 Cloud Native 关系型数据库
PolarDB开源:云原生数据库的架构革命
本文围绕开源核心价值、社区运营实践和技术演进路线展开。首先解读存算分离架构的三大突破,包括基于RDMA的分布式存储、计算节点扩展及存储池扩容机制,并强调与MySQL的高兼容性。其次分享阿里巴巴开源治理模式,涵盖技术决策、版本发布和贡献者成长体系,同时展示企业应用案例。最后展望技术路线图,如3.0版本的多写多读架构、智能调优引擎等特性,以及开发者生态建设举措,推荐使用PolarDB-Operator实现高效部署。
234 3
|
4月前
|
SQL 关系型数据库 分布式数据库
PolarDB开源数据库入门教程
PolarDB是阿里云推出的云原生数据库,基于PostgreSQL、MySQL和Oracle引擎构建,具备高性能、高扩展性和高可用性。其开源版采用计算与存储分离架构,支持快速弹性扩展和100%兼容PostgreSQL/MySQL。本文介绍了PolarDB的安装方法(Docker部署或源码编译)、基本使用(连接数据库、创建表等)及高级特性(计算节点扩展、存储自动扩容、并行查询等)。同时提供了性能优化建议和监控维护方法,帮助用户在生产环境中高效使用PolarDB。
1394 21
|
4月前
|
Cloud Native 关系型数据库 分布式数据库
PolarDB开源:云原生数据库的新篇章
阿里云自研的云原生数据库PolarDB于2023年5月正式开源,采用“存储计算分离”架构,具备高性能、高可用及全面兼容性。其开源版本提供企业级数据库解决方案,支持MySQL、PostgreSQL和Oracle语法,适用于高并发OLTP、核心业务系统等场景。PolarDB通过开放治理与开发者工具构建完整生态,并展望更丰富的插件功能与AI集成,为中国云原生数据库技术发展贡献重要力量。
417 17
|
4月前
|
存储 关系型数据库 分布式数据库
PolarDB开源进阶篇:深度解析与实战优化指南
PolarDB是阿里云开源的云原生数据库,采用计算-存储分离架构,结合高性能共享存储与Parallel Raft多副本一致性协议,实现微秒级延迟和卓越性能。本文深入解析其架构设计,涵盖智能调度层、性能优化技巧(如查询优化器调优和分布式事务提升)、高可用与容灾配置、扩展功能开发指南以及监控运维体系。同时,通过电商平台优化案例展示实际应用效果,并展望未来演进方向,包括AI结合、多模数据库支持及Serverless架构发展。作为云原生数据库代表,PolarDB为开发者提供了强大支持和广阔前景。
253 16
|
安全 druid Java
Seata 1.8.0 正式发布,支持达梦和 PolarDB-X 数据库
Seata 1.8.0 正式发布,支持达梦和 PolarDB-X 数据库
962 87
Seata 1.8.0 正式发布,支持达梦和 PolarDB-X 数据库
|
存储 DataWorks 监控
DataWorks,一个 polar db 有上万个数据库,解决方案
DataWorks,一个 polar db 有上万个数据库,解决方案

相关产品

  • 云原生数据库 PolarDB