讲解一下有关 DDL 为什么慢,如何让 DDL 快起来的标准操作。
详细说明
DDL 版本的不同
OceanBase 数据库现有两种 DDL,即并行 DDL 和串行 DDL 两种。在 OceanBase 数据库 V4.1 版本之前只有串行 DDL 任务,V4.1 版本之后陆续开放补全不同模式下的并行 DDL 任务。
| 版本 | 新增的并行 DDL 功能 |
|---|---|
| V4.1.0.0 | truncate table |
| V4.2.1.0 | create table |
| V4.2.2.0 | create index;create comment |
| V4.2.3.0 | create view |
| V4.2.5.1 | drop table |
| V4.3.x | 当前与 V4.2.1 一致 |
注意
表中该版本新增的并行 DDL 功能表示该版本首次支持的并行执行 DDL 操作;后续版本默认会继承之前版本已经支持的操作。例如,V4.2.1 版本支持 TRUNCATE TABLE 和 CREATE TABLE 的并行执行。
DDL 构架
首先要明确历史上 OceanBase 认为 DDL 是低频操作,整个集群仅有一个线程串行处理所有的 DDL 请求,即 RS 上的 DDLQueueTh。所以所有租户的 DDL 任务都会打到 RS 上,由 RS 来串行执行。但是随着越来越多的用户使用 OceanBase 数据库,DDL 的需求也越来越大,修改不同 Schema 版本的 DDL 理论上是可以并行的,所以进行串行 DDL 向并行 DDL 的改造,增加 DDL 语句的吞吐量来满足相关业务的需求。
DDL 执行模型
首先要明确 DDL 简单来说是 Schema 的变更,而 OceanBase 数据库的 Schema 是写在内部表上的,所以说 DDL 就是为了写 Schema 内部表的过程。
下面以一个无需读写的 DDL 的流程为例子:
Client 向 OBServer 发 DDL 要执行 DDL 任务(任务 1)。
OBServer 经过 Parser 和 Resolver 解析生成 Stmt,并将其提交给 Executor,同时调用 RS 对应的 RPC(任务 2)。
RS 立即通过广播 RPC 通知所有其他 OBServer 刷新 Schema(任务 5)。并且自身开启事务,向内部表中写入 Schema 变更信息(任务3,4)这样可以实现 RS 与各 OBServer 节点并行刷新 Schema,提升整体效率。
RS 向 OBServer 返回执行结果(任务 6)。
最终,OBServer 向 Client 返回执行结果(任务 7)。
在这里要注意 DDL 事务提交后 RS 本地进行 Schema 刷新,并通知各 OBServer 进行 Schema 刷新。RS 会不断刷新直到成功,其他 OBServer 仅尽力而为,不保证 Schema 刷新成功。所以只能保证在同一个 Session 下做完这个 DDL 再次查询是可见的,如果不同 Session 或者直连到 Schema 刷新慢的机器上其实是可能出现不可见的。

串并行 DDL
在只有串行 DDL 的时候,所有的 Session 同时发送大量的 DDL 任务,集群只有在 RS 上有一个 DDL 线程。如果多租户都在做 DDL 任务,只要有一个 DDL 任务慢了执行时间久一点,就会产生大量的积压,会造成队列抢占,DDL 任务在队列中等待时间过长从而导致整体时间过长。

为了缓解这个问题提出采用并行 DDL 任务。增加了一个并行 DDL 任务的线程池,让多个线程(DDLPQueueTh)来处理这个 DDL 任务。
根据 CPU 个数分配 2 到 24 个线程,并行 DDL 请求转发至新队列执行不同的 DDL 请求可以在不同的线程上并行执行,例如执行多个 Create Table 操作,提高整体吞吐。

但仍然保留串行 DDL 的任务,将串行 DDL 和并行 DDL 定义为互斥关系,串行 DDL 会获取 RS 内存锁的一个写锁,并行 DDL 会获取 RS 内存锁的一个读锁,即有串行 DDL 就没有并行 DDL,反之亦然。 并行 DDL 之前也有互斥和限制关系,例如修改同一个 Schema 的并行 DDL 也是互斥的,必须要等到并行 DDL 中 Schema Version 更小的事务提交才能提交当前事务。
Schema 刷新
串行 DDL: 当 DDL 线程做完 DDL 事务后会进行 Publish Schema 刷新 RS 本地 Schema 并通知其他 OBServer 刷新 Schema,等待通知 RPC 返回后再返回 Executor。

并行 DDL: 因为 Schema 刷新是比较耗时的操作,在并行 DDL 场景下,若仍像串行 DDL 在 DDL 线程上同步等待 Schema 刷新结果,会空占 DDL 线程,影响整体的吞吐。所以并行 DDL 采取了异步刷新的机制。
异步刷新: 并行 DDL 事务提交后尽快退出并行 DDL 线程防止线程空转,在 ObDDLTransController 的一个后台线程上执行 Publish Schema。

