分库分表合并同步
本文介绍了 DM 提供的分库分表合并同步功能。此功能用于将上游 MySQL/MariaDB 实例中结构相同的表同步到下游 TiDB 的同一个表中。DM 不仅支持同步上游的 DML 数据,也支持协调同步多个上游分表的 DDL 表结构变更。
注意:
要执行分库分表合并同步任务,必须在任务配置文件中设置
is-sharding: true
。
使用限制
DM 进行分表 DDL 的同步有以下几点使用限制:
在一个逻辑 sharding group(需要合并同步到下游同一个表的所有分表组成的 group)内,所有上游分表必须以相同的顺序执行相同的 DDL 语句(库名和表名可以不同),并且只有在所有分表执行完当前一条 DDL 语句后,下一条 DDL 语句才能执行。
- 比如,如果在 table_1 表中先增加列 a 后再增加列 b,则在 table_2 表中就不能先增加列 b 后再增加列 a,因为 DM 不支持以不同的顺序来执行相同的 DDL 语句。
对于每个逻辑 sharding group,推荐使用一个独立的任务进行同步。
- 如果一个任务内存在多个 sharding group,则必须等待一个 sharding group 的 DDL 语句同步完成后,才能开始对其他 sharding group 执行 DDL 语句。
在一个逻辑 sharding group 内,所有上游分表都应该执行对应的 DDL 语句。
- 比如,若 DM-worker-2 对应的一个或多个上游分表未执行 DDL 语句,则其他已执行 DDL 语句的 DM-worker 都会暂停同步任务,直到等到 DM-worker-2 收到上游对应的 DDL 语句。
sharding group 数据同步任务不支持
DROP DATABASE/TABLE
语句。- DM-worker 中的 binlog 同步单元(sync)会自动忽略掉上游分表的
DROP DATABASE
和DROP TABLE
语句。
- DM-worker 中的 binlog 同步单元(sync)会自动忽略掉上游分表的
sharding group 数据同步任务支持
RENAME TABLE
语句,但有如下限制(online DDL 中的RENAME
有特殊方案进行支持):- 只支持
RENAME TABLE
到一个不存在的表。 - 一条
RENAME TABLE
语句只能包含一个RENAME
操作。
- 只支持
增量同步任务需要确认开始同步的 binlog position 上各分表的表结构必须一致,才能确保来自不同分表的 DML 语句能够同步到表结构确定的下游,并且后续各分表的 DDL 语句能够正确匹配与同步。
如果需要变更 table routing 规则,必须先等所有 sharding DDL 语句同步完成。
- 在 sharding DDL 语句同步过程中,使用 dmctl 尝试变更 router-rules 会报错。
如果需要创建新表加入到一个正在执行 DDL 语句的 sharding group 中,则必须保持新表结构和最新更改的表结构一致。
- 比如,原 table_1, table_2 表初始时有 (a, b) 两列,sharding DDL 语句执行后有 (a, b, c) 三列,则同步完成后新创建的表也应当有 (a, b, c) 三列。
由于已经收到 DDL 语句的 DM-worker 会暂停任务以等待其他 DM-worker 收到对应的 DDL 语句,因此数据同步延迟会增加。
背景
目前,DM 使用 ROW 格式的 binlog 进行数据同步,且 binlog 中不包含表结构信息。在 ROW 格式的 binlog 同步过程中,如果不需要将多个上游表合并同步到下游的同一个表,则只存在一个上游表的 DDL 语句会更新对应下游表结构。ROW 格式的 binlog 可以认为是具有 self-description 属性。
分库分表合并同步过程中,可以根据 column values 及下游的表结构构造出相应的 DML 语句,但此时若上游的分表执行 DDL 语句进行了表结构变更,则必须对该 DDL 语句进行额外同步处理,以避免因为表结构和 binlog 数据不一致而造成同步出错的问题。
以下是一个简化后的例子:
在上图的例子中,分表的合库合表过程简化成了上游只有两个 MySQL 实例,每个实例内只有一个表。假设在数据同步开始时,将两个分表的表结构版本记为 schema V1,将 DDL 语句执行完后的表结构版本记为 schema V2。
现在,假设数据同步过程中,DM-worker 内的 binlog 同步单元(sync)从两个上游分表收到的 binlog 数据有如下时序:
开始同步时,sync 从两个分表收到的都是 schema V1 版本的 DML 语句。
在 t1 时刻,sync 收到实例 1 上分表的 DDL 语句。
从 t2 时刻开始,sync 从实例 1 收到的是 schema V2 版本的 DML 语句;但从实例 2 收到的仍是 schema V1 版本的 DML 语句。
在 t3 时刻,sync 收到实例 2 上分表的 DDL 语句。
从 t4 时刻开始,sync 从实例 2 收到的也是 schema V2 版本的 DML 语句。
假设在数据同步过程中,不对分表的 DDL 语句进行额外处理。当实例 1 的 DDL 语句同步到下游后,下游的表结构会变更成为 schema V2 版本。但在 t2 到 t3 这段时间内,sync 从实例 2 上收到的仍是 schema V1 版本的 DML 语句。当尝试把这些 schema V1 版本的 DML 语句同步到下游时,就会由于 DML 语句与表结构的不一致而发生错误,从而无法正确同步数据。
实现原理
基于上述例子,本部分介绍了 DM 在合库合表过程中进行 DDL 同步的实现原理。
在这个例子中,DM-worker-1 负责同步来自 MySQL 实例 1 的数据,DM-worker-2 负责同步来自 MySQL 实例 2 的数据,DM-master 负责协调多个 DM-worker 间的 DDL 同步。
从 DM-worker-1 收到 DDL 语句开始,简化后的 DDL 同步流程为:
在 t1 时刻,DM-worker-1 收到来自 MySQL 实例 1 的 DDL 语句,自身暂停该 DDL 语句对应任务的 DDL 及 DML 数据同步,并将 DDL 相关信息发送给 DM-master。
DM-master 根据收到的 DDL 信息判断得知需要协调该 DDL 语句的同步,于是为该 DDL 语句创建一个锁,并将 DDL 锁信息发回给 DM-worker-1,同时将 DM-worker-1 标记为这个锁的 owner。
DM-worker-2 继续进行 DML 语句的同步,直到在 t3 时刻收到来自 MySQL 实例 2 的 DDL 语句,自身暂停该 DDL 语句对应任务的数据同步,并将 DDL 相关信息发送给 DM-master。
DM-master 根据收到的 DDL 信息判断得知该 DDL 语句对应的锁信息已经存在,于是直接将对应锁信息发回给 DM-worker-2。
根据任务启动时的配置信息、上游 MySQL 实例分表信息、部署拓扑信息等,DM-master 判断得知自身已经收到了来自待合表的所有上游分表的 DDL 语句,于是请求 DDL 锁的 owner(DM-worker-1)向下游同步执行该 DDL。
DM-worker-1 根据第二步收到的 DDL 锁信息验证 DDL 语句执行请求;向下游执行 DDL,并将执行结果反馈给 DM-master;若 DDL 语句执行成功,则自身开始继续同步后续的(从 t2 时刻对应的 binlog 开始的)DML 语句。
DM-master 收到来自 owner 执行 DDL 语句成功的响应,于是请求在等待该 DDL 锁的所有其他 DM-worker(DM-worker-2)忽略该 DDL 语句,直接继续同步后续的(从 t4 时刻对应的 binlog 开始的)DML 语句。
根据上面的流程,可以归纳出 DM 协调多个 DM-worker 间 sharding DDL 同步的特点:
根据任务配置与 DM 集群部署拓扑信息,DM-master 内部也会建立一个逻辑 sharding group 来协调 DDL 同步,group 中的成员为负责处理该同步任务拆解后的各子任务的 DM-worker。
各 DM-worker 从 binlog event 中收到 DDL 语句后,会将 DDL 信息发送给 DM-master。
DM-master 根据来自 DM-worker 的 DDL 信息及 sharding group 信息创建或更新 DDL 锁。
如果 sharding group 的所有成员都收到了某一条相同的 DDL 语句,则表明上游分表在该 DDL 执行前的 DML 语句都已经同步完成,此时可以执行该 DDL 语句,并继续后续的 DML 同步。
上游所有分表的 DDL 在经过 table router 转换后需要保持一致,因此仅需 DDL 锁的 owner 执行一次该 DDL 语句即可,其他 DM-worker 可直接忽略对应的 DDL 语句。
在上面的示例中,每个 DM-worker 对应的上游 MySQL 实例中只有一个待合并的分表。但在实际场景下,一个 MySQL 实例可能有多个分库内的多个分表需要进行合并,这种情况下,sharding DDL 的协调同步过程将更加复杂。
假设同一个 MySQL 实例中有 table_1 和 table_2 两个分表需要进行合并:
在这个例子中,由于数据来自同一个 MySQL 实例,因此所有数据都是从同一个 binlog 流中获得,时序如下:
开始同步时,DM-worker 内的 sync 从两个分表收到的数据都是 schema V1 版本的 DML 语句。
在 t1 时刻,sync 收到 table_1 分表的 DDL 语句。
从 t2 到 t3 时刻,sync 收到的数据同时包含 table_1 的 DML 语句(schema V2 版本)及 table_2 的 DML 语句(schema V1 版本)。
在 t3 时刻,sync 收到 table_2 分表的 DDL 语句。
从 t4 时刻开始,sync 从两个分表收到的数据都是 schema V2 版本的 DML 语句。
假设在数据同步过程中,不对分表的 DDL 语句进行额外处理。当 table_1 的 DDL 语句同步到下游从而变更下游表结构后,table_2 的 DML 语句(schema V1 版本)将无法正常同步。因此,在单个 DM-worker 内部,我们也构造了与 DM-master 内类似的逻辑 sharding group,但 group 的成员是同一个上游 MySQL 实例的不同分表。
DM-worker 内协调处理 sharding group 的同步与 DM-master 处理 DM-worker 之间的同步不完全一致,主要原因包括:
当 DM-worker 收到 table_1 分表的 DDL 语句时,同步不能暂停,需要继续解析 binlog 才能获得后续 table_2 分表的 DDL 语句,即需要从 t2 时刻继续解析直到 t3 时刻。
在继续解析 t2 到 t3 时刻的 binlog 的过程中,table_1 分表的 DML 语句(schema V2 版本)不能向下游同步;但当 sharding DDL 同步并执行成功后,这些 DML 语句则需要同步到下游。
DM-worker 内部 sharding DDL 同步的简化流程为:
在 t1 时刻,DM-worker 收到 table_1 的 DDL 语句,并记录 DDL 信息及此时的 binlog 位置点信息。
DM-worker 继续向前解析 t2 到 t3 时刻的 binlog。
对于 table_1 的 DML 语句(schema V2 版本),忽略;对于 table_2 的 DML 语句(schema V1 版本),正常同步到下游。
在 t3 时刻,DM-worker 收到 table_2 的 DDL 语句,并记录 DDL 信息及此时的 binlog 位置点信息。
根据同步任务配置信息、上游库表信息等,DM-worker 判断得知该 MySQL 实例上所有分表的 DDL 语句都已收到;于是将该 DDL 语句同步到下游执行并变更下游表结构。
DM-worker 设置 binlog 流的新解析起始位置点为第一步时保存的位置点。
DM-worker 重新开始解析从 t2 到 t3 时刻的 binlog。
对于 table_1 的 DML 语句(schema V2 版本),正常同步到下游;对于 table_2 的 DML 语句(schema V1 版本),忽略。
解析到达第四步时保存的 binlog 位置点,可得知在第三步时被忽略的所有 DML 语句都已经重新同步到下游。
DM-worker 继续从 t4 时刻对应的 binlog 位置点开始正常同步。
综上可知,DM 在处理 sharding DDL 同步时,主要通过两级 sharding group 来进行协调控制,简化的流程为:
各 DM-worker 独立地协调对应上游 MySQL 实例内多个分表组成的 sharding group 的 DDL 同步。
当 DM-worker 收到所有分表的 DDL 语句时,向 DM-master 发送 DDL 相关信息。
DM-master 根据 DM-worker 发来的 DDL 信息,协调由各 DM-worker 组成的 sharing group 的 DDL 同步。
当 DM-master 收到所有 DM-worker 的 DDL 信息时,请求 DDL 锁的 owner(某个 DM-worker) 执行该 DDL 语句。
DDL 锁的 owner 执行 DDL 语句,并将结果反馈给 DM-master;自身开始重新同步在内部协调 DDL 同步过程中被忽略的 DML 语句。
当 DM-master 收到 owner 执行 DDL 成功的消息后,请求其他所有 DM-worker 继续开始同步。
其他所有 DM-worker 各自开始重新同步在内部协调 DDL 同步过程中被忽略的 DML 语句。
在完成被忽略的 DML 语句的重新同步后,所有 DM-worker 继续正常同步。