前言
数据库同步是一个比较常见的需求,业务数据一般存储在一致性要求比较高的OLTP数据库中,在分析场景中往往需要OLAP数据库或者比较火的数据湖方案;CDC是数据库同步较为流行的方案,全称是Change Data Capture,主要用于捕捉数据库中变化的数据,然后根据变化的数据写入不同的目标存储。接下来是一些数据库CDC方案的调研及原理探讨,调研方案基于MySQL数据库。
本文会主要关注同步过程中一致性的技术点,如有偏颇,欢迎拍砖。
CDC方案大致可以分为基于查询的和基于日志的方案,基于查询的方案是基于SELECT语句来查询发现变化的数据,基于日志是指MySQL的binlog,天然记录了数据库的变化数据。
基于查询的方案
基于查询是指通过定期执行SELECT语句来查看两次查询的结果的差别,每次查询都全量查询数据,然后比较两次数据的差别的方案比较重,并且对于大数据量场景效率也比较低,一般会借助数据库原有数据的递增字段来查询,比较常见的追踪变化的字段类型包括:
基于更新时间戳
在数据库的行的定义中定义更新字段,比如update_time;将上次同步的时间记为last_update_time,然后下次查询时只拉取update_time比last_update_time大的数据;
此方案实现简单,但是对于update_time有要求,在两次查询期间可能有update_time相同的数据可能会造成数据重复(update_time >= last_update_time)或者漏查(update_time > last_update_time)的风险。
基于递增主键
数据库的行中一般会定义主键,比如订单ID,用户ID等可以定义成自增主键,这些主键也可以用于跟踪变化的数据,下次查询数据只要比上次查询的数据最大的主键大就可以获取到变化的数据;
此方案对于主键有一个要求就是主键必须是递增的,新的数据的主要一定要比原有数据的主键大(类型不限),比如uuid作为主键是不适合的(uuid作为主键一般也不是最佳实践)。
还有一些其他类似的字段或者是多个字段的组合,本质上都是通过字段的递增变化来发现变化的数据。基于查询的方案有如下一些特点:
- 要求数据库表包含递增字段,最好是严格递增字段(不含等于)。
- 适合appendonly的数据库表,例如归档订单数据,消息表等,如果含有更新或删除操作,查询的方案往往不能发现。
- 使用基于查询的产品有Splunk DBConnect,Kafka Connect。
基于Binlog方案
binlog是在MySQL服务层实现的独立于存储引擎的事务日志,会记录所有的DDL和和包含变更的DML,以Event的格式记录,同时会记录语句的执行时间。可以认为是MySQL的ChangeLog。binlog主要用在数据库的崩溃恢复和主从复制场景。数据同步方案类一般是模拟主从复制场景。开源领域有很多成熟的方案,如阿里的Canal,debezium等。
以下是MySQL借助binlog实现主备复制的原理:
master写入binlog后,slave通过IO线程将binlog写到自己的relay log,然后slave通过sql线程重放relay log中的事件,来实现主备的数据同步。
canal是基于master/slave的同步协议,将自己注册成slave来实现同步,主要步骤:
- canal发送dump协议给MySQL master。
- master收到dump请求,将binlog推送到canal。
- canal解析binlog事件对象。
在该方案中,主要通过注册slave和dump协议来完成对数据的同步,对于订阅数据库的变更是很理想的方案,特点如下:
- binlog记录了数据库的变更,对于增量数据的同步非常适合,比如数据的变更更新到消息系统或者更新缓存。
- 由于master的binlog可能会删除,所以对于全量同步还是有一定局限性。
基于Snapshot+Binlog
Flink CDC 2版本在2021年发布,包含了MySQL CDC的版本更新,接下来根据分别介绍下其旧版本的原理和新版本的改进。
如果要全量读取MySQL的数据并且保持增量数据的更新,直观的方案就是先对MySQL的数据进行读取snapshot,然后接着从binlog消费变更数据。这个方案的关键点在于binlog消费与snapshot的无缝衔接。总体来说:Flink CDC 1.x使用了锁方案,Flink CDC 2使用了无锁方案。
全局锁方案
Flink CDC背后使用的是Debezium对数据进行同步,同步一张表包含两个阶段:
- 全量同步:拉取所有表中的现有数据。
- 增量同步:消费binlog变更数据。
全量同步阶段使用了锁,默认使用全局锁,在全局锁不可用的时候会使用表锁。我们来看下为什么需要锁,假如不用锁的情况下,可能分为如下两种情况:
- 1)SELECT所有数据,2)读取binlog消费点,3)从binlog消费点消费:这种情况SELECT数据在执行的过程中,会消耗一定的时间,这段时间内可能会有新的更新,这个更新在SELECT数据中不存在;在SELECT数据读取完毕后,在获取binlog消费点,这时候binlog消费点可能丢失了SELECT数据后的一小段更新,造成数据丢失。
- 1)读取binlog消费点,2)SELECT所有数据,3)从binlog消费点消费。在步骤2中的SELECT数据可能已经包含了步骤1中的消费点后的数据,比如SELECT包含了数据a,binlog消费点后包含数据a的insert,这时候步骤3从binlog消费点消费时,数据a又会插入,出现数据重复。
简单原理可以通过下图示意:图中每个格子表示一个CUD操作,可以简单的把每个格子连起来当做binlog。如果在读取Snapshot时加锁,在读完的时候再读取binlog的位置,可以保证数据是连续的,不重不漏,过早或者过晚读取binlog都会造成数据的不一致。
所以为了保证数据的一致性,需要增加全局锁或者表锁,然后借助MySQL的可重复读事务进行读取全量数据和binlog消费点,最后释放锁;在可重复事务中,可以保证消费点和当前数据的无缝衔接。在最后的binlog消费过程中,保证数据不重不漏。
该方法有如下特点:
- 可重复读事务保证了数据库表的schema和binlog消费位点更当前数据的匹配。
- 全局锁或者表锁,会阻止数据的更新,锁的时间越长对于数据库影响越多,特别是线上业务可能会造成业务的阻塞。
无锁方案
Flink CDC 2.0使用了无锁方案,避免了1.x中的锁造成的阻塞。方案借鉴了Netflix的DBlog的设计。主要改进在于Chunk并发读取和无锁操作。所以这里直接看下DBLog的基于Chunk的无锁实现。直接拿文中的实例来看算法的实现。
以MySQL为例,在执行SELECT前后在binlog中增加Low watermark和High watermark,这时候SELECT出的resultset一定是lw和hw之间的快照。然后在result set中去掉与lw和hw之间有重叠的数据,最后再把去掉后的数据作为output buffer,最后在执行binlog的replay,这样就能做到数据的不重不漏。可以通过一些例子来看是否有重复和遗漏:
- 一个insert事件在lw之后,select之前,这时select的数据是包含insert的数据,把select中的数据去掉insert的条目,在执行从lw到hw的binlog replay,insert的数据还是会出现在最终结果中。
- insert事件在select之后,hw之前,这时select不包含insert的数据,在select数据去掉insert的条目(没有改变),在执行lw到hw的binlog replay,insert的数据还是会出现在最终结果中。
同理,对于update和delete事件,都可以做到保证数据的一致性。
接下来以一个简单的图示来描述根据snapshot和binlog如何做到一致性:
上图中排除chunk的影响,把数据库的数据可以想象成binlog的序列,假设binlog是无大小限制,图中每个格子代表一个原子操作,操作序列从开始到任意一个格子按顺序apply可以得到当前数据库的所有内容。图中LW,HW代表获取的binlog位置的低水位和高水位,执行流程:
- 首先通过 SHOW MASTER STATUS 获取当前binlog文件的偏移量当做LW;
- 然后通过SELECT读取全量数据的快照Snaphost,在读取的过程中无锁操作,允许数据的插入和更新;
- 读取完快照Snaphost再通过SHOW MASTER STATUS获取当前binlog文件偏移量当做HW;
- 读取LW到HW中的数据集称为delta,设置Snapshot = Snapshot - delta;
- 基于最新的Snaphost,从LW开始消费binlog;从此之后可以获取数据库的Full State。
从这张图的可以看出提前读取binlog可以做到无锁读取数据库的Full State,根本原因在于从Snaphot排除掉LW到HW的binlog的影响,第4步得到的结果可以当作LW之前的全量数据;这里第4步有一个前提:数据行必须有主键,否则不好做差集操作。
通过上面的例子可以看出无锁方案获取完整数据的思路,但是在获取Snapshot时,往往数据量很庞大,由于无锁方案一般要求数据行有主键,所以可以根据主键将数据拆分成多个chunk,然后在每个chunk中执行类似操作,使用并发的方式可以提供读取的性能。细节在参考文件中会有介绍,本文主要关注在数据表Full State的获取方案,chunk方案可以参考论文或者Flink CDC 2.0的实现。还有很多细节文中没有过多介绍,比如binlog中数据库schema的变更,也会影响到数据的同步。
总结
在CDC方案中,基于查询和binlog的方案都有一定的劣势,在全量数据下,一致性方面都会有所欠缺,基于Snapshot+binlog的方案结合了两者的特点,可以保证同步数据的一致性。但是原生的基于锁的全量数据获取具有一定的性能问题,基于DBLog的无锁方案在性能和一致性上可以兼得。是比较理想的方案。
参考
Change data capture:https://en.wikipedia.org/wiki/Change_data_capture#Methodology
Alibaba canal:https://github.com/alibaba/canal
Flink CDC 2.0 正式发布,详解核心改进:https://flink-learning.org.cn/article/detail/3ebe9f20774991c4d5eeb75a141d9e1e
DBLog: A Generic Change-Data-Capture Framework:https://netflixtechblog.com/dblog-a-generic-change-data-capture-framework-69351fb9099b
splunk DB Connect:https://splunkbase.splunk.com/app/2686/
MySQL Source (JDBC) Connector for Confluent Cloud:https://docs.confluent.io/cloud/current/connectors/cc-mysql-source.html#step-2-add-a-connector