写在前面
如今数据库的种类繁多,功能也很丰富。数据库类型总体上分为两类:关系型和非关系型。其中关系型数据库通常以表格形式组织数据,并遵循表关系的约束。例如创建一张表,表里面包含多个列,不同的列可以有不同的类型。若我们需要修改表结构,如创建索引、修改主键、加减列操作、重分区等,我们称这类操作为 DDL。OceanBase 支持多种 DDL 操作来满足用户的实际生产需要。那么 DDL 的具体表现形式是什么,其实现的原理又是怎样?这篇文章将从执行 alter table 的视角,来剖析 DDL 语句的背后原理。特此说明,以下内容仅针对 OceanBase 4.x 发行版本。
DDL 流程
我们以一条修改列类型的 SQL 来开启 DDL 原理的探索,一起看下这条 SQL 语句的背后到底发生了什么,假设我们的表名为 t1,表结构里有 c1 列,原列类型为 int,我们执行如下的 DDL 操作。
alter table t1 modify column c1 bigint;
当用户执行了上述 SQL 后,如果我们部署了 OBProxy 的集群,SQL 指令会先发送到 OBProxy 中处理,然后 OBProxy 经过简单的解析,路由计算,把这条 SQL 发送到集群中的一台 OBserver,我们称为中控 OBServer。中控 OBServer 在收到消息后,经过 SQL 语法语义解析,发现这是一条 alter table 的 DDL 语句,在经过优化器、执行器以及生成物理计划后,通过 RPC 转发给另外一台 RootService(RS) 所在的 OBServer 节点来处理。当 RS 接收到来自中控 OBServer 的 RPC 请求后,从请求包里解析出需要修改的 Schema 表结构信息,并把这些信息更新到对应的内部表,然后再发起一个 DDL Task 任务,最后任务会被相关的 OBServer 节点执行。当任务执行完后,RS 会把任务状态发送回中控 OBServer 节点。特别的,对于执行时间可能比较长的 DDL 操作,RS 会识别出来并在任务创建后,返回任务 ID 给中控 OBServer,然后中控 OBServer 通过轮询任务状态,来决定什么时候结束返回。最后中控 OBServer 会把任务状态回给 OBProxy,OBProxy 再返回给客户端,结束本次 DDL 操作。我们来看下流程:
Alter table
了解了 DDL 的执行流程之后,我们再来看下 alter table 内部触发了哪些具体操作。
当 RS 收到中控 OBServer 发送的请求包后,就已经知道这是一条修改列类型的 DDL。RS 经过一番计算后,发现这个修改列类型的 DDL 是 Offline DDL,需要对目标表的数据进行重整,于是 RS 结合解析出来的信息,创建出一个 DDL Task,并把这个 Task 放入到一个任务队列,我们叫 DDL Task Queue。后续会通过 DDL 任务调度(DDL Scheduler )从任务队列中得到这个 Task,执行完成后,把执行结果记录到内部表,最后中控 OBServer 从内部表中获取实时的执行状态,判断是否需要结束。我们看下简易流程:
这里面的 DDL Scheduler 调度器工作最为繁重,因为要不停的从 DDL Task Queue 里取出任务并执行。
可能就有同学会问,如果我的一个 DDL 任务耗时很长,会不会阻塞调度器对其他任务的执行呢?答案是不会。这里我补充说明下,首先,DDL Scheduler 执行线程不止一个。其次,每一个 DDL 任务会被切分为多个任务状态来分段执行,DDL Scheduler 每调度一次,完成一次状态切换。DDL Scheduler 依赖这些状态的切换来推进任务,直到成功走到最后 Succ 成功的状态。
那如果在某个任务状态卡住了怎么办?这个问题问得太好了,我们会通过 DDL 重试机制来容错,一般一个 DDL 任务会设定 72h 的超时时间,如果确实是因为数据量比较大,在 DDL 补数据阶段需要的时间比较长,这种只能等待补数据结束,其他场景如获取 Table Entry 失败,或者获取锁冲突等异常,我们会做重试,根据经验 72 个小时足够容错。
那如果正在执行 DDL 的 RS 发生切主了,旧主的任务队列内存信息没有了,要怎么恢复呢?这很好解决,我们已经考虑到故障重启/切主等场景,RS 在创建 DDL Task 任务的同时,我们会把 DDL 任务状态持久化到内部表,切主后从内部表恢复任务状态,继续从上一个状态执行。
DDL 任务状态及演进
好了,既然 OceanBase 是根据状态机来推进 DDL 任务的,那么我们一起来看下修改列类型时,主要的任务状态含义以及演进过程吧:
Wait_trans_end
等事务结束。这里补充说明下,OceanBase 在做列类型转换的 DDL 时,是通过双表双写的方式进行的,即按照 DDL 后的表结构 Schema 创建一张新的隐藏表,并把旧表的数据全部切换到新隐藏表里。但因为创建表之后可能还有一些 DDL 操作前的事务还没结束,因此需要等待这些事务结束,并获取一个全局快照点,来做后续的操作。
Obtain_snapshot
获取快照点。在上面等事务结束后,我们获取一个全局快照点,这个快照点是新表从旧表中重写数据的依据。此时,只要对快照点之前的数据进行补全就能保证新表的数据是完整的。需要进一步说明的是,在快照点之前也有一些事务已经往新表里插入过数据,但根据幂等性,我们可以保证读出来的数据没有问题。
Redefinition
补数据。在上面的状态结束后,我们得到了一个全局快照点,并且因为这是一个修改列类型的 DDL,我们假设修改的列非主键列,不涉及分区键的变更,因此修改后的列相关的宏块是不需要重新排序的。这里应该很好理解,分区键未变化,修改后新表的分区和原表的分区规则相同,在分区键不变的情况下,新表数据分区位置不变。为了会更好的理解补数据的逻辑,下面稍微展开介绍下补数据的类别。
补全数据分为三类:
- 第一类是需要分区排序,但源表和目的表分区规则不同,如创建全局索引。排序是指将主表分区之间的数据做排序后,填充到全局索引表。
- 第二类是需要分区排序,但源表和目的表分区规则相同,如修改主键、创建局部索引。这个应该很好理解,修改主键和创建局部索引不会变更新旧表的分区方式,但会影响分区内数据的顺序,排序是指数据重新排列后,填充到新表分区。
- 第三类是分区内不需要排序,例如,修改列类型(假设是未修改主键列的列类型)。这就更好理解了,因为主键(rowkey)不变,修改列类型不会影响数据在存储中的顺序,因此不需要重新排序。
为了更加直观查看数据的分区排序,我们举例创建全局索引的补数据过程吧:
Copy_table_dependent_object
重建与原表相关的对象以及约束。例如索引表,在上面的例子中,我们修改了列类型,假设这个列是索引表中的列,那么索引表同样需要被重建,也需要 RS 创建一个重建任务,主体流程和主表 DDL 类似。
Take_effect
新表生效。在做完上面的补数据流程后,我们认为新表已经具备了完整的数据,并且可以对外提供服务了。由于在整个补数据阶段我们是锁表的,也就是屏蔽了 DML/DDL 的并发干扰,因此只需要把新表的名字直接切换成旧表的名字即可。在这之后还会做一些收尾的动作,例如恢复分区读写、删除原表、清理临时数据等。
Online 和 Offline
上面提到 Online 和 Offline 这些个字样,我在这里简单补充说明一下。由于 OceanBase 采用 Offline DDL 双表双写方式,涉及隐藏表的创建以及对主表数据的重整,即需要把数据写入到新表,表数据越多,执行时间越长。Offline 的意思是说,在执行 DDL 的同时,会阻塞其他 DML 的操作。实现的原理其实也简单,给需要做 DDL 的表加上一把表锁,当 DML 执行需要获取这把锁时,发现已经被其他事务持有,那么就会等待重试直至超时退出。与之相对应的是 Online DDL,也就是在 DDL 的同时,允许 DML 操作。实现上是 DDL 只修改表 Schema 原数据信息,例如修改表名,加列等操作,表结构完成修改后,其他事务可以立刻操作新表,源表数据不需要立刻做重整。
对于我们支持的 Online DDL 类型,同学们可以参考这篇文章:OceanBase 4.0解读:兼顾高效与透明,我们对DDL的设计与思考
写在最后
经过上面的介绍,相信同学们的心里已经对 OceanBase 的 alter table 流程有了大概的认识。当然,DDL 的场景还有很多,上面只是列举了列类型变更一种,或说一类操作吧(Offline DDL),希望能让同学们有个感性的认识。另外,OceanBase 的 DDL 的整体流程相对并不复杂,但因为往往需要和事务、RS、复制迁移等交互,以及需要控制 DDL 任务执行对 CPU、磁盘 IO 的影响,细节很多,内容还是相当丰富的。有兴趣的同学可以看下代码的具体实现细节,也欢迎在评论区和我们一起探讨。