分布式TDSQL for MySQL数据库是一种支持存算分离、自动水平拆分、Shared Nothing 架构的分布式数据库。整体架构分为数据节点和计算节点。数据节点由腾讯自研的 TXSQL 负责底层数据管理相关功能,计算节点在协议层和功能方面兼容 MySQL 8.0。本文主要介绍的是,计算节点如何将一个 DDL 正确地执行到这些数据节点,从而保证集群整体对外的一致性。
背景
本文介绍 TDSQL for MySQL 架构中 DDL 框架实现原理。我们首先需要了解两个专业术语:
●CN:TDSQL 的计算节点,全称是 Compute Node。包含 DDL 组件,计算下推组件,分布式 XA 事务组件等计算层核心功能。
●DN:TDSQL 的数据节点,全称是 Data Node。我们把每个存储数据分片的节点叫做 DN,每个 DN 都是一个独立的 TXSQL 引擎。
假设一个集群由2个 CN 和3个 DN 组成,那么一条 DDL 语句需要在所有 CN 和 DN 上执行,如果其中某个节点执行失败或超时,需要一些机制来让整个集群的元数据能自动恢复到一致状态,以减少人工干预。
DDL 框架的主要工作也是在保证各类 DDL 能够正确执行,主要包含以下几种方式:
● 前置检查:执行 DDL 前检查所有节点,以减少执行出错的概率。
● 出错重试与自动回滚:执行 DDL 过程中出错,通过重试减少偶发错误。对于支持回滚的 DDL,通过自动回滚保证所有节点一致。
● 任务接管:CN 接管丢失心跳的 DDL 任务来继续执行。
● 手动重试:DDL 任务执行失败后,通过重新触发任务,来补偿未执行完成的步骤。
本文将通过对 TDSQL for MySQL DDL 框架实现描述,让读者对 DDL 框架正确性保障有一个大概了解。
实现原理
1、执行流程总览
当客户端向 CN 提交一个 DDL 后,CN 会通过如下流程来执行:
1. 根据当前上下文信息创建出一个 DDL Job,并将任务信息持久化在元数据 DB 上,当前会选择最后一个 DN 作为元数据节点。
2. 开始执行 DDL 状态机:
a. 对所有 DN 和 CN 都进行前置检查。不同的 DDL 的类型,所做的前置检查也会不一样。
b. 广播需要执行的 DDL 至所有 DN 和 CN。当执行出错时,会自动进行重试。重试多次失败后,针对支持回滚的 DDL 类型,会自动回滚任务。
c. 写入元数据,并标记该任务执行完成。
3. 返回客户端执行结果。
整体流程如下图(该集群由2个 CN 和3个 DN 组成):
2、前置检查
DDL 框架为了保证集群整体的执行表现与单机 MySQL 一致,在真正执行 DDL 前会进行各种必要的前置检查,以减轻执行出错的可能性。
我们以 rename tables 场景来举例说明,假设集群由1个 CN 和2个 DN 组成,DN 分别为 DN1 和 DN2。t1 和 t2 都为分布式表(数据分布在一个或多个 DN 上),t1 表只存在于 DN1 上,t2 表存在于 DN1 和 DN2 上。当执行 rename table t1 to t1_new, t2 to t2_new 时,如果 DN1 上已经存在了 t1_new 表,那么当执行该 DDL 时会有如下两种应对方式:
● 执行前置检查:查询出 DN1 上已经存在了 t1_new ,则不会继续执行该 DDL 任务。避免执行后导致集群不一致。
● 不进行前置检查:执行 DN1 rename SQL 会报错提示 t1_new 已经存在,执行 DN2 rename SQL 会成功。导致了集群不一致。
因此,上述示例需要执行前置检查。同时,并不是所有 rename tables 中的 new table 都需要检查存在性,比如包含中间表的情况, rename table t1 to tmp, t2 to t1, tmp to t2,其中 tmp 作为一个中间表不应该去 DN 上检查存在性,否则也会产生一些误判导致 DDL 无法执行。
前置检查中除了表存在性检查,还会包含表的一致性检查,即查询所有 CN 和 DN 保证它们在执行 DDL 前表结构是一致的。该检查主要用于 Alter Table 的场景,为了防止在已经不一致的表结构上,继续追加变更,导致不一致的情况加剧,给后续恢复造成困难。
目前表一致性检查会包含如下几种分类:
● Table Attribute:检查表的基础信息。
● Column Attribute:检查表上列的基础信息。
● Index Attribute:检查表上的索引信息。
● Character:检查表的字符集。
● Partition:检查表的分区信息。
前置检查中还会去所有 DN 上尝试短暂获取并释放被操作表的 Exclusive Lock,以降低执行 DDL 阶段时被锁阻塞的可能性。
举例来说,假设一个 Alter Table DDL 任务需要在所有 DN 上执行,如果某个 DN 上刚好存在一个长事务,如果不进行锁检查,那么该任务执行会一直等待,而在等待期间内,执行成功的 DN 的表结构已经发生了变化,从而造成了集群整体的不一致。
通过上述示例,可以发现必要前置检查可以一定程度地降低集群不一致的情况。下表也列举了当前不同 DDL 类型会包含的前置检查类型:
\ 前置检查类型DDL 类型 | 表存在性检查 | 表一致性检查 | 锁检查 |
---|---|---|---|
Create Table | ✅(非 if not exists) | ❌ | ❌ |
Drop Table | ✅(非 if exists) | ❌ | ✅ |
Rename Table | ✅ | ❌ | ✅ |
Alter Table | ✅ | ✅ | ✅ |
3、容错处理
DDL 执行阶段会在所有 DN 并行执行,待 DN 全部执行成功后,再在所有 CN 并行执行。不难发现,这个过程中很容易出现一些节点执行失败,另外一些节点执行成功的情况。举例来说,DDL 执行 DN 阶段某个 DN 突然重启导致连接断开,这时则需要进行重试来恢复执行。
对于重试的策略,我们采用了尽可能重试的策略,来尽可能保证执行成功。以并行执行 DN 举例,会包含如下策略:
● 有限的黑名单来配置不允许重试的错误,比如:SQL 语法错误。
● 白名单配置必须重试的错误,比如:网络错误。
● 除了命中以上黑白名单的错误,如果所有 DN 都返回相同错误,则不进行重试。比如:Add column 时所有 DN 都返回列已存在。
同时,重试也需要保证幂等性。DDL 框架中也有两种策略来处理:
● 执行的 SQL 本身是幂等的,比如:create table if not exists。
● 通过 Check SQL 来检查是否需要继续执行,比如:执行 rename table 前会先检查 new table 是否存在,只有不存在时,才会继续执行 rename table。
当遇到无法重试的错误,或重试多次失败后,DDL 框架会对支持的 DDL 类型进行自动回滚。比如:
● Create table 执行失败时,会通过 Drop table SQL 来进行回滚。
● Rename table 执行失败时,会反写将 rename 成功的节点进行回滚。
4、任务接管
CN 本身是一个无状态的计算节点,集群中会存在多个 CN 的情况,并且每个 CN 都可以执行 DDL 任务。任务接管主要讨论的一个场景是,当 CN 执行 DDL 任务期间发生故障时,如何将任务继续执行下去。
DDL 框架通过一些机制和后台线程来减轻该问题:
● 表级全局唯一锁:框架会为 DDL 任务涉及到的每个表,向元数据 DB 写入一条记录,代表集群同一时刻该表只能有一个 DDL 任务。
● 状态持久化机制:DDL 任务在执行状态机中每次流转前,都会在元数据 DB 记录状态信息。以确保如果任务重新执行,可以从上一状态开始执行。
● 幂等性机制:执行状态机中执行的 SQL 都需要幂等性保证,上面章节的容错处理中也提到该机制。
● 任务心跳线程:CN 更新所有正在执行的 DDL 任务的心跳时间,并持久化到元数据 DB 中。
● 接管任务线程:定期扫描未完成且失去心跳的 DDL 任务,一旦发现满足条件的任务,则立即更新该任务的心跳,代表任务已接管,接着后台线程会继续执行该任务。
下图是任务接管和执行的大致流程:
周边管理命令
除了上文提到的 DDL 框架自身的正确性保障机制,真实使用场景中还需要一些周边命令,来增强任务观测性和补偿异常任务,以减轻人为干预成本。
下面会依次介绍目前支持的 DDL 管理命令。
1、SHOW DDL
该命令用于展示当前集群中正在执行或已经执行结束的所有 DDL 任务。主要使用场景如下:
● 观察任务当前执行状态,是否成功或失败、执行的耗时、执行任务的 CN 信息等。
● 快速筛选出某个表已经执行的 DDL,方便回溯执行历史。
下面简单列举了使用用例:
-- 只展示当前正在执行的任务
SHOW DDL;
-- 只展示任务 ID 为8的任务
SHOW DDL 8;
-- 展示最近10个任务
SHOW FULL DDL LIMIT 10;
-- 筛选表名为 test 的正在执行的任务
SHOW DDL WHERE `table_name`='test';
-- 筛选表名为 test 的所有任务
SHOW FULL DDL WHERE `table_name`='test';
-- 筛选所有的任务,并用 '%err%' 去模糊匹配任务信息
SHOW FULL DDL LIKE '%err%';
2、KILL DDL
该命令用于强制停止当前正在执行的任务。主要使用场景如下:
● 当前正在执行的 DDL 任务耗时过长,影响正常 DML,需要强制停止并断开与所有 DN 的连接。
● 误提交了某个 DDL 任务,需要强制停止。
下面简单列举了使用用例:
-- 1. 通过 SHOW DDL 获取需要强制停止的任务ID,假设任务ID为9
SHOW DDL;
-- 2. 执行 KILL DDL
KILL DDL 9;
-- 3. 通过 SHOW DDL 观察任务被停止状态
SHOW DDL 9;
3、REPEAT DDL
该命令用于重新执行已经完成并执行失败的 DDL 任务。并且会检查所操作的表不能存在已经执行成功的 DDL 任务。主要使用场景如下:
● 由于 DN 数据导致执行 DDL 失败,人为干预修复后,需要重新执行该 DDL 任务。
● 对误停止的 DDL 任务来恢复重新执行。
下面简单列举了使用用例:
-- 1. 通过 SHOW DDL 获取需要强制停止的任务ID,假设任务ID为9
SHOW FULL DDL WHERE `error_code` !=0;
-- 2. 执行 REPEAT DDL
ALTER REPEAT 9;
-- 3. 通过 SHOW DDL 观察任务重新状态
SHOW DDL 9;
4、计算节点本地对象的DDL
某些 DDL 特性需要单独走特殊执行流程,并且它们只存在于计算节点本地,不会持久化到数据节点。我们称此类 DDL 为计算节点本地对象的 DDL,会包含如下类型:
这些类型的 DDL 也会通过 DDL 框架去执行,因此周边的管理命令也同样适用于它们。但是,它们的执行流程不同于广播 SQL 的方式,而是通过异步通知并同步等待的方式让所有 CN 节点来执行。
具体执行流程如下:
1.向元数据 DB 写入 DDL 任务信息,并异步通知所有 CN。
2.同步等待所有 CN 执行结果。
3.通过 SHOW CREATE 语句获取 DDL 定义语句,并写入 snapshot 表中,用于后期增量或全量同步。
除了正常的执行流程以为,我们还加入了如下正确性保证:
●相邻 CN 下线后,执行阶段能够正确感知并将它剔除执行结果判断。
●每个 CN 都会包含一个系统表,用于持久化目前已经执行的 DDL 的版本号,保证后期同步时的幂等性和数据完备性。
同步模块
该模块属于元数据同步模块,但与 DDL 框架密不可分,因为 CN 作为一个无状态计算节点,需要再启动或网络隔离后,能够快速追上当前集群最新元数据信息。下面会分别介绍 CN 在初始启动和网络隔离恢复后的同步行为是如何保证正确性的。
1、初始启动
同步阶段会包含两类不同的同步流程,第一类为常规 DDL 同步流程,第二类为计算节点本地对象的 DDL 同步流程。这两类同步流程都需要持久化一个最大版本信息,记录当前已经应用过的版本,下文会将他们称为 applied_version。
对于常规 DDL 同步流程来说,同步阶段会按照如下步骤进行:
1. 通过元数据 DB 查询大于自己版本的所有已经完成的任务。
2. 根据 DDL 类型执行不同的前置检查,也会复用上文提到的前置检查逻辑,在本地执行需要检查的 SQL。
3. 根据 DDL 任务信息,恢复执行上下文,比如:sql_mode 和 character_set_client 等 session 变量。
4. 本地执行 DDL 对应 SQL。
5. 如果执行出错,会加入强同步列表,最后进行表强同步。
6. 持久化最新的 applied_version。
其中第5步中的强同步的考虑是:CN 本身不会持久化数据,因此对于无法同步的表可以通过删除后重新创建的方式来快速恢复它。
对于计算节点本地对象的 DDL 同步流程来说,同步阶段会按照如下步骤进行:
1. 通过元数据 DB 查询大于自己版本的所有已经完成的任务。
2. 获取这些任务对应的 snapshot 信息。
3. 删除任务中存在,但 snapshot 中不存在的 DDL 信息。
4. 根据 DDL 任务信息,恢复执行上下文,比如:sql_mode 和 character_set_client 等session变量。
5. 根据 snapshot 信息依次执行 DDL。
6. 持久化最新的 applied_version。
其中第3步执行删除的考虑是:如果删除一个计算节点本地对象的 DDL ,它也会在 snapshot 中被删除,但任务中仍然会记录,因此需要将这类 DDL 先删除。
2、网络隔离
CN 可能会处于网络隔离环境中后恢复,这是需要依赖后台线程定期扫描机制,来察觉当前 applied_version 是否已经落后再进行同步。
同步流程与初始启动流程一致。但区别在于,相比启动阶段先会执行常规 DDL 同步流程,再执行计算节点本地对象的 DDL 同步流程来说,后台同步线程是并行执行的,但常规 DDL 与 计算节点本地对象的 DDL 是有明显的偏序关系。比如,CREATE VIEW V1 AS SELECT * FROM T1; 需要依赖 CREATE TABLE T1 已经执行成功。如果同步计算节点本地对象的 DDL 的线程先执行任务,就会导致执行报错。
针对这种存在偏序关系的同步问题,有多种应对方案,比如:计算节点本地对象的 DDL 同步流程需要更完备的前置检查与重试机制来修复同步乱序问题;通过全局序列号给两种类型的 DDL 同步事件定序来实现全序关系。受限于篇幅,该部分也会在未来分享。
总结
本文详细阐述了 DDL 框架在正确性保障方面的任务容错能力和任务管理能力,未来也会分享更多复杂点:
● 更全面的自动回滚场景:针对 ALTER TABLE 下的具体场景,通过进一步细分,来实现某些异常下的自动回滚的能力,以整体提高易用性。比如 CREATE INDEX 场景。
● 更全面的隔离性:DDL 框架执行或同步阶段无法避免并发的 DML 访问到一些中间状态,但可以通过多版本元数据,来提高与 DML 之间的隔离性。