TiDB 应用 | TiDB Online DDL 在 TiCDC 中的应用
TiCDC 作为 TiDB 的数据同步组件,负责直接从 TiKV 获取数据变更信息并同步到下游。其中比较核心的问题是数据解析正确性问题,具体而言就是如何使用正确的 schema 解析 TiKV 传递过来的 Key-Value 数据,从而还原成正确的 SQL 或者其他下游支持的形式。本文主要通过对 TiDB Online DDL 机制原理和实现的分析,引出对当前 TiCDC 数据解析实现的讨论。
背景和问题
F1 Online Schema Change 机制
这里我们定义数据不一致问题为数据多余(orphan data anomaly)和数据缺失(integrity anomaly),Schema 变更结束后出现数据多余和数据缺失我们就认为数据不一致了。这类系统的 schema 变更问题特点可以总结成以下 3 点:
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。
引入共存的中间状态
Delete-only 状态
假设我们已经引入了明确的隔离时间区间(下一个小节会细讲),能保证同一时刻最多只出现 2 个 schema 状态。所以当我们引入 delete-only 状态之后,需要考虑的场景就变成:
1. old schema + new schema(delete-only)
2. 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',所有的服务层节点要么处于 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 状态的节点能看到完整的数据和索引,所以对于用户而言数据都是一致的。
小节总结
引入确定的隔离时间区间
中间状态可见性
小图 (1) 中,服务层节点已经过渡到了场景 1,部分节点处于 old schema 状态,部分节点处于 new schema(delete-only) 状态。此时 c2 对用户是不可见的,不管是 insert < c1,c2> 还是 delete
的显式指定 c2 都是失败的。但是存储层如果存在 [1,xxx] 这样的数据是可以顺利删除的,只能插入 [7] 这样的缺失 c2 的行数据。 小图 (2) 中,服务层节点已经过渡到了场景 2,部分节点处于 new schema(delete-only) 状态,部分节点处于 new schema(write-only) 状态,此时 c2 对用户仍是不可见的,不管是 insert <c1,c2> 还是 delete 的显式指定 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 结果,不过底层的数据是完整且一致的。
总结
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 中;
TiCDC 中 Data 和 Schema 处理关系
1 对应 old schema 状态 此时 old schema data 和 old schema 是对应的*;* 4 对应 new schema public 及之后 此时 new schema data 和 new schema 是对应的; 3 对应 write-only ~ public 之间数据
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 过滤掉,最终上下游都是没有这个表存在的,满足最终一致性。
总结