跳至内容

逻辑复制

[TOC]


1. 概述

逻辑复制是一种基于数据对象的复制标识(通常是主键)复制数据对象及其更改的方法。我们使用术语“逻辑”来与物理复制加以区分,后者使用准确的块地址以及逐字节的复制方式。PostgreSQL两种机制都支持。逻辑复制允许在数据复制和安全性上更细粒度的控制。

逻辑复制的典型用法是:

  • 在一个数据库或者一个数据库的子集中发生更改时,把增量的改变发送给订阅者。
  • 在更改到达订阅者时引发触发器。
  • 把多个数据库联合到单一数据库中(例如用于分析目的)。
  • 在PostgreSQL的不同主版本之间进行复制。
  • 在不同平台上(例如Linux到Windows)的PostgreSQL实例之间进行复制。
  • 将复制数据的访问给予不同的用户组。
  • 在多个数据库间共享数据库的一个子集。

当订阅者被应用当作只读时,单一的订阅中不会有冲突。在另一方面,如果应用或者对相同表集合的订阅者执行了其他的写动作,冲突可能会发生。

2. 细节

在PostgreSQL 10版本中新增了一个逻辑复制的特性,逻辑复制是基于逻辑解析(Logical Decoding)进行复制。它使用类似消息队列的发布者、订阅者的模型,利用复制槽的技术,发布者将wal日志解析成一定格式的数据流,订阅者对解析后的wal日志数据流进行回放应用,从而实现数据的同步。但是需要注意的是,PostgreSQL的逻辑复制不是“SQL”复制,而是复制SQL操作的结果

2.1 逻辑复制架构

img

logical decoding是实现所有logical功能的核心技术,下面有对它的详细解释。跟这个核心技术一起加入PG内核的还有一个demo级别的功能test_decoding,但是test_decoding的输出结果看起来没有那么容易被二次开发使用,wal2json以一个 contrib 的形式出现,它的输出结果对二次开发更加友好。BDR和pglogical是实现了逻辑复制的插件,BDR特点是支持多主机、DDL复制、全局序列、全局DDL锁,pglogical特点是灵活性高、支持级联逻辑复制,在他们的基础上PG内核引入了logical replication,它实现了逻辑复制的功能,从使用方法来看logical replication与pglogical更为一致。

2.2 核心 logical decoding

img

walsender进程不断的从自己的复制槽中获取新产生的wal record,并通过内核中的LogicalDecodingProcessRecord()函数进行wal record的初步过滤和解析,解析结果为一个ReorderBufferChange结构的数据(对于DML语句而言这个结构里面主要的信息为oldtuple和newtuple),并将这个解析结果放置到当前事务ID对应的解析 buf 中去。当某个事务发生了提交,这个事务对应的buf里面的ReorderBufferChange会被指定的plugin做二次处理,并将处理结果按照指定的途径输出。

2.2 逻辑同步的过程

  1. 逻辑同步接收部分。是逻辑同步的接收端,接收 walsender 进程发送的报文进行解析。接收到的包,如果以 ‘k’ 开头,是保活通信包;如果是以 ‘w‘ 开头是 wal 日志包。包结构为:

    messagetype[1]+datastart[8]+walEnd[8]+sendtime[8]+message
  2. walsender 部分。是以进程的形式向逻辑同步接收端提供服务。负责后台各部分调度和接收端的通信。

  3. slot 复制槽部分。对于解析 wal 日志的事务控制,保证事务发送的完整性。有以下几种操作,创建复制槽(逻辑解码动态库-插件的名称);删除复制槽;切换到下一事务;重新开始;从某个指宣的 lsn 开始。

  4. 逻辑解码部分。这部分将 wal 日志解析为各种形式,比如说标准 sql,倒排 sql 和其他需要方式。比如开源的有test_decoding 和 ali_decoding。pg 源码中也有一个 pgoutput 的默认格式。除了解析SQL外还在这块部分里添加发送包里的信息,比如说 xid(事务号),commit_lsn(commit的lsn)等。

  5. wal 文件解析部分。这个部分是逻辑同步的最核心的部分,最主要的功能就是将 wal 日志以事务为单位进行整理。一个数据库集簇中的 wal 日志同时只有一份,在多并发和长事务的的操作下,wal 日志会出现交叉混打,wal 日志的事务的 lsn 不是顺序的。如果没有这个部分,基本上是不可读的。

3. 发布

