什么是逻辑复制

我们前面讲的都是基于物理的流复制,接下来我们来讲讲逻辑复制。pg 逻辑复制基于逻辑解析,将 wal 日志流解析成一定格式输出,从节点收到解析后的数据进行应用。

逻辑复制基于发布(Publisher)与订阅(Subscription)模型。

  • 一个发布者可以有多个发布,一个订阅者上可以有多哥订阅。

  • 一个发布可被多个订阅者订阅,一个订阅只能订阅一个发布者,但可订阅同发布者上的多个不同发布。

逻辑复制的典型用途是:

  • 迁移,跨PostgreSQL大版本,跨操作系统平台进行复制。

  • CDC,收集数据库(或数据库的一个子集)中的增量变更,在订阅者上为增量变更触发触发器执行定制逻辑。

  • 分拆,将多个数据库集成为一个,或者将一个数据库拆分为多个,进行精细的分拆集成与访问控制。

逻辑复制不同于流复制(物理复制)基于实例级别主从库物理结构上就是一样,逻辑复制可以基于表级别选择性复制。

逻辑复制(Logical Replication)在官方文档里专指”复制-订阅“模式,其实有许多工具可以基于逻辑解析做异构数据库间数据同步。

pg9.4 pglogical插件可以支持逻辑复制(https://github.com/2ndQuadrant/pglogical),pg10开始原生支持逻辑复制。

复制标识

一个被纳入发布中的表,必须带有 复制标识(Replica Identity),只有这样才可以在订阅者一侧定位到需要更新的行,完成UPDATEDELETE操作的复制。

默认情况下,主键 (Primary Key)是表的复制标识,非空列上的唯一索引 (UNIQUE NOT NULL)也可以用作复制标识。如果这两个都没有的话,可以把复制标识设置为 FULL,也就是把整个行当作复制表示,效率很低,每一行修改都需要在订阅者上执行全表扫描,很容易把订阅者拖垮。

INSERT操作总是可以无视 复制标识 直接进行(因为插入一条新记录,在订阅者上并不需要定位任何现有记录;而删除和更新则需要通过复制标识 定位到需要操作的记录)。如果一个没有 复制标识 的表被加入到带有UPDATEDELETE的发布中,后续的UPDATEDELETE会导致发布者上报错。

表的复制标识模式可以查阅pg_class.relreplident获取,可以通过ALTER TABLE进行修改。

ALTER TABLE tbl REPLICA IDENTITY 
{ DEFAULT | USING INDEX index_name | FULL | NOTHING };

发布

一个 发布(Publication) 可以在物理复制主库 上定义。创建发布的节点被称为 发布者(Publisher)

一个 发布由一组表构成的变更集合。也可以被视作一个 变更集(change set)复制集(Replication Set) 。每个发布都只能在一个 数据库(Database) 中存在。

发布不同于模式(Schema),不会影响表的访问方式。(表纳不纳入发布,自身访问不受影响)

发布目前只能包含(即:索引,序列号,物化视图这些不会被发布),每个表可以添加到多个发布中。

除非针对ALL TABLES创建发布,否则发布中的对象(表)只能(通过ALTER PUBLICATION ADD TABLE)被显式添加

发布可以筛选所需的变更类型:包括INSERTUPDATEDELETETRUNCATE的任意组合,类似触发器事件,默认所有变更都会被发布。

CREATE PUBLICATION用于创建发布,DROP PUBLICATION用于移除发布,ALTER PUBLICATION用于修改发布。

创建发布

发布创建之后,可以通过ALTER PUBLICATION动态地向发布中添加或移除表,这些操作都是事务性的。

CREATE PUBLICATION name
    [ FOR TABLE [ ONLY ] table_name [ * ] [, ...]
      | FOR ALL TABLES ]
    [ WITH ( publication_parameter [= value] [, ... ] ) ]

ALTER PUBLICATION name ADD TABLE [ ONLY ] table_name [ * ] [, ...]
ALTER PUBLICATION name SET TABLE [ ONLY ] table_name [ * ] [, ...]
ALTER PUBLICATION name DROP TABLE [ ONLY ] table_name [ * ] [, ...]
ALTER PUBLICATION name SET ( publication_parameter [= value] [, ... ] )
ALTER PUBLICATION name OWNER TO { new_owner | CURRENT_USER | SESSION_USER }
ALTER PUBLICATION name RENAME TO new_name

DROP PUBLICATION [ IF EXISTS ] name [, ...];
#创建一个发布,发布两个表中所有更改。
CREATE PUBLICATION mypublication FOR TABLE users, departments;
#创建一个发布,发布所有表中的所有更改。
CREATE PUBLICATION alltables FOR ALL TABLES;
#创建一个发布,只发布一个表中的INSERT操作。
CREATE PUBLICATION insert_only FOR TABLE mydata WITH (publish = 'insert');
#修改发布的动作。
ALTER PUBLICATION insert_only SET (publish='insert,update,delete');
#向发布中添加表。
ALTER PUBLICATION insert_only ADD TABLE mydata2;
#删除发布。
DROP PUBLICATION insert_only;

publication_parameter 主要包括两个选项:

  • publish:定义要发布的变更操作类型,逗号分隔的字符串,默认为insert, update, delete, truncate

  • publish_via_partition_root:13后的新选项,如果为真,分区表将使用根分区的复制标识进行逻辑复制。

订阅

订阅(Subscription) 是逻辑复制的下游。定义订阅的节点被称为 订阅者(Subscriber)

订阅定义了:如何连接到另一个数据库,以及需要订阅目标发布者上的哪些发布

逻辑订阅者的行为与一个普通的PostgreSQL实例(主库)无异,逻辑订阅者也可以创建自己的发布,拥有自己的订阅者。

每个订阅者,都会通过一个 复制槽(Replication) 来接收变更,在初始数据复制阶段,可能会需要更多的临时复制槽。

逻辑复制订阅可以作为同步复制的备库,备库的名字默认就是订阅的名字,也可以通过在连接信息中设置application_name来使用别的名字。

只有超级用户才可以用pg_dump转储订阅的定义,因为只有超级用户才可以访问pg_subscription视图,普通用户尝试转储时会跳过并打印警告信息。

逻辑复制不会复制DDL变更,因此发布集中的表必须已经存在于订阅端上。只有普通表上的变更会被复制,视图、物化视图、序列号,索引这些都不会被复制。

发布与订阅端的表是通过完整限定名(如public.table)进行匹配的,不支持把变更复制到一个名称不同的表上。

发布与订阅端的表的列也是通过名称匹配的。列的顺序无关紧要,数据类型也不一定非得一致,只要两个列的文本表示兼容即可,即数据的文本表示可以转换为目标列的类型。订阅端的表可以包含有发布端没有的列,这些新列都会使用默认值填充。

pg_publication_tables是由pg_publicationpg_classpg_namespace拼合而成的视图,记录了发布中包含的表信息。

管理订阅

CREATE SUBSCRIPTION用于创建订阅,DROP SUBSCRIPTION用于移除订阅,ALTER SUBSCRIPTION用于修改订阅。

订阅创建之后,可以通过ALTER SUBSCRIPTION 随时暂停恢复订阅。

移除并重建订阅会导致同步信息丢失,这意味着相关数据需要重新进行同步。

CREATE SUBSCRIPTION subscription_name
    CONNECTION 'conninfo'
    PUBLICATION publication_name [, ...]
    [ WITH ( subscription_parameter [= value] [, ... ] ) ]

ALTER SUBSCRIPTION name CONNECTION 'conninfo'
ALTER SUBSCRIPTION name SET PUBLICATION publication_name [, ...] [ WITH ( set_publication_option [= value] [, ... ] ) ]
ALTER SUBSCRIPTION name REFRESH PUBLICATION [ WITH ( refresh_option [= value] [, ... ] ) ]
ALTER SUBSCRIPTION name ENABLE
ALTER SUBSCRIPTION name DISABLE
ALTER SUBSCRIPTION name SET ( subscription_parameter [= value] [, ... ] )
ALTER SUBSCRIPTION name OWNER TO { new_owner | CURRENT_USER | SESSION_USER }
ALTER SUBSCRIPTION name RENAME TO new_name

DROP SUBSCRIPTION [ IF EXISTS ] name;
#创建一个到远程服务器的订阅,复制发布mypublication和insert_only中的表,并在提交时立即开始复制。
CREATE SUBSCRIPTION mysub
         CONNECTION 'host=192.168.1.50 port=5432 user=foo dbname=foodb password=xxxx'
        PUBLICATION mypublication, insert_only;
#创建一个到远程服务器的订阅,复制insert_only发布中的表, 并且不开始复制直到稍后启用复制。
CREATE SUBSCRIPTION mysub
         CONNECTION 'host=192.168.1.50 port=5432 user=foo dbname=foodb password=xxxx '
        PUBLICATION insert_only
               WITH (enabled = false);
#修改订阅的连接信息。
ALTER SUBSCRIPTION mysub CONNECTION 'host=192.168.1.51 port=5432 user=foo dbname=foodb password=xxxx';
#激活订阅。
ALTER SUBSCRIPTION mysub SET(enabled=true);
#删除订阅。
DROP SUBSCRIPTION mysub;

subscription_parameter定义了订阅的一些选项,包括:

  • copy_data(bool):复制开始后,是否拷贝数据,默认为真

  • create_slot(bool):是否在发布者上创建复制槽,默认为真

  • enabled(bool):是否启用该订阅,默认为真

  • connect(bool):是否尝试连接到发布者,默认为真,置为假会把上面几个选项强制设置为假。

  • synchronous_commit(bool):是否启用同步提交,向主库上报自己的进度信息。

  • slot_name:订阅所关联的复制槽名称,设置为空会取消订阅与复制槽的关联。

管理复制槽

每个活跃的订阅都会通过复制槽 从远程发布者接受变更。

通常这个远端的复制槽是自动管理的,在CREATE SUBSCRIPTION时自动创建,在DROP SUBSCRIPTION时自动删除。

在特定场景下,可能需要分别操作订阅与底层的复制槽:

  • 创建订阅时,所需的复制槽已经存在。则可以通过create_slot = false关联已有复制槽。

  • 创建订阅时,远端不可达或状态不明朗,则可以通过connect = false不访问远程主机,pg_dump就是这么做的。这种情况下,您必须在远端手工创建复制槽后,才能在本地启用该订阅。

  • 移除订阅时,需要保留复制槽。这种情况通常是订阅者要搬到另一台机器上去,希望在那里重新开始订阅。这种情况下需要先通过ALTER SUBSCRIPTION解除订阅与复制槽点关联

  • 移除订阅时,远端不可达。这种情况下,需要在删除订阅之前使用ALTER SUBSCRIPTION解除复制槽与订阅的关联。

如果远端实例不再使用那么没事,然而如果远端实例只是暂时不可达,那就应该手动删除其上的复制槽;否则它将继续保留WAL,并可能导致磁盘撑爆。

复制冲突

逻辑复制的行为类似于正常的DML操作,即使数据在用户节点上的本地发生了变化,数据也会被更新。如果复制来的数据违反了任何约束,复制就会停止,这种现象被称为 冲突(Conflict)

当复制UPDATEDELETE操作时,缺失数据(即要更新/删除的数据已经不存在)不会产生冲突,此类操作直接跳过。

冲突会导致错误,并中止逻辑复制,逻辑复制管理进程会以5秒为间隔不断重试。冲突不会阻塞订阅端对复制集中表上的SQL。关于冲突的细节可以在用户的服务器日志中找到,冲突必须由用户手动解决

参考链接:管理 | Pigsty