逻辑复制概念
逻辑复制是 PG 数据库基于数据对象的复制标志,也就是 Replica Identity 来复制数据对象及数据变化的方法。它最初是在 2014 年 12 月份的 PG9.4.0 引入,称为逻辑解码,后来在 2017 年 10 月 PG10.0 版本中又引入了基于发布订阅的逻辑复制概念。之所以称为逻辑复制是为了和更早引入的物理复制加以区分。逻辑复制规律和物理复制这两种复制机制有比较大的区别,物理复制是使用准确的块地质遗迹逐字节的复制方式,而逻辑复制则允许在数据层面复制,并在安全性上提供更细粒度的控制。
逻辑复制功能演进大致分为两个比较大的阶段,第一阶段是 2014 年 12 月份,随着 9.4.0 版本发布,PG 为 DML 带来数据修改流动到外部提供基础设施,用户可以基于它实现复制解决方案和审计等目的。数据修改以流的形式发出,通过逻辑复制槽 Replication Slot 来识别,配合解码插件 Decoding Plugin,实现每一份数据修改在每一个流中输出。
同时 PG 基于 WAL 日志作为原始输出,Slot 保证数据流的唯一性,Decoder 插件赋予用户自定义输出能力,来做到将 PG 实例的数据变化流动到外部。第二阶段是 2017 年 10 月 4.0 版本的发布,PG 逻辑复制提供了发布和订阅模型,内部和订阅可以是多对多关系,例如允许有一个或多个订阅者来订阅同一个实例上多个发布。订阅者与发布者相同顺序,应用数据变更实现在同一个订阅中保证发布事务的一致性,这种数据复制方式有时候被称为事务性复制。
数据迁移的应用场景及宏观的技术架构
下面介绍迁移的应用场景跟宏观的技术架构。数据迁移产品对云上的数据库,公有云数据库是一个避不开的话题,也是用户数据上云的一个入口。云上数据迁移产品能够帮客户在应用不停服前提下轻松完成数据迁移上云,利用实时同步通道轻松构建高可用数据库容灾架构,通过数据订阅来满足商业数据挖掘、应用异步解耦等场景需求。
多个来源数据库要经过迁移产品将数据转移到云上,包括 IDC 自建、其他云厂商、腾讯云自己的数据库及 CVM 自建等。为了稳定可靠方便地实现用户迁云同步订阅需求,我们定下了一致性、高性能、高可用的架构设计目标,在数据库类型上有多款数据库,不仅要实现同构迁移,还要实现异构数据库迁移,这是一个整体的数据迁移应用场景。
插入 PPT 图片
迁移场景总的技术架构,首先产品要具备向用户提供 API 和控制页面能力,就像这张图的最上层,其次是通过插件式的热加载,实现不同链路迁移应用快速接入,比方刚刚说的既有同构,也有异构。同构有多种数据库类型,异构同样有多种数据库类型,而且是 N×N 的链路组合。
看一下中间部分,任务执行平台,它不仅能够提供任务调度能力,还将组件高可用、监控告警、任务编排、运维系统等纳入其中,做到了对前台高可用高可靠,对后台好运维、好接入的目标。图正下方这个区域,Node Server 是具体执行迁移节点,可以把它看成是干事,具体干迁移这件事情的一个节点或进程,它不仅能够快速扩容,还能够动态负载均衡,完成全量和增量、并列、同步等任务。这个技术架构支持了腾讯云所有数据库类型的迁移应用。
接下来回到 PG 逻辑复制,重点介绍逻辑复制内部的概念和原理。首先是比较基础但又比较重要的概念,复制标识是一个感知属性,它是用来控制表中记录被更新或改删除时写入 WAL 的内容,这个属性它只用于逻辑复制,并只可以通过 ALTER LABLE 来进行修改。同时需要注意的是,表中记录至少有一个列值发生变化了才会产生修改记录。感知属性有四个可感知,分别是 DEFAUTL、USINGINDEX、FULL、NOTHING,其中 FULL 和 NOTHING 这两个值比较好理解,FULL 是指当记录变更时写入 WAL 日志的内容将包括所有字段原始值和所有字段的旧值。
例如表 T 它有三个字段 ABC,那么它记录了三个字段的原始值分别是 123,当这条记录发生变更时,PG 会把这个变更记录写到 WAL 日志内容,把 A=1、B=2、C=3 都放进去,也就是说让这个内容的消费者,不管是开发者还是应用程序,都能够得到这几个字段的旧值与原始值,NOTHING 是不计入任何旧值。
但是 DEFAUTL 和 USINGINDEX 这两个放在一起理解,DEFAUTL 就是把组件涉及到的字段放在 WAL 日志内容中,比方说 T 表有 ABC 三个字段,如果 A 字段是主键,那么 DEFAUTL 会只把 A 主键 A 字段组件涉及到的字段放到 WAL 日志内容中。USINGINDEX 比较特殊,它是记录索引相关列的旧值,但是它要求索引必须是唯一线索引,必须是部分索引,也不能是延期索引,并且其中也不能包括空列。另外它是唯一键,不能为空,这可能是必须常见的 case,这时它允许用户指定,比如说指定哪个索引它就会用到哪个索引相关字段,这样相关字段旧值输入到 WAL 日志内容中。
那么如何来读取和消费这些 WAL 内容?如果把 WAL 视为一个仓库,每个复制槽 Replication Slot 则可以视为这个仓库诸多窗户。通过窗户可以读取仓库里面的内容,也就是数据变更。同时复制槽也记录着消费位点,可以保证每个数据变化它是被唯一地消费,并且是有序的。
多个复制槽之间相互位点是独立记录的,不能相互干扰,同时消费输出的解码插件也是独立的,不同消费者可以在同一个 WAL 日志上通过申请不同的复制槽得到自己想要的数据内容,这个是复制槽的概念。
插入 PPT 图片
在迁移中除了消费 WAL 日志得到数据变更内容外还有更普遍表的存量数据有待读取消费,在很多场景中存量数据的体量甚至要远远大于数据变更的体量,这时就要用到快照读办法。当两个或者更多会话需要查看数据库中相同内容时,PG 允许一个事务导出它正在使用的快照,只要事务保持打开,其他事务就可以导入这个快照,来保证这些事务看到的数据和第一个会话中事务看到的完全一致。
创建快照有两种方式,第一种是利用 CREATE_REPLICATION_SLOT 命令,在创建复制槽时自动创建快照,并保证事务一致性。第二个是使用系统函数 pg_export_snapshot()来导出快照。使用这两种方式都可以得到一个快照标志,当事务接入前,在其他事务中导入快照便能看到与当前事务完全相同的数据。
在能够实现复制槽快照之间事务一致性后,我们知道复制槽可以消费数据变更,也就是增量数据,快照可以读取存量数据,保证二者之间的事务一致性,那么我们可以去可靠地开启存量和数据读取了。前面所提到,通过 CREATE_REPLICATION_SLOT 来实现复制槽和快照同时创建,需要两个连接会话。如图所示,打开一个 HelperConn,利用它来开启事务,创建复制槽和快照,记录下复制槽和快照的标志,然后打开第二个会话,在这个会话里开启事务,导入刚刚的快照标志,这样就能够实现增量数据和存量数据的读取和传输。通过两个会话我们就可以把一种同时读取和传输的方式启动起来。
插入 PPT 图片
有了刚创建好的复制槽,接下来应该去关注如何将原始复制槽中读取的内容变更到目标实例。前面有提到 PG 逻辑复制是以 Decoder 插件赋予用户自定义输出格式的能力,那么这个插件到底如何去自定义?PG 内置一个非常简单的插件实现,代码在 contrib 目录下,叫 test_decoding,它的实现逻辑仅仅对数据变更值做简单打印,但是整体比较丰富,插件实现就是为 PG_output_plugin_init 函数提供回调实现,但是在 test_decoding 实现的基础上做修改,我们可以将结构和数字拼接成可以执行的四个五句,便于我们直接执行来进行回放。
接下来结束一下发布订阅基本原理,发布订阅前有提到是在 2017 年 10 月份在 10.0 版本中引入,将 SLOT 消费和回放,包括解码插件进行了封装,让用户免于操作复制槽、快照读、解码插件等一系列底层功能,方便获得稳定的全量加增量的数据迁移通道。
用户只需简单在云端创建发布,在目标单创建订阅,并保证目标单到云端的网络连通性,就可以轻松实现一个迁移链路。但需要注意几点,第一,发布时如果要整个库下的所有表,使用 FOR ALL TABLES 语法必须要用 superuser 权限。第二点,对于无主键表,如果使用默认的 DEFAUTL 复制标志,会导致源表的使用产生问题。第三点,受限于版本,版本 10 只支持 Insert、Delete、Update 三种语句,而版本 11 增加了对 Truncate 语句支持。第四,DDL 变更无法被自动执行。第五,序列无法自动复制,只能依靠应用自行迁移的。最后是发布订阅只支持字段计算表,对于分区表、外表以及大对象都是不支持的,实际使用中要更加注意。
解决方案
下面一起通过真实场景案例来看看我们遇到的问题以及我们的解决方案。首先看网络连通性问题,在前面多次提到,云上数据库为了保证安全性一定不会轻易放开实例对外部网络的访问,当然不同网络架构下可能放开程度有所不同,这里只是强调在逻辑复制的应用场景上,云上实例对于公网地址一般不允许访问。这样在外部公网实例上云时,基于逻辑复制,首先就会遇到网络连通性问题。
9.4 版本中逻辑复制对于源实例从公网进入内网要用到网络代理,有了代理后问题就会转化成内网实例间的连接,这时迁移节点 Node Server 会先进行存量数据的远程传输,然后用到中间存储 COS 服务,这是腾讯云的存储服务,来进行增量数据的消费和回放。这个过程中,数据一直被迁移节点主动推动到目标实例来,结构比较简单,就是 Node Server 主动拿数据,然后把数据推到目标实例。
10 以上版本用到的是发布订阅场景,有一个目标实例反向拉取源实例的数据链路,这时迁移节点 Node Server 完全变成控制节点,来控制发布订阅的创建、启动、删除等生命周期,在发布订阅生效期间目标实例通过网络代理直接拉去源实例数据构成数据链路。
再看一个问题,对于无主键表的增量迁移。前面提到一张表的复制标志控制这张表的数据修改,删除时写入 WAL 内容,此时对无主键表这一种特殊类型表进行分析,可以得知如果不修改复制标志,也就是保持默认 DEFAUTL 标志,万一没有主键将无法输出数据变更到 WAL 日志内容中。如果将复制标志设为 FULL 会怎么样呢?它无法保证数据的一致性。我们在图中放了一个例子。假如在源实例侧有一张无主键表,名叫 TB,它里面有两个字段分别是 id 和 name,可以看右边的 SQL 语句。有三条记录,在存量迁移中这三条记录都已经被迁移过去了。然后对源表进行数据修改,删除其中一条记录,注意是以 ctid 的方式来删除。ctid 是什么?它是数据行所处的表内物理位置,而且会随着数据行在快内物理位置移动而变化,无法被逻辑复制标志所识别。在目标端,由于使用了 FULL 复制标志,我们知道它修改之前的值。可以得到源端 id 等于 1、name 等于 Alice 的记录,利用这个条件我们来做回放,就会一次性地删除三项记录。
可以看到 SQL 语句源实例只删除了一条,目标实例删除了多条,这样不一致性就出现了。这是非常底层的问题,以 ctid 物理位置进行数据修改的方式虽然不推进,但是在实际场景中却无法做出限制。这个方式是可行的,就一定有可能有用户这么做。我们能做的就是在迁移预检查的过程中提示用户去修改表结构,遵循合理的数据库设计规范,无主键表不是一个合理的设计实践,同时再将可能出现数据不一致的风险暴露出来,提示给用户。
接下来第三个问题,如何实现 DDL 自动同步。我们知道逻辑复制包括发布订阅都无法实现自动同步 DDL 变更,在实际场景中,特别是在产品无法实现而我们又避免不了在迁移过程中对结构进行变更。一个最常用的场景就是很多数据分析应用场景中有分区表或者计程表,它们的创建和删除动作会不断去创建新的分区和删除老的分区场景。就可以通过其他方式,通过应用上的方式来实现手动或半自动的 DDL 自动同步。
第一种方式是什么?迁移应用自动探测源端和目标端的结构差异,生成差异 DDL 变更至目标。我们定时去做结构对比,对比出来差异 DDL 我们再默认做一下执行,让它的结构变得更源端是一样的。这样做的优点是什么?不需要什么特殊权限,只要你使用迁移过程中的 superuser 权限就可以,也不需要安装额外插件,因为做结构对比逻辑都是迁移应用来实现。缺点是什么呢?它无法保证数据的一致性,事务一致性,只能够应用在比较简单的场景。无法保证数据的事务一致性。举个例子,对比源端和目标端结构时,源端结构变更变化已经发生了一段时间,不可能说及时的实时去探测。在一段时间后在这个时间区间内源端发生后到你探测到之前这段时间,如果发生了数据变更,这时你是获取不到的,要么是获取到但是你无法变更的目标,因为目标没有这个结构或结构不一致而无法去回放。
插入 PPT 图片
第二种方法是使用事件触发器来追踪表结构变更,并记录到一张特殊的表。事件触发器是 PG 里的机制,它需要一些操作权限,能做到一个库里的表发生变化,通过触发器的形式去跟踪记录。优点是可初步保证事务一致性,因为一直在跟踪它,那么它发生变化了就会第一时间,只要变更事务提交了它就会被监测到,然后写到一张特殊表里。如果把这个特殊的表也放到一个逻辑复制里去追踪它的增量日志,那么它一定和其他的变更顺序是一致的,因为 SLOT 保证了数据变更顺序是唯一的且有序的。优点是可初步保证这个缺点是它需要 superuser 权限,这个权限一般来讲是社区版是操作不了的。
第三个方法是安装外部插件来解析 WAL,在 Slot 中添加 DDL 相关信息,原始逻辑复制变更到目标。它的优点是可以保证事务一致性,因为使用原生的逻辑复制机制,但是缺点是目前这些插件都是第三方不是社区原生,另外它需要源实例的运维和插件安装权限。
遇到的挑战
接下来和大家分享在云上做 PG 迁移应用会有哪些挑战如何去应对。我们从分安全性、碎片化和社区跟随这三个方面来看,首先是安全性,随着云上数据库技术的不断发展演进,各厂商都锤炼出了针对自身定制的网络安全规则和数据库用户权限体系,相较于传统迁移场景的完全复制,我们需要按源和目标各自的安全规则,来定制迁移策略。例如在不同厂商之间迁移,数据库账号权限点不尽相同,考虑到各个厂商的安全策略,为了顺利完成迁移,就需要对账号权限点做适当裁剪。这个是由于各种安全规则所导致,这是在迁移应用中要去应对的第一个挑战。
第二个挑战是碎片化,PG 是一个开源、商业友好的数据库,基于 PG 的数据库发行厂商也非常多,在内核架构上有着大大小小的差异性。为了提供这些对各个发行厂商更好的服务,我们必须持续地针对各个发行厂商的产品做适配,给用户带来更好的使用体验。
最后是社区跟随,PG 有一个强大的社区,在社区版本迭代中,会有持续不断的特性变更和演进。以迁移场景为例,社区 11 版本删除了 abstime、reltime、tinterval 这三种数据类型,逻辑复制又增加了对 truncate 的支持,因此我们在做 11 版本之前,迁移到 11 及以后的版本时,就要针对这种变化去做适配和支持。再举个例子,社区 13 版本中的逻辑复制增加了对分级表附表的支持,因此版本间的迁移链路也需要随之不断迭代。后续我们将继续跟进社区和云数据库自身内核演进一起,做更好的数据库迁移解决方案。