发布可以被定义在任何物理复制的主服务器上。定义有发布的节点被称为发布者。发布是从一个表或者一组表生成的改变的集合,也可以被描述为更改集合或者复制集合每个发布都只存在于一个数据库中。

发布与模式不同,不会影响表的访问方式。如果需要,每个表都可以被加入到多个发布。当前,发布只能包含表。对象必须被明确地加入到发布,除非发布是用ALL TABLES创建的。

Publication可以选择把它们产生的更改限制为INSERTUPDATEDELETE以及TRUNCATE的任意组合,类似于触发器如何被特定事件类型触发的方式。默认情况下,所有操作类型都会被复制。

3.1 复制标识

为了能够复制UPDATEDELETE操作,被发布的表必须配置有一个“复制标识”,这样在订阅者那一端才能标识对于更新或删除合适的行。默认情况下,复制标识就是主键(如果有主键)

也可以在复制标识上设置另一个唯一索引(有特定的额外要求)。如果表没有合适的键,那么可以设置成复制标识“full”,它表示整个行都成为那个键。不过,这样做效率很低,只有在没有其他方案的情况下才应该使用。如果在发布者端设置了“full”之外的复制标识,在订阅者端也必须设置一个复制标识,它应该由相同的或者少一些的列组成。

设置复制标识:

alter table table_name REPLICA IDENTITY default / full / nothing / using

这种形式更改被写入到预写式日志来标识被更新或删除行的信息。除非使用逻辑复制, 这个选项将不会产生效果。

  • DEFAULT:(非系统表的默认值)记录主键列 (如果有)的旧值。
  • USING INDEX:记录被所提到的索引所覆盖的列的旧值,该索引必须是唯一索引、不是部分索引、不是可延迟索引并且只包括被标记成 NOT NULL的列。
  • FULL: 记录行中所有列的旧值。
  • NOTHING:不记录有关旧行的任何信息(这是系统表的默认值)。在所有情况下,除非至少有一个要被记录的列在新旧行版本之间发生变化,将不记录旧值。

如果在复制UPDATEDELETE操作的发布中加入了没有复制标识的表,那么订阅者上后续的UPDATEDELETE操作将导致错误。不管有没有复制标识,INSERT操作都能继续下去。

3.2 创建发布

CREATE PUBLICATION向当前数据库添加一个新的发布。 发布的名称必须与当前数据库中任何现有发布的名称不同。

发布本质上是一组表,其数据更改旨在通过逻辑复制进行复制。

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

参数:

  • name:新发布的名称。

  • FOR TABLE:指定要添加到发布的表的列表。如果在表名之前指定了ONLY, 那么只有该表被添加到发布中。如果没有指定ONLY, 则添加表及其所有后代表(如果有的话)。可选地,可以在表名之后指定 *以明确指示包含后代表。但是,这不适用于分区表。 分区表的分区始终被隐式视为发布的一部分,因此永远不会明确将其添加到发布中。

    只有持久基表和分区表才能成为出版物的一部分。 临时表,未记录表,外部表,物化视图和常规视图不能成为发布的一部分。

将分区表添加到发布时,其所有现有分区和将来分区都被隐式视为发布的一部分。 因此,即使直接在分区上执行的操作也会通过其祖先所在的发布来发布。

  • FOR ALL TABLES:将发布标记为复制数据库中所有表的更改,包括在将来创建的表。

  • WITH ( publication_parameter [= value] [, … ] ):该子句指定发布的可选参数。支持下列参数:

    • publish (string):这个参数决定了哪些DML操作将由新的发布发布给订阅者。 该值是用逗号分隔的操作列表。允许的操作是insertupdatedeletetruncate。 默认是发布所有的动作,所以这个选项的默认值是'insert, update, delete, truncate'

    • publish_via_partition_root (boolean):此参数确定是否会使用分区表的标识和模式而不是实际更改的单个分区的标识和模式来发布发布中包含的分区表(或其分区)中的更改。 单个分区的标识和模式是默认设置。 启用此功能可以将更改复制到非分区表或由一组不同的分区组成的分区表中。

      如果启用此功能,则不会复制直接在分区上执行的TRUNCATE操作。

