关于tidb:TiDB-Online-DDL-在-TiCDC-中的应用丨TiDB-工具分享

36次阅读

共计 8048 个字符,预计需要花费 21 分钟才能阅读完成。

引言
TiCDC 作为 TiDB 的数据同步组件,负责间接从 TiKV 感知数据变更同步到上游。其中比拟外围的问题是数据解析正确性问题,具体而言就是如何应用正确的 schema 解析 TiKV 传递过去的 Key-Value 数据,从而还原成正确的 SQL 或者其余上游反对的模式。本文次要通过对 TiDB Online DDL 机制原理和实现的剖析,引出对以后 TiCDC 数据解析实现的探讨。

背景和问题
数据同步组件是数据库生态中不可或缺的生态工具,比拟出名的开源单机数据库 MySQL 就将数据同步作为 Server 能力的一部分,并基于 MySQL binlog 实现异步 / 半同步 / 同步的主从复制。因为 MySQL 乐观事务模型和表元数据锁的存在,咱们总是能够认为 MySQL binlog 中存在因果关系的 data 和 schema 合乎工夫先后顺序的,即:

New data commitTs > New schema commitTs

然而对于 TiDB 这种存储计算拆散的架构而言,schema 的变更在存储层长久化,服务层节点作为多缓存节点,总是存在一个 schema 状态不统一的时间段。为了保证数据一致性和实现在线 DDL 变更,现有的分布式数据库大都采纳或者借鉴了 Online, Asynchronous Schema Change in F1 机制。所以咱们要答复的问题变成了,在 TiDB Online DDL 机制下,TiCDC 如何正确处理 data 和 schema 的对应关系,存在因果关系的 data 和 schema 是否依然满足:

New data commitTs > New schema commitTs

为了答复这个问题,咱们首先须要先论述原始的 F1 Online Schema Change 机制的外围原理,而后形容以后 TiDB Online DDL 实现,最初咱们探讨在以后 TiCDC 实现下,data 和 schema 的解决关系和可能呈现的不同的异样场景。

F1 Online Schema Change 机制
F1 Online Schema Change 机制要解决的外围问题是,在单存储多缓存节点的架构下,如何实现满足数据一致性的 Online Schema 变更,如图 1 所示:

图 1: 单存储多缓存节点的架构下的 schema 变更
这里咱们定义数据不统一问题为数据多余 (orphan data anomaly) 和数据缺失(integrity anomaly),Schema 变更完结后呈现数据多余和数据缺失咱们就认为数据不统一了。这类零碎的 schema 变更问题特点能够总结成以下 3 点:

一份 schema 存储,多份 schema 缓存

局部 new schema 和 old schema 无奈共存

间接从 old schema 变更到 new schema 时,总是存在一个工夫区间两者同时存在

特点 1 和特点 3 是零碎架构导致的,比拟容易了解。特点 2 的一个典型例子是 add index,加载了 new schema 的服务层节点插入数据时会同时插入索引,而加载了 old schema 的服务层节点执行删除操作只会删除数据,导致呈现了没有指向的索引, 呈现数据多余。

Schema 变更问题的特点 2 和特点 3 看起来是互相矛盾的死结,new schema 和 old schema 无奈共存,但又必然共存。而 F1 Online Schema 机制提供的解决方案也很奇妙,扭转不了后果就扭转条件。所以该论文的解决思路上次要有 2 点,如图 2 所示:

图 2: F1 Online DDL 解决方案

  1. 引入共存的两头 schema 状态,比方 S1->S2’->S2, S1 和 S2’能够共存,S2’和 S2 能够共存;
  2. 引入确定的隔离工夫区间,保障无奈共存的 schema 不会同时呈现;

具体来讲:

引入共存的两头 schema 状态
因为间接从 schema S1 变更到 schema S2 会导致数据不统一的问题,所以引入了 delete-only 和 write-only 中间状态,从 S1 -> S2 过程变成 S1 -> S2+delete-only -> S2+write-only -> S2 过程,同时应用 lease 机制保障同时最多有 2 个状态共存。这时只须要证实每相临的两个状态都是能够共存的,保证数据一致性,就能推导出 S1 到 S2 变更过程中数据是统一的。

引入确定的隔离工夫区间
定义 schema lease,超过 lease 时长后节点须要从新加载 schema,加载时超过 lease 之后没法获取 new schema 的节点间接下线,不提供服务。所以能够明确定义 2 倍 lease 工夫之后,所有节点都会更新到下一个的 schema。

