数据复制不论是对于 IT 核心系统的高可用,数据库系统国产化替换,还是大数据系统的数据集成,都是一个历久弥新的话题,时至今日,数翊科技在众多客户的实施案例中,数据复制和数据集成都是我们的最重要的产品和服务之一。小六在本系列文章中将和大家探讨这个话题,本文小六首先定义出来数据复制的五类问题,后续将结合行业以及数翊的产品和交付实践,一一展开讨论。
这里的讨论的不是同数据库实例副本之间同步,而是两个不同实例,甚至是不同架构的数据管理系统之间的数据复制问题。
1. 离线还是实时:不管黑猫白猫,捉住老鼠就是好猫
离线和实时是一个相对概念,没有绝对的离线,也没有绝对的实时。
我们在这里定义一下离线和实时,他们是站在数据复制目标端和业务需求端的角度来说的,如果数据目标端接收到数据的新鲜度超过一定阈值就是离线,反之就是实时。比如,对于一个业务系统,要求读到源端 5s 内的最新数据,离线和在线的阈值就是 5s。如果一个迁移软件,宣称自己是支持实时复制,但是由于某些原因,目标端的时延达到了 10s,那也不能叫实时复制。如果一个软件不是基于实时复制技术(比如事务日志),但能基于某些特殊的标记,能做到每批次的复制在 5s 以内的复制延迟,那他也是满足业务实时要求的。
数翊的灵渠软件,支持多种复制模式,离线大批量、准实时、实时复制三种模式,这三种模式是从技术角度出发的,越是实时的架构越复杂,我们给客户的建议是,不要过分关注技术,而是选择满足业务对数据新鲜度规格的模式。
2. 单向复制和双向复制:听起来很美好,做起来很烦恼
单向复制时,数据仅从源端复制到目标端。这是绝大部分数据复制的场景。双向复制的系统,两个系统互为源端和目标端。双向复制一般是在双活的场景下使用。比如两个数据库系统在两个 AZ 对等部署,应用跨 AZ 进行写操作,双向复制比单向复制引入了额外两个难题:
a) 循环复制
A 系统插入一条数据 R1,生成 RLog1,RLog1 复制到 B 系统,在 B 系统回放后,会相应地生成一条 R1 的日志 RLog1,这条日志又会被复制到 A 系统中回放,循环往复。解决这个问题有两种方式:
1) 日志标记:在日志中增加一条标识,以区分该条日志是自己产生,回放这条日志的系统,将不会把其再发送到目标端。这适合能自己自主把控内核的用户,比如 PostgreSQL 16,HexaDB 3.x。
2) 数据标记:在事务的开始额外插入一条写操作(可以不用修改数据,而仅使用系统记录日志),标识该事务是复制过来的,而不是自己产生的,目标端识别到该操作,忽略复制即可。这适合对业务能做相应改造的场景。
b) 数据冲突
1) 解决冲突最好的办法是预防冲突。业务在 A、B 系统中分流,可以根据某个特定 ID,使得 ID1 只在 A 系统更新,ID2 只在 B 系统更新。尽量避免两侧均对同一 ID 进行写操作。当然,我们可能会遇到自增序列作为 ID 的场景,这时我们可以采取在 A、B 两个系统中配置不同的自增序列规则来规避。预防冲突应当至少占到 99%的场景,否则后续的冲突解决和人工裁决将会消耗你大量的时间,甚至你都没有时间和你的朋友看电影了。
2) 当冲突不可避免时,也不要担心,我们至少还有两种办法解决:一是你的数据行中恰好有一个至少精确到毫秒(TPS < = 1000)的时间戳,回放时,让后来者胜利。二是采取领土争端解决机制,“搁置争议,共同开发”,即记录下来冲突的记录,由业务的同事去判断谁胜利,而你就可以去和朋友看电影了。
3. 物理复制和逻辑复制:通用和专用的问题
物理复制即复制物理的存储块,或者存储块的物理改变。逻辑复制时复制上层抽象的操作语言,我们常见的就是 SQL 语句或者明文数据。
物理复制一般只适用于严格同构(甚至同构系统的不同版本都有严格要求)的系统间的数据复制。逻辑复制则宽泛很多,因为 SQL 语言有标准的规范,明文数据可以二次转换。
我们大部分的异构系统的数据复制均采用逻辑复制。海纳数据库 HexaDB 这两种模式均支持,物理复制一般用在同构系统的容灾。
4. 异构类型的转换:像病毒一样影响后续各个阶段
和数据冲突的解决办法如出一辙,解决类型不一致的最好办法就是类型一致或者兼容,无需转换。当两个系统的数据类型不一致时,我们一般采用两种办法:
a) 等价转换。比如转字符形式存储。
b) 截断舍弃。比如数据精度舍弃、字符串截断。当然,我们可以采取记录异常的做法,以备后续排查。
5. Schema Change 还是 No Schema Change:为了改变而付出的代价
对于结构化数据的复制,我们不可避免会遇到数据结构的变化,因为是异步复制(同步复制一般用在数据副本 HA),在发生表结构变化后,我们可能已经无法找到对应的表结构描述了,这可能会导致目标端回放出错(比如新增、删除、变更列)。我们有两种策略来处理:
a) 采用内置的元数据注册管理器,根据日志中的 DDL 语句,注册管理不同版本的元数据,回放时根据元数据版本获取相应的结构描述。
b) 采用第三方的元数据管理系统,跟踪数据库系统元数据的版本,回放时根据元数据版本获取相应的结构描述。
两种策略的选择要根据业务系统来决定,海纳数据库 HexaDB 的灵渠软件目前采用第一种策略,以达到支持 DDL 的目的。
6. No-Schema 到 Schema 的转换:两个世界的桥梁
No-Schema 一般说的就是非关系型数据,Schema 指的关系型数据。现实中经常会有从非关系型数据复制到关系型数据的场景,比如 K-V 型的 LevelDB、Document 型的 MongoDB 数据复制到海纳关系型数据库 HexaDB-A。
最简单、我们最喜闻乐见的场景,是 No-Schema 数据的格式保持不变,这样我们就只要简单映射即可。但现实往往是残酷的,K-V 格式变幻莫测,Document 五花八门。这时,我们需要一个智能 Schema 检测器来帮忙,这个检测器完成如下工作:
a) 通过特殊的算法扫描并检测源数据的各种 Schema,然后组合成一个所有 Schema 的合集(扁平化处理):一张宽表,可以容忍所有的变化。或者建立符合它结构的层次表(层次化处理)。
b) 如果遇到算法的“失误”,采取一些措施应对:比如记录例外、改变目标表结构等。
7. Database 还是 非 Database:越来越像“集邮”
人类的世界是五彩缤纷的,数据的世界是纷繁复杂的。我们遇到的除开 Relation、K-V、Document 以外,我们还会遇到 FTP、syslog、Plain Text、webSocket、web page、Message Queue 等多种多样的数据来源,每一种都需要我们细心对待。不同协议、不同形式将会采用不同的复制策略,这些以后再一一展开。
1. 完整性:恪守 Once and Only Once
做且只做一次,这是确保完整性的基本原则。保证源端的每一条数据在目标端回放一次且只回放一次。回放一次的要求不言而喻,只回放一次在有主键或者唯一键约束的系统,可以通过约束保证。但是对于除此以外的数据结构,将会造成重复记录,而且这种重复检测比较困难。
如何确保 Once and Only Once,业界比较通用的做法是,记录位点,位点有很多表现形式,可以是日志或者消息的序列号,可以是数据行中某个特殊列。假设我们已经找到了这个位点,那么如何利用这个位点?
a) 如果系统有约束,我们可以使用异步的方式记录位点,故障恢复后,一来约束就帮我们排除重复数据,这可能会导致故障恢复时间变长。
b) 如果没有约束可以利用,我们就只能使用“组装位点事务”来解决,创建另外一张位点表,记录消费的位点,和业务数据的写入组装成一个事物,利用目标库事物的原子性来保证位点数据与业务数据的完整性。
如果以上两种做法你都没有时间实施,那我们提供给你另外一个选择,使用鲁班的重复数据检测处理,鲁班提供一个特性,帮助用户在海量数据中找出重复或疑似重复的数据行,提供多个参数可改进扫描的时间和精度。
2. 完整性校验的 N 种做法
可以从多个维度来评估数据是不是被完整迁移:
a) 数量。数据是粗粒度的评估指标,也是性能比较好的方式。但是对于超大数据集,产品对于精准的计数往往会采取不同的实现方式,最终也导致获取数据行数的代价不一样。一般产品也提供行数估算的手段,适合于对行数精准度要求不高的场景。
b) 内容。对内容完整性的校验,也分为精准和采样两种方式。精准的模式,可以遍历数据行,比对内容(比如获得全部列或者指定关键列的哈希特征,需要对不兼容的数据类型特殊处理),如果对精度要求不严苛,可以采用数据行采样,调整采样的比率,可以获得不同的精准度。
以上两种方式,均只对静态数据有效,对于动态增长的数据,我们就要采用动态比对的方式了,一种可行的做法是采用构造一棵 Merkle 树,此时可能需要一台额外的服务器来运行这个服务,灵渠目前就是采用这种做法,它支持动态获得两个系统数据的完整性比对结果,指出不一致的行。
3. 迁移工具要做 HA 吗
如果您的远端数据稍瞬即逝,并且数据对你至关重要,请考虑引入 HA 技术,反之,可以精简掉 HA 架构,选择重入或者全量重做。
1. 源端加速和目标端加速
整个数据复制链路中有三个环节,源端获取、中间缓冲、目标端回放,系统的提升主要在源端获取和目标端回放。主要有两种思路来提升性能:
a) 提升并行度
源端的并行度可以在多表间并行,也可以在单表内并行。多表并行很好理解,更多地是针对存量数据复制的场景,启动多个任务同时获取源端不同的表数据。单表内并行则依靠特定的数据结构,根据主键或者唯一键进行分区,然后启动多个任务分块读取。如果源端同时有业务活动,还需要考虑新数据的一致性,通常需要源端支持快照读,并获得一个快照位点,存量数据读取完毕后,增量数据从该位点继续。
b) SQL 还是 SQL bypass
这里主要针对目标端。前面讨论的都是基于 SQL 的方式(不同接口:statement、copy 等)回放数据,这里使用了一种更加高级的方式,跳过 SQL 层(解析、计划生成、执行等步骤),直接写数据库文件,这通常能收获极高的性能,但是对于目标端的读写会有限制。
2. 修一条中国高速还是德国高速
a) 限速还是不限速
通常,我们会认为,存量数据的复制越快越好,因为,你不想错过今天晚上的电影。但是在现实中往往事与愿违,读得越快,对源端系统 IO 和 CPU 的冲击就越高,这往往会影响源端的活跃业务。你的客户是断然不会同意的,那么,我们的迁移就需要控制我们的欲望,限速是需要的,比如:控制并发,控制一次读取的多少,控制两次读取的时间间隔。
b) 最好能做到自适应:无人值守
上面提到,源端存在活跃的业务,但是有些业务并不是 24 小时活跃的,在业务低谷期加大数据复制的速度,何乐而不为呢。灵渠将采取的做法是,检测源端资源的繁忙程度,动态、自主调整获取的数据量。直到达到我们设定的占用资源的阈值。
1. 性能与一致性的矛盾
一致性一直是业界比较严肃的话题,因为不谈这个,有些程序员会试图把跑车加装翅膀。对于严肃的系统,我们要采取严肃的做法。
把一致性分成多个方面来考虑:
a) 表内一致性
有时候,我们为了做到表内一致性,会将表上的数据分区并行写入,比如前面提到的表内并行,或者将表上的不同操作并行处理,比如 INSERT、UPDATE、DELETE 并行处理。这些操作都要有严格的场景约束,不可一概而论行还是不行。
b) 表间一致性
多表之间的数据复制并行进行。这在目标端没有读操作,或者没有多表一致性要求时没有问题,除此以外的场景,需要数据复制系统做特别的处理。
c) 最终一致性和严格一致性
上面提到的表内分区写入和表间并行,不做特殊处理的情况下,均只能满足最终一致性。对于严格一致性要求的场景,灵渠采取了一种行之有效的策略,提供一个“最大并行区间裁决”机制,该机制将找到最大可能的满足数据严格一致性要求的记录区间,该区间内可并行。通过该机制,在满足严格一致性要求的前提下,将收获相比最终一致性 80%的性能。
1. 噩梦从下发迁移命令开始
前面简要定义了数据复制过程的几个关键问题,这些问题您可能不会全部遇到,但如果您在敲下“Start Replication”这个命令的时候,如果您不预先考虑并一一排查这些问题,您或者您的客户将会面临不少困扰。
a) 复制开始时:源端数据库无法给你提供足够高权限的用户
b) 复制过程中:源端系统限流,网络抖动导致错误频发,目标端性能不足中间缓冲层拥塞等等。
c) 复制尾声:客户发现 N 条记录在目标端丢失,客户发现目标端多出 N 条记录等等,限你在 10 分钟内给出 N 的列表,并补齐数据…
2. 如何做个好梦
这只是开始,后续将会有系列文章来和大家分享更多细节,下面是小六在交付和落地数据复制过程中的一点小心得,和大家分享:
a) 制定完整的迁移流程,并自动化,让我们在客户、老板面前不会惊慌失措、手忙脚乱。
b) 做好断点续作,并核实一致性保障逻辑。
c) 准备好快速而精准的数据一致性检查手段。
d) 复制工具记录好所有 checkpoint,“啰嗦”的日志好过客户的“啰嗦”。