注意:

  1. 如果既没有指定FOR TABLE,也没有指定FOR ALL TABLES, 那么这个发布就是以一组空表开始的。这在稍后添加表格的情况下是有用的。
  2. 创建发布不会开始复制。它只为未来的订阅者定义一个分组和过滤逻辑。
  3. 要创建一个发布,调用者必须拥有当前数据库的CREATE权限。 (当然,超级用户不需要这个检查。)
  4. 要将表添加到发布中,调用者必须拥有该表的所有权。FOR ALL TABLES 子句要求调用者是超级用户。
  5. 添加到发布UPDATE和/或DELETE 操作的发布的表必须已经定义了REPLICA IDENTITY。 否则将在这些表上禁止这些操作。
  6. 对于INSERT ... ON CONFLICT命令, 发布将公布从命令实际产生的操作。因此,根据结果,它可以作为 INSERTUPDATE发布,也可以根本不发布。
  7. COPY ... FROM命令是作为INSERT操作发布的。
  8. 不发布DDL操作。

示例:

创建一个发布,发布两个表中所有更改布:

CREATE PUBLICATION mypublication FOR TABLE users, departments;

创建一个发布,发布所有表中的所有更改:

CREATE PUBLICATION alltables FOR ALL TABLES

创建一个发布,只发布一个表中的INSERT操作:

CREATE PUBLICATION insert_only FOR TABLE mydata
    WITH (publish = 'insert');

3.3 修改发布

ALTER PUBLICATION是PostgreSQL的一个扩展。

命令ALTER PUBLICATION可以更改发布的属性。

前三个语句更改哪些表是该发布的一部分。SET TABLE 子句用指定的表替换发布中的表的列表。ADD TABLEDROP TABLE子句将从发布中添加和删除一个或多个表。 请注意,将表添加到已订阅的发布中将需要在订阅端执行ALTER SUBSCRIPTION ... REFRESH PUBLICATION操作才能生效。

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

第四条语句SET可以改变在CREATE PUBLICATION中指定的所有发布属性。 该命令中未提及的属性保留其先前的设置。

其余语句更改所有者和发布的名称。

你必须拥有该发布才能使用ALTER PUBLICATION。要改变所有者, 你也必须是新所有者角色的直接或间接成员。新的所有者必须在数据库上拥有 CREATE权限。此外,FOR ALL TABLES 发布的新所有者必须是超级用户。但是, 超级用户可以在避开这些限制的情况下更改发布的所有权。

参数:

name:要修改定义的现有发布的名称。

table_name:现有表的名称。如果在表名之前指定了ONLY,则只有该表受到影响。 如果没有指定ONLY,则该表及其所有后代表(如果有的话)都会受到影响。 可选地,可以在表名之后指定*以明确指示包含后代表。

SET ( *`publication_parameter`* [= *`value`*] [, ... ] )

该子句修改最初由CREATE PUBLICATION设置的发布参数。

new_owner:发布的新所有者的用户名。

new_name:发布的新名称。

示例:

将发布修改为只发布删除和更新:

ALTER PUBLICATION noinsert SET (publish = 'update, delete');

给发布添加一些表:

ALTER PUBLICATION mypublication ADD TABLE users, departments;

4. 订阅

订阅是逻辑复制的下游端。订阅被定义在其中的节点被称为订阅者。一个订阅会定义到另一个数据库的连接以及它想要订阅的发布集合(一个或者多个)。

订阅者数据库的行为与任何其他PostgreSQL实例相同,并且可以被用作其他数据库的发布者,只需要定义它自己的发布。

如果需要,一个订阅者节点可以有多个订阅。可以在一对发布者-订阅者之间定义多个订阅,在这种情况下要确保被订阅的发布对象不会重叠。

每一个订阅都将通过一个复制槽接收更改。预先存在的表数据的初始数据同步过程可能会要求额外的临时复制槽。

逻辑复制订阅可以是同步复制的后备服务器。后备名称默认是该订阅的名称。可以在订阅的连接信息中用application_name指定一个可供选择的名称。

如果当前用户是一个超级用户,则订阅会被pg_dump转储。否则订阅会被跳过并且写出一个警告,因为非超级用户不能从pg_subscription目录中读取所有的订阅信息。

在一个订阅被删除并且重建时,同步信息会丢失。这意味着数据必须被重新同步。

模式定义不会被复制,并且被发布的表必须在订阅者上存在。只有常规表可以成为复制的目标。例如,不能复制视图。

表在发布者和订阅者之间使用完全限定的表名进行匹配。不支持复制到订阅者上命名不同的表。

表的列也通过名称匹配。订阅表中的列顺序不需要与发布表中的顺序一样。 列的数据类型也不需要一样,只要可以将数据的文本表示形式转换为目标类型即可。 例如,您可以从integer类型的列复制到bigint类型的列。 目标表还可以具有发布表中不存在的额外列。额外列都将使用目标表的定义中指定的默认值填充。