CDC简介
CDC(Change Data Capture)是一种用于跟踪数据库库变更事件(插入、更新、删除)中的行级更改,并将事件以发生的顺序通知到其他系统处理。在容灾场景下,CDC主要实现的是主备间的数据同步,即从主数据库到备数据库的数据实时同步。
source ----------> CDC ----------> sink
Apache SeaTunne CDC
SeaTunnel CDC的数据同步分为两种:
-
快照读:读取表的历史数据
-
增量跟踪:读取表的增量日志更改数据
2.1 无锁快照同步
无锁快照同步阶段,为什么强调无锁,是因为现有的CDC平台在进行历史数据的同步时可能会进行锁表操作,例如Debezium。快照读阶段就是对数据库的历史数据库进行同步的过程,其基本概述流程如下:
css
storage------------->splitEnumerator----------split---------->reader
^ |
| |
\-----------------report------------/
split划分:splitEnumerator(split分发器)按照指定的字段(例如表id或唯一键)和步长将表数据划分为多个分片split。 并行处理:每个split通过路由算法分配给不同的reader进行并行读取,一个reader会占用一个连接。 事件反馈:每个reader完成split读取后会向splitEnumerator报告进度。 splitEnumerator会发送给reader一个分片,分片的元数据信息如下:
javascript
String splitId 路由id
TableId tableId 表id
SeatunnelRowType splitKeyType 分片基于的字段的类型
Object splitStart 分片读取起点
Object splitEnd 分片读取终点
reader收到split信息后会生成相关的sql语句,在此之前会记录当前split对应到数据库日志log的开始位置,等处理完当前split后上报report给splitEnumerator,report内容如下:
bash
String splitId 分片id
Offset highWatermark 分片对应log的位置,用于后续的校对
2.2 增量同步
增量同步阶段是基于上述快照读取阶段后,在源数据库发生变化时,实时将变更的数据同步到备数据库,不同的是,此阶段监听的是数据库的log日志,例如mysql的bin log。增量跟踪通常是单线程处理,这样可以避免重复拉取bin log,减轻对数据库的压力,因此该阶段只有一个reader工作,只占用一个连接。
lua
data log------------->splitEnumerator----------split---------->reader
^ |
| |
\-----------------report------------/
增量同步会合成快照阶段所有split、table,因此只会存在一个split,增量同步阶段的split信息如下:
bash
String splitId
Offset startingOffset 所有split中最小的log start
Offset endingOffset log的结束位置,若无则代表是持续的,例如增量阶段
List<TableId> tableIds
Map<TableId, Offset> tanleWatermarks 所有split的watermark
List<CompletedSnapshotSplitInfo> completedSnapshotSplitInfos 快照阶段读取的split细节信息
其中CompletedSnapshotSplitInfo的具体字段如下:
javascript
String splitId
TableId tableId
SeatunnelRowType splitKeyType
Object splitStart
Object splitEnd
Offset watermark 对应了report中的highWatermark
增量阶段的split包含了快照阶段所有split的watermark,会去从其中选出一个合适的位置进行增量同步,这个合适位置就是最小的watermark。
三、Exactly-once 无论是快照读还是增量读,同步的过程中数据库可能也在经历变化,如何保证exactly-once?
3.1 快照读阶段
在快照读阶段,例如某个split在同步的过程中,这段split中的数据发生了变换,例如下图操作,插入一条k3,更新k2,删除k1,如果在读的过程中不做任务标识,那么这部分的更新信息就会丢失,seatunnel的做法是:
在split读取之前首先去数据库查一下bin log位置:low watermark
读取split{start, end}数据
再记录一下高水位high watermark
如果high = low 说明在读取该split期间,该split的数据没有发生变化;如果(high - low) > 0,说明在处理的过程中发生了数据变化,会进行如下操作:①将读到的split数据在内存中建立内存表缓存;②将low watermark~high watermark的变更;③按顺序、主键重放操作到内存表
报告report high watermark
sql
insert k3 update k2 delete k1
| | |
v v v
bin log --|---------------------------------------------------|-- log offset
low watermark high watermark
CDC读到的数据: k1 k3 k4
| 重放
v
真实的数据: k2 k3' k4
增量阶段
在增量阶段开始之前首先会对上一个步骤的所有split做校验,因为在split和split之间的间隙也有可能出现数据更新,例如在split1和split2之间插入了若干条记录,在快照阶段就会遗漏掉,对于这种split之间的数据回捞,seatunnel的做法是:
从所有的split的report中找到最小的watermark,作为start watermark,开始读取log。 每读一条log都去completedSnapshotSplitInfos中找该条数据是否在某个split被处理过了,如果没有被处理过,说明是split间隙数据,应该被重新修正。 当表过滤完后,可以从completedSnapshotSplitInfos中删除,继续处理剩余的表。 直到所有的split都校验结束,就进入到了完全的增量阶段。
lua
|------------filter split2-----------------|
|----filter split1------|
data log -|-----------------------|------------------|----------------------------------|- log offset
min watermark split1 watermark split2 watermark max watermark
断点续传
如果做到暂停恢复?分布式快照算法(Chandy-Lamport):
假设系统中包含了两个进程p1和p2,p1进程状态包含三个变量X1 Y1 Z1,p2包含了三个变量X2 Y2 Z2,初始状态如下:
makefile
p1 p2
X1:0 X2:4
Y1:0 Y2:2
Z1:0 Z2:3
此时由p1发起全局snapshot记录,p1先记录本身的进程状态,然后向p2发送marker信息。在marker信息到达p2之前,p2向p1发送message M。
makefile
p1 p2
X1:0 -------marker-------> X2:4
Y1:0 <---------M---------- Y2:2
Z1:0 Z2:3
p2收到p1发送来的marker信息后,记录自己的状态,然后p1收到p2之前发送来的message M,由于p1已经做了local snapshot了,所以p1只需要记录M。,所以最终的snapshot如下:
makefile
p1 M p2
X1:0 X2:4
Y1:0 Y2:2
Z1:0 Z2:3
在SeaTunnel CDC的过程中,marker同发送给所有的reader、splitEnumerator、writer等节点都会保存自己的内存状态。
本文由 白鲸开源科技 提供发布支持!