Publish Schema 后正常情况下我们需要检查两个 Version:
检查 Refreshed Version。
OBServer 本地缓存的值。
在 OBServer 本地 Schema 刷新成功后推高,代表 OBServer 本地已经刷到的 Schema Version。
检查 Consensus Version。
OBServer 本地缓存的值。
DDL Controller 在成功刷新 RS 和租户所有 OBServer 后,将刷到的 Schema Version 广播给租户所在 OBServer,OBServer 收到广播后推高,代表集群中租户所有 OBServer 都已经刷到的 Schema Version。
有配置项控制
_wait_interval_after_parallel_ddl默认等待 30s。
但是注意并不是一定要保证都刷新成功,设定配置项是尽可能让刷新成功后返回 Client。
OceanBase 数据库 V3.x DDL 事务提交后,如果
_enable_sync_ddl = true,会触发同步 Schema 刷新,RPC 返回失败则认为 DDL 执行失败。OceanBase 数据库 V4.x 不再提供强同步机制,Schema 异步刷新,并始终忽略 Schema 刷新 RPC 返回的错误,但可以配置
_publish_schema_mode最多等待两秒再返回。
并行 DDL 的实现
执行一次并行 DDL 的流程大致如下:
Start DDL Trans,开并行 DDL。
事务 Lock Object,加对象锁,冲突并行 DDL 互斥。
Gen Schemas,根据参数生成待写入的 Schema。
Pre Allocate Schema Version,计算并预分配的 Schema Version。
Register Task,向 Controller 注册 Task,后续顺序提交。
Alter Schemas,写 Schema 内部表。、
Wait DDL Trans,等待 Schema Version 更小的并行 DDL 事务全部提交。
End DDL Trans,提交或中止事务。
详细可以查看文件 src/rootserver/parallel_ddl/ob_ddl_helper.cpp 中的 ObDDLHelper::execute()(建表的子类 ObCreateTableHelper)。
DDL 为什么慢
经过了上面的铺垫后应该知道什么会影响 DDL 任务的执行:
当有大量租户在同时做 DDL 任务的时候,非常有可能造成队列挤占现象(因为 DDL 服务没有拆到每一个租户下面,对 OceanBase 来说每个租户的 DDL 服务是互相影响的)。
串并行 DDL 同时做,串并行是互斥的,如果串行 DDL 执行了很长时间也就意味着积压了一堆并行 DDL 任务。
本地执行还是远程执行,DDL 任务都要向 RS 发送 RPC。
DDL 间是否有冲突,如果都选择修改一个表那无论开多少个 Session 都是串行 Schema 广播和刷新速度。
并行 DDL 线程是否打满(根据 CPU 分配 2~24 个线程)。
排查 DDL 慢的问题
查询耗时
因为 DDL 都是要打到 RS 上面的所以可以在 RS 那台机器上的 rootservice.log* 所有关键字 execute ddl like stmt.*ddl_stmt_str 加上 DDL 语句。可以查看每一个所需的 DDL 的耗时时间。
# 查看 RS 所在机器的位置
select concat(svr_ip, ':', svr_port) as rs_addr from CDB_OB_LS_LOCATIONS where tenant_id = 1 and role = "LEADER";
# 然后搜索关键字
grep -rni 'execute ddl like stmt.*ddl_stmt_str' rootservice.log* | vim -
示例如下图所示:

分析耗时
总体查看:
总体看一下最近的 DDL 任务,是并串行还是并行,是否多租户都在执行 DDL,是某个租户执行了很久还是都执行了很久(可以按照下面的标准操作解决)。 可以看到这个语句中有一个关键词 Cost 里面表示了这个 DDL 语句的执行时间。 注意:这个 Cost 对不同 DDL 是不一样的。
对于只执行串行 DDL 而言是每一个 DDL 的执行时间,如果这个执行时间比较短,但是总体时间比较长被列为慢 SQL 可以考虑是队列挤占的方向想。需要根据这个线程号来排查一下这个线程之前执行的 DDL 任务有没有特别久的,如果有那很有可能就是这个 DDL 任务导致的该 DDL 任务在队列里面等待时间过久。
对于并行串行混跑的模式下,这个 Cost 是包含了加锁的时间,因为并串行的调度是不一样的。如上图根据线程名字发现有大量的并串行混跑的现象,这个时候 Cost 是加上了等锁时间(并行获取读锁,串行获取写锁)。
细节查看:
根据这个 Trace,继续在 rootservice 里面搜一下到底时间耗时在哪里,Schema 刷新,等锁,写内部表很慢,RPC 传输很慢。根据细节的 DDL 来查看到底哪方面慢。
DDL慢的标准操作
按照版本号打开并行 DDL 操作建表或者别的并行操作。
多开几个 Session 同时执行。
将 RS 的 Leader 和租户的 Leader 切在一起。
多租户同时并发做 DDL 下:让别的租户停止做 DDL 并且尽量删除无用租户。
串并行同时做 DDL 下:尽量一批一批的去操作,按照版本号去找那些能够去做去做一批的串行或者一批并行,不要一个串行一个并行的去执行。
调节
_publish_schema_mode = async。在默认模式下,DDL 会等待两个 Schema 检查(检查时间由配置项_wait_interval_after_parallel_ddl控制,默认为 30 秒)确认 Schema 成功刷新后才返回结果给 Client。而将_publish_schema_mode设置为 async 之后不需要等待这两个检查。CPU 分配太少,压力太大导致并发线程不多的情况下还有很大的压力。减少压力,增加 CPU 核数。
适用版本
OceanBase 数据库 V3.x,V4.x 版本。