引入共存的中间状态
咱们须要引入什么样的中间状态呢?那要看咱们须要解决什么问题。这里咱们依然应用 add index 这个 DDL 作为例子,其余 DDL 细节能够查阅 Online, Asynchronous Schema Change in F1。

Delete-only 状态
咱们能够看到 old schema 是无奈看到索引信息的,所以会导致呈现删除数据,遗留没有指向的索引这种数据多余的异样场景,所以咱们要引入的第一个中间状态是 delete-only 状态,赋予 schema 删除索引的能力。在 delete-only 状态下,schema 只能在 delete 操作的时候对索引进行删除,在 insert/select 操作的时候无奈操作索引,如图 3 所示:

图 3: 引入 delete-only 中间状态
原始论文对于 delete-only 的定义如下:

假如咱们曾经引入了明确的隔离工夫区间(下一个大节会细讲),能保障同一时刻最多只呈现 2 个 schema 状态。所以当咱们引入 delete-only 状态之后,须要思考的场景就变成:

old schema + new schema(delete-only)

new schema(delete-only) + new schema

对于场景 1,所有的服务层节点要么处于 old schema 状态,要么处于 new schema(delete-only) 状态。因为 index 只能在 delete 的时候被操作,所以基本没有 index 生成,就不会呈现后面说的遗留没有指向的索引问题,也不会有数据缺失问题,此时数据是统一的。咱们能够说 old schema 和 new schema(delete-only) 是能够共存的。

对于场景 2,所有的服务层节点要么处于 new schema(delete-only) 状态,要么处于 new schema 状态。处于 new schema 状态的节点能够失常插入删除数据和索引,处于 new schema(delete-only) 状态的节点只能插入数据,然而能够删除数据和索引,此时存在局部数据短少索引问题,数据是不统一的。

引入 delete-only 状态之后,曾经解决了之前提到的索引多余的问题,然而能够发现,处于 new schema(delete-only) 状态的节点只能插入数据,导致新插入的数据和存量历史数据都短少索引信息,依然存在数据缺失的数据不统一问题。

Write-only 状态
在场景 2 中咱们能够看到,对于 add index 这种场景,处于 new schema(delete-only) 状态节点插入的数据和存量数据都存在索引缺失的问题。而存量数据自身数量是确定且无限的,总能够在无限的工夫内依据数据生成索引,然而 new insert 的数据却可能随工夫一直减少。为了解决这个数据缺失的问题,咱们还须要引入第二个中间状态 write-only 状态,赋予 schema insert/delete 索引的能力。处于 write-only 状态的节点能够 insert/delete/update 索引,然而 select 无奈看到索引,如图 4 所示:

图 4: 引入 write-only 状态
原始论文中对于 write-only 状态的定义如下:


引入 write-only 状态之后,上述的场景 2 被切分成了场景 2‘和场景 3:

2’: new schema(delete-only) + new schema(write-only)

3: new schema(write-only) + new schema

对于场景 2‘,所有的服务层节点要么处于 new schema(delete-only) 状态,要么处于 new schema(write-only)。处于 new schema(delete-only) 状态的服务层节点只能插入数据,然而能够删除数据和索引,处于 new schema(write-only) 能够失常插入和删除数据和索引。此时依然存在索引缺失的问题,然而因为 delete-only 和 write-only 状态下,索引对于用户都是不可见的,所以在用户的视角上,只存在残缺的数据,不存在任何索引,所以外部的索引缺失对用户而言还是满足数据一致性的。

对于场景 3,所有的服务层节点要么处于 new schema(write-only) 状态,要么处于 new schema。此时 new insert 的数据都能失常保护索引,而存量历史数据依然存在缺失索引的问题。然而存量历史数据是确定且无限的,咱们只须要在所有节点过渡到 write-only 之后,进行历史数据索引补全,再过渡到 new schema 状态,就能够保证数据和索引都是残缺的。此时处于 write-only 状态的节点只能看到残缺的数据,而 new schema 状态的节点能看到残缺的数据和索引,所以对于用户而言数据都是统一的。

大节总结
通过上面对 delete-only 和 write-only 这两个中间状态的表述,咱们能够看到,在 F1 Online DDL 流程中,原来的单步 schema 变更被两个中间状态分隔开了。每两个状态之间都是能够共存的,每次状态变更都能保证数据一致性,全流程的数据变更也能保证数据一致性。


