转载请注明原文地址https://www.jianshu.com/p/b86cc9354b20
TiCDC拥有自身的分布式高可用设计,且这些设计依赖数据库实例的PD组件,所以从这个角度来看,相对于debezium这种小工具,可能要稍微复杂一些。对于实现TiDB的Change Data Capture,并不是一个很新的话题,除了PingCAP原厂支持的binlog, cdc以外,业界早几年前就有过相关探索。
上周Flink CDC宣布在即将推出的2.2版本中新增tidb的cdc connector,当时就在想,如果是java版的tidb cdc,很有可能从TiFlink这个项目中衍生……今天看到pr了,居然真的是借鉴了TiFlink的代码……
https://github.com/ververica/flink-cdc-connectors/commit/b911dd2a1b884d728393bd95e2a5966c8b878bb8
TiFlink与TiCDC
TiFlink是2020年TiDB Hackathon大赛的一个著名项目,使用 TiKV 和 Flink 实现了强一致的物化视图的功能。
其内部并未采用TiCDC,而是直接调用了TiKV的CDC接口,自己用java实现接收了TiKV推送过来的cdc日志并组装,并将该功能与flink集成,使得flink拥有了抽取tidb变化数据的能力。
首先,作为渣渣的我先膜拜一下参赛大佬,能在极短的时间内开发出这样一套工具并演说出来,真的不简单,NB……orz
但是,从客观事实角度来讲,短时间开发的experimental项目,功能稳定性,与经历过生产考验的TiCDC对比,这个结果是毫无疑问的,不用说了吧。
两者共同点
都是grpc通信,都是通过TiKV侧的CDC接口来接收TiKV推送过来的cdc日志,组装成完整的事务。
我之前有一篇文章专门介绍TiKV侧CDC接口:
https://www.jianshu.com/p/dd5c7c222703
不同点,或者说:基于TiFlink二次开发,有哪些核心功能需要完善的?
以下都是ticdc都已经具备的功能,但tiflink缺少的。我先按功能重要性列了个表格,其中:
★ 表示必要功能,缺少将不能投产
☆ 表示重要功能,缺少将严重影响运维、很多场景会出现问题
功能点 | 重要性 |
---|---|
对table id变化的处理 | ★ |
对region状态变化的处理 | ★ |
对GC safepoint的考虑 | ★ |
对表结构变化的支持 | ☆ |
对大事务的支持 | ☆ |
支持分区表 | 一般 |
- 对table id变化的处理
在tidb中,每张表都对应有一个table id。我们想抽取这张表的数据,首先需要通过table id计算出这张表对应的key区间,然后去pd里面找这个区间对应的region,再向对应region发起请求。
key区间的编码规则为:
从 t_r 至 t_s 的左闭右开
然后编码为8+1字节对齐,便于memcmp,具体规则如下
// EncodeBytes guarantees the encoded value is in ascending order for comparison,
// encoding with the following rule:
// [group1][marker1]...[groupN][markerN]
// group is 8 bytes slice which is padding with 0.
// marker is `0xFF - padding 0 count`
// For example:
// [] -> [0, 0, 0, 0, 0, 0, 0, 0, 247]
// [1, 2, 3] -> [1, 2, 3, 0, 0, 0, 0, 0, 250]
// [1, 2, 3, 0] -> [1, 2, 3, 0, 0, 0, 0, 0, 251]
// [1, 2, 3, 4, 5, 6, 7, 8] -> [1, 2, 3, 4, 5, 6, 7, 8, 255, 0, 0, 0, 0, 0, 0, 0, 0, 247]
// Refer: https://github.com/facebook/mysql-5.6/wiki/MyRocks-record-format#memcomparable-format
举个例子,我建了一张表,table id为45,那么这张表的区间范围就是:
7480000000000000ff2d5f720000000000fa
至
7480000000000000ff2d5f730000000000fa
先不要去纠结key怎么编码的,这里我想说的是table id会发生变化。当对一张表执行某些特定的ddl语句时,比如truncate,table id一定会发生变化。一旦table id发生变化,还用原先计算的key区间去订阅region,明显获取不到你想要的数据的。
我们看下ticdc是怎么处理这个问题的:
其实很简单,只需要订阅ddl对应的region即可,如果发现有对这张表的ddl会造成table id变化,取消原来的订阅,根据新的table id重新计算key区间,发起新的订阅。
tidb的元数据也存储于tikv内,对应的key区间编码规则为:
从 m<元数据属性名>l 至 m<元数据属性名>m 的左闭右开
比如ddl对应的元数据属性名为 "DDLJobList"
订阅这类区间就可以获得所有ddl操作。
- 对region状态变化的处理
之前就已经说过,TiKV的region不是固定不变的,可能发生分裂、合并、易主等状况,尤其是在生产环境,region变化是会比较频繁的,如果不处理此类状态变化,明显是不能用于生产的。
region常见状态变化,来自cdcpb.proto:
message Error {
// 某tikv节点比较繁忙可能出现,另一个tikv节点被选为了region的leader
errorpb.NotLeader not_leader = 1;
// region发生合并的常见异常
errorpb.RegionNotFound region_not_found = 2;
// region发生分裂的常见异常
errorpb.EpochNotMatch epoch_not_match = 3;
DuplicateRequest duplicate_request = 4;
Compatibility compatibility = 5;
ClusterIDMismatch cluster_id_mismatch = 6;
}
当收到region状态变化的通知时,需要重新去pd获取当前region信息。不过有一点需要注意,pd的region信息可能也不是最新的,获取到之后还要进行检查,region是否cover了你订阅的key区间。
可以通过以下命令手动迫使region分裂合并,观察ticdc的行为:
分裂:
mysql -h xxx -u root -P 4000 -e "split table cdc_bench.sbtest1 between (0) and (10000) regions 2"
合并:
pd-ctl --pd=http://x.x.x.x:2379 operator add merge-region 528 2
- 对GC safepoint的考虑
注意这里说的gc不是jvm gc,是指TiDB内部对于MVCC的过期数据的一个定期清理过程。众所周知MVCC并不是把update和delete的数据覆盖掉,而是新建立一个版本。如果不定期做清理,那么历史版本会越积越多,影响数据库的读性能。所以GC safepoint是指某个时间点,早于这个时间点的所有历史数据都将被清理掉;TiDB负责定期推进这个时间点,TiKV负责按时间点来执行清理操作。
TiCDC问世以后,由于TiCDC宕机与重启这期间的数据完全靠TiKV的“增量扫”来获取,如果历史数据被清理掉,增量扫是无法获取到正确的值(至少delete操作会丢失)。故TiCDC也有一个GC safepoint,只有正常运行时,这个safepoint才会往前推进。TiKV综合TiDB的safepoint和cdc的safepoint来进行GC操作,以此保证不会将不该清理的数据清理掉。
GC safepoint有一个专门的rpc接口定义在pdpb.proto
// TiDB的
rpc UpdateGCSafePoint(UpdateGCSafePointRequest) returns (UpdateGCSafePointResponse) {}
// TiCDC的
rpc UpdateServiceGCSafePoint(UpdateServiceGCSafePointRequest) returns (UpdateServiceGCSafePointResponse) {}
- 对表结构变化的支持
从TiKV接口中获取的rowdata是schemaless的,并不包含表结构信息。所以要想将DML数据完整还原出来,必须获取该条rowdata对应的正确版本的schema。
但是很不幸,和binlog不同,TiKV的CDC接口并未提供这一行数据对应的schema版本号。
所以这个问题不太好处理。并且由于TiDB是分布式数据库,其对于Online DDL的支持会比单机数据库要复杂许多,变相增加了处理难度。
TiCDC对于这块的处理主要是以时间戳和因果关系为原则,兼具考虑Online DDL的所有中间态,逻辑比较复杂,这里不做详细展开。
- 对大事务的支持
这个就不用细说了,因为要基于prewrite和commit操作来进行事务组装,如果事务很大的话,内存可能装不下。TiCDC里使用了外排,组件名称是叫UnifiedSorter,大家可以自行看下。TiFlink则直接使用的hashmap来装,对大事务的支持有限。