引入确定的隔离工夫区间
为了保障同一时刻最多只能存在 2 种状态,须要约定服务层节点加载 schema 的行为:

所有的服务层节点在 lease 之后都须要从新加载 schema;

如果在 lease 工夫内无奈获取 new schema,则下线拒绝服务;

通过对服务层节点加载行为的约定,咱们能够失去一个确定的工夫边界,在 2*lease 的工夫周期之后,所有失常工作的服务层节点都能从 schema state1 过渡到 schema state2, 如图 5 所示:

图 5: 最多 2*lease 时长后所有的节点都能过渡到下一个状态
中间状态可见性
要正确理解原始论文的中间状态,须要正确理解中间状态的可见性问题。后面大节为了不便咱们始终应用 add index 作为例子,而后表述 delete-only 和 write-only 状态下索引对于用户 select 是不可见的,然而 write-only 状态下,delete/insert 都是能够操作索引的。如果 DDL 换成 add column,那节点处于 write-only 状态时,用户 insert 显式指定新增列能够执行胜利吗?答案是不能。

总得来说,中间状态的 delete/insert 可见性是外部可见性,具体而言是服务层节点对存储层节点的可见性,而不是用户可见性。对于 add column 这个 DDL,服务层节点在 delete-only 和 write-only 状态下就能看到 new column,然而操作受到不同的限度。对用户而言,只有到 new schema 状态下能力看到 new column,能力显式操作 new column,如图 6 所示:


图 6: 中间状态可见性
为了清晰表述可见性,咱们举个例子,如图 7 所示。原始的表列信息为 , DDL 操作之后表列信息为 <c1,c2>。


图 7: 中间状态过渡
小图 (1) 中,服务层节点曾经过渡到了场景 1,局部节点处于 old schema 状态,局部节点处于 new schema(delete-only) 状态。此时 c2 对用户是不可见的,不论是 insert<c1,c2> 还是 delete<c1,c2> 的显式指定 c2 都是失败的。然而存储层如果存在 [1,xxx] 这样的数据是能够顺利删除的,只能插入 [7] 这样的缺失 c2 的行数据。

小图 (2) 中,服务层节点曾经过渡到了场景 2,局部节点处于 new schema(delete-only) 状态,局部节点处于 new schema(write-only) 状态,此时 c2 对用户仍是不可见的,不论是 insert<c1,c2> 还是 delete<c1,c2> 的显式指定 c2 都是失败的。然而处于 write-only 状态的节点,insert [9] 在外部会被默认值填充成 [9,0] 插入存储层。处于 delete-only 状态的节点,delete [9] 会被转成 delete [9,0]。

小图 (3) 中,服务层所有节点都过渡到 write-only 之后,c2 对用户仍是不可见的。此时开始进行数据填充,将历史数据中缺失 c2 的行进行填充(实现时可能只是在表的列信息中打上一个标记,取决于具体的实现)。

小图 (4) 中,开始过渡到场景 3,局部节点处于 new schema(write-only) 状态,局部节点处于 new schema 状态。处于 new schema(write-only) 状态的节点,c2 对用户仍是不可见的。处于 new schema 状态的节点,c2 对用户可见。此时连贯在不同服务层节点上的用户,能够看到不同的的 select 后果,不过底层的数据是残缺且统一的。

总结
下面咱们通过 3 个大节对 F1 online Schema 机制进行了简要形容。原来单步 schema 变更被拆解成了多个两头变更流程,从而保证数据一致性的前提下实现了在线 DDL 变更。


对于 add index 或者 add column DDL 是上述的状态变更,对于 drop index 或者 drop column 则是齐全相同的过程。比方 drop column 在 write-only 阶段及之后对用户都不可见了,外部能够正确 insert/delete,可见性和之前的阐述齐全一样。

TiDB Online DDL 实现
TiDB Online DDL 是基于 F1 Online Schema 实现的,整体流程如图 8 所示:


图 8 TiDB Online DDL 流程
简略形容如下:

TiDB Server 节点收到 DDL 变更时,将 DDL SQL 包装成 DDL job 提交到 TIKV job queue 中长久化;

TiDB Server 节点选举出 Owner 角色,从 TiKV job queue 中获取 DDL job,负责具体执行 DDL 的多阶段变更;

DDL 的每个中间状态 (delete-only/write-only/write-reorg) 都是一次事务提交,长久化到 TiKV job queue 中;

Schema 变更胜利之后,DDL job state 会变更成 done/sync,示意 new schema 正式被用户看到,其余 job state 比方 cancelled/rollback done 等示意 schema 变更失败;

Schema state 的变更过程中应用了 etcd 的订阅告诉机制,放慢 server 层各节点间 schema state 同步,缩短 2*lease 的变更工夫。

DDL job 处于 done/sync 状态之后,示意该 DDL 变更曾经完结,挪动到 job history queue 中;

具体的 TiDB 解决流程能够参见:schema-change-implement.md 和 TiDB ddl.html

TiCDC 中 Data 和 Schema 解决关系
后面咱们别离形容了 TiDB Online DDL 机制的原理和实现,当初咱们能够回到最一开始咱们提出的问题:在 TiDB Online DDL 机制下,是否还能满足:

New data commitTs > New schema commitTs

答案是否定的。在后面 F1 Online Schema 机制的形容中,咱们能够看到在 add column DDL 的场景下,当服务层节点处于 write-only 状态时,节点曾经可能插入 new column data 了,然而此时 new column 还没有处于用户可见的状态,也就是呈现了 New data commitTs < New schema commitTs,或者说上述论断变成了:

New data commitTs > New schema(write-only) commitTs

然而因为在 delete-only + write-only 过渡状态下,TiCDC 间接应用 New schema(write-only) 作为解析的 schema,可能导致 delete-only 节点 insert 的数据无奈找到对应的 column 元信息或者元信息类型不匹配,导致数据失落。所以为了保证数据正确解析,可能须要依据不同的 DDL 类型和具体的 TiDB 外部实现,在外部保护简单的 schema 策略。

在以后 TiCDC 实现中,抉择了比较简单的 schema 策略,间接疏忽了各个中间状态,只应用变更实现之后的 schema 状态。为了更好表述在 TIDB Online DDL 机制下,以后 TiCDC 须要解决的不同场景,咱们应用象限图进行进一步归类形容。

Old schema New schema
Old schema data 1 2
New schema data 3 4
1 对应 old schema 状态

此时 old schema data 和 old schema 是对应的

4 对应 new schema public 及之后

此时 new schema data 和 new schema 是对应的;

3 对应 write-only ~ public 之间数据

此时 TiCDC 应用 old schema 解析数据,然而处于 write-only 状态的 TiDB 节点曾经能够基于 new schema insert/update/delete 局部数据,所以 TiCDC 会收到 new schema data。不同 DDL 解决成果不同,咱们选取 3 个常见有代表性的 DDL 举例。

add column:状态变更 absent -> delete-only -> write-only -> write-reorg -> public。因为 new schema data 是 TiDB 节点在 write-only 状态下填充的默认值,所以应用 old schema 解析后会被间接抛弃,上游执行 new schema DDL 的时候会再次填充默认值。对于动静生成的数据类型,比方 auto_increment 和 current timestamp,可能会导致上下游数据不统一。
change column:有损状态变更 absent -> delete-only -> write-only -> write-reorg -> public, 比方 int 转 double,编码方式不同须要数据重做。在 TiDB 实现中,有损 modify column 会生成不可见 new column,中间状态下会同时变更新旧 column。对于 TiCDC 而言,只会解决 old column 下发,而后在上游执行 change column,这个和 TiDB 的解决逻辑保持一致。
drop column:状态变更 absent-> write-only -> delete-only -> delete-reorg -> public。write-only 状态下新插入的数据曾经没有了对应的 column,TiCDC 会填充默认值而后下发到上游,上游执行 drop column 之后会抛弃掉该列。用户可能看到预期外的默认值,然而数据能满足最终一致性。
2 对应间接从 old schema -> new schema

阐明这类 schema 变更下,old schema 和 new schema 是能够共存的,不须要中间状态,比方 truncate table DDL。TiDB 执行 truncate table 胜利后,服务层节点可能还没有加载 new schema,还能够往表中插入数据,这些数据会被 TiCDC 间接依据 tableid 过滤掉,最终上下游都是没有这个表存在的,满足最终一致性。

总结
TiCDC 作为 TiDB 的数据同步组件,数据解析正确性问题是保障上下游数据一致性的外围问题。为了能充沛了解 TiCDC 解决 data 和 schema 过程中遇到的各种异样场景,本文首先从 F1 Online Schema Change 原理登程,详细描述在 schema 变更各个阶段的数据行为,而后简略形容了以后 TiDB Online DDL 的实现。最初引出在以后 TiCDC 实现下在 data 和 schema 解决关系上的探讨。

正文完
 0