逻辑复制是一种基于数据对象的复制标识(通常是主键)复制数据对象及其更改的方法。我们使用术语"逻辑"来与物理复制加以区分,后者使用准确的块地址以及逐字节的复制方式。PostgreSQL两种机制都支持。逻辑复制允许在数据复制和安全性上更细粒度的控制。
逻辑复制使用一种发布 和订阅 模型,其中有一个或者更多订阅者 订阅一个发布者 节点上的一个或者更多发布 。订阅者从它们所订阅的发布拉取数据并且可能后续重新发布这些数据以允许级联复制或者更复杂的配置。
一个表的逻辑复制通常开始于对发布者服务器上的数据取得一个快照并且将快照拷贝给订阅者。一旦这项工作完成,发布者上的更改会被实时发送给订阅者。订阅者以与发布者相同的顺序应用那些数据,这样在一个订阅中能够保证发布的事务一致性。这种数据复制的方法有时候也被称为事务性复制。
逻辑复制的典型用法是:
-
在一个数据库或者一个数据库的子集中发生更改时,把增量的改变发送给订阅者。
-
在更改到达订阅者时引发触发器。
-
把多个数据库联合到单一数据库中(例如用于分析目的)。
-
在PostgreSQL的不同主版本之间进行复制。
-
在不同平台上(例如Linux到Windows)的PostgreSQL实例之间进行复制。
-
将复制数据的访问给予不同的用户组。
-
在多个数据库间共享数据库的一个子集。
订阅者数据库的行为与任何其他PostgreSQL实例相同,并且可以被用作其他数据库的发布者,只需要定义它自己的发布。当订阅者被应用当作只读时,单一的订阅中不会有冲突。在另一方面,如果应用或者对相同表集合的订阅者执行了其他的写动作,冲突可能会发生。
一、发布
发布 可以被定义在任何物理复制的主服务器上。定义有发布的节点被称为发布者。发布是从一个表或者一组表生成的改变的集合,也可以被描述为更改集合或者复制集合。每个发布都只存在于一个数据库中。
发布与模式不同,不会影响表的访问方式。如果需要,每个表都可以被加入到多个发布。当前,发布只能包含表。对象必须被明确地加入到发布,除非发布是用ALL TABLES
创建的。
Publication可以选择把它们产生的更改限制为INSERT
、UPDATE
、DELETE
以及TRUNCATE
的任意组合,类似于触发器如何被特定事件类型触发的方式。默认情况下,所有操作类型都会被复制。
为了能够复制UPDATE
和DELETE
操作,被发布的表必须配置有一个"复制标识",这样在订阅者那一端才能标识对于更新或删除合适的行。默认情况下,复制标识就是主键(如果有主键)。也可以在复制标识上设置另一个唯一索引(有特定的额外要求)。如果表没有合适的键,那么可以设置成复制标识"full",它表示整个行都成为那个键。不过,这样做效率很低,只有在没有其他方案的情况下才应该使用。如果在发布者端设置了"full"之外的复制标识,在订阅者端也必须设置一个复制标识,它应该由相同的或者少一些的列组成。如何设置复制标识的细节请参考REPLICA IDENTITY。如果在复制UPDATE
或DELETE
操作的发布中加入了没有复制标识的表,那么订阅者上后续的UPDATE
或DELETE
操作将导致错误。不管有没有复制标识,INSERT
操作都能继续下去。
每一个发布都可以由多个订阅者。
Publication通过使用CREATE PUBLICATION命令创建并且可以在之后使用相应的命令进行修改或者删除。
表可以使用ALTER PUBLICATION动态地增加或者移除。ADD TABLE
以及DROP TABLE
操作都是事务性的,因此一旦该事务提交,该表将以正确的快照开始或者停止复制。
二、订阅
订阅 是逻辑复制的下游端。订阅被定义在其中的节点被称为订阅者。一个订阅会定义到另一个数据库的连接以及它想要订阅的发布集合(一个或者多个)。
订阅者数据库的行为与任何其他PostgreSQL实例相同,并且可以被用作其他数据库的发布者,只需要定义它自己的发布。
如果需要,一个订阅者节点可以有多个订阅。可以在一对发布者-订阅者之间定义多个订阅,在这种情况下要确保被订阅的发布对象不会重叠。
每一个订阅都将通过一个复制槽接收更改。预先存在的表数据的初始数据同步过程可能会要求额外的临时复制槽。
逻辑复制订阅可以是同步复制的后备服务器。后备名称默认是该订阅的名称。可以在订阅的连接信息中用application_name
指定一个可供选择的名称。
如果当前用户是一个超级用户,则订阅会被pg_dump
转储。否则订阅会被跳过并且写出一个警告,因为非超级用户不能从pg_subscription
目录中读取所有的订阅信息。
可以使用CREATE SUBSCRIPTION增加订阅,并且使用ALTER SUBSCRIPTION在任何时刻停止/继续订阅,还可以使用DROP SUBSCRIPTION删除订阅。
在一个订阅被删除并且重建时,同步信息会丢失。这意味着数据必须被重新同步。
模式定义不会被复制,并且被发布的表必须在订阅者上存在。只有常规表可以成为复制的目标。例如,不能复制视图。
表在发布者和订阅者之间使用完全限定的表名进行匹配。不支持复制到订阅者上命名不同的表。
表的列也通过名称匹配。允许在目标表中的列序不同,但是列类型必须匹配。目标表可以有被发布表没有提供的额外列。额外列将用其默认值填充。
2.1. 复制槽管理
如早前所提到的,每一个(活跃的)订阅会从远(发布)端上的一个复制槽接收更改。通常,远程复制槽是在使用CREATE SUBSCRIPTION
创建订阅是自动创建的,并且在使用DROP SUBSCRIPTION
删除订阅时,复制槽也会自动被删除。不过,在一些情况下,有必要单独操纵订阅以及其底层的复制槽。下面是一些场景:
-
在创建一个订阅时,复制槽已经存在。在这种情况下,可以使用
create_slot = false
选项创建订阅并关联到现有的槽。 -
在创建一个订阅时,远程主机不可达或者处于一种不明状态。在这种情况下,可以使用
connect = false
选项创建订阅。那么远程主机将根本不会被联系。这是pg_dump所使用的方式。这样,在订阅可以被激活之前,必须手工创建远程复制槽。 -
在删除一个订阅时,复制槽应该被保留。当订阅者数据库正在被移动到一台不同的主机并且将从那里再被激活时,这种行为很有用。在这种情况下,可以在尝试删除该订阅之前,使用
ALTER SUBSCRIPTION
将复制槽解除关联。 -
在删除一个订阅是,远程主机不可达。在这种情况下,可以在尝试删除该订阅之前,使用
ALTER SUBSCRIPTION
将复制槽解除关联。如果远程数据库实例不再存在,那么不需要进一步的行动。不过,如果远程数据库实例只是不可达,那么复制槽应该被手动删除。否则它将会继续保留WAL并且最终可能会导致磁盘被填满。这种情况应该要仔细地研究。
三、冲突
逻辑复制的行为类似于正常的DML操作,即便数据在订阅者节点本地被修改,逻辑复制也会根据收到的更改来更新数据。如果流入的数据违背了任何约束,复制将停止。这种情况被称为一个冲突 。在复制UPDATE
或DELETE
操作时,缺失的数据将不会产生冲突并且这类操作将被简单地跳过。
冲突将会产生错误并且停止复制,它必须由用户手工解决。在订阅者的服务器日志中可以找到有关冲突的详细情况。
通过更改订阅者上的数据(这样它就不会与到来的数据发生冲突)或者跳过与已有数据冲突的事务可以解决这种冲突。通过调用pg_replication_origin_advance()函数可以跳过该事务,函数的参数是对应于该订阅名称的*node_name
* 以及一个位置。复制源头的当前位置可以在pg_replication_origin_status系统视图中看到。
四、限制
逻辑复制当前有下列限制或者缺失的功能。这些可能在未来的发行中解决。
-
数据库模式和DDL命令不会被复制。初始模式可以手工使用
pg_dump --schema-only
进行拷贝。后续的模式改变需要手工保持同步(不过值得注意的是,模式其实不需要在两端保持绝对相同)。当一个活跃的数据库中模式定义改变时,逻辑复制是鲁棒的:当模式在发布者上发生改变并且被复制的数据开始到达订阅者但却不适合表模式时,复制将报错,直至模式被更新。在很多情况下,可以通过先对订阅者应用额外的模式更改来避免间歇性的错误。 -
序列数据不被复制。后台由序列支撑的serial或者标识列中的数据当然将被作为表的一部分复制,但是序列本身在订阅者上仍将显示开始值。如果订阅者被用作一个只读数据库,那么这通常不会是什么问题。不过,如果订阅者数据库预期有某种转换或者容错,那么序列需要被更新到最后的值,要么通过从发布者拷贝当前数据的防范(也许使用
pg_dump
),要么从表本身决定一个足够高的值。 -
支持
TRUNCATE
命令的复制,但是在截断由外键连接在一起的表群体时必须要小心。在复制截断动作时,订阅者将截断与发布者上被截断的相同的表群体,这些表或者被明确指定或者通过CASCADE
隐含地收集而来,然后还要减去不属于该订阅的表。如果所有受影响的表都属于同一个订阅,这会正确地工作。但是如果订阅者上要被截断的某些表有外键链接到不属于同一订阅的表,那么在订阅者上该截断动作的应用将会失败。 -
大对象不会被复制。没有办法可以解决这个问题,除非把数据存储在普通表中。
-
复制只能从基表到基表。也就是说,发布端和订阅端上的表都必须是普通表,而不是视图、物化视图、分区根表或者外部表。如果是分区,可以一一对应地复制分区层次,但当前不能复制成一种不同的分区设置。尝试复制不是基表的表将会导致错误。
五、架构
逻辑复制从拷贝发布者数据库上的数据库快照开始。拷贝一旦完成,发布者上的更改会在它们发生时实时传送给订阅者。订阅者按照数据在发布者上被提交的顺序应用数据,这样任意单一订阅中的发布的事务一致性才能得到保证。
逻辑复制被构建在一种类似于物理流复制的架构上。它由"walsender"和"apply"进程实现。walsender进程开始对WAL的逻辑解码并且载入标准逻辑解码插件(pgoutput)。该插件把从WAL中读取的更改转换成逻辑复制协议并且根据发布说明过滤数据。然后数据会被连续地使用流复制协议传输到应用工作者,应用工作者会把数据映射到本地表并且以正确的事务顺序应用它们接收到的更改。
订阅者数据库上的应用进程总是将session_replication_role
设置为replica
运行,这会产生触发器和约束上通常的效果。
逻辑复制应用进程当前仅会引发行触发器,而不会引发语句触发器。不过,初始的表同步是以类似一个COPY
命令的方式实现的,因此会引发INSERT
的行触发器和语句触发器。
5.1. 初始快照
已有的被订阅表中的初始数据会被快照并且以一种特殊类型的应用进程的并行实例进行拷贝。这种进程将创建自己的临时复制槽并且拷贝现有的数据。一旦现有的数据被拷贝完,工作者会进入到同步模式,主应用进程会流式传递在使用标准逻辑复制拷贝初始数据期间发生的任意改变,这会确保表被带到一种已同步的状态。一旦同步完成,该表的复制的控制权会被交回给主应用进程,其中复制会照常继续。
六、监控
因为逻辑复制是基于与物理流复制相似的架构的,一个发布节点上的监控也类似于对物理复制主节点的监控。
有关订阅的监控信息在pg_stat_subscription中可以看到。每一个订阅工作者在这个视图都有一行。一个订阅能有零个或者多个活跃订阅工作者取决于它的状态。
通常,对于一个已启用的订阅会有单一的应用进程运行。一个被禁用的订阅或者崩溃的订阅在这个视图中不会有行存在。如果有任何表的数据同步正在进行,对正在被同步的表会有额外的工作者。
七、安全性
用于复制连接的角色必须有REPLICATION
属性(或者是一个超级用户)。该角色的访问必须被配置在pg_hba.conf
中,并且它必须有LOGIN
属性。
为了能够拷贝初始表数据,用于复制连接的角色必须在被发布的表上具有SELECT
特权(或者是一个超级用户)。
要创建发布,用户必须在数据库中有CREATE
特权。
要把表加入到一个发布,用户必须在该表上有拥有权。要创建一个自动发布所有表的发布,用户必须是一个超级用户。
要创建订阅,用户必须是一个超级用户。
订阅的应用过程将在本地数据库上以超级用户的特权运行。
特权检查仅在复制连接开始时被执行一次。在从发布者读到每一个更改记录时不会重新检查特权,在每一个更改被应用时也不会重新检查特权。
八、配置设置
逻辑复制要求设置一些配置选项。
wal_level = logical
在发布者端,wal_level
必须被设置为logical
,而max_replication_slots
中设置的值必须至少是预期要连接的订阅数加上保留给表同步的连接数。max_wal_senders
应该至少被设置为max_replication_slots
加上同时连接的物理复制体的数量。
订阅者还要求max_replication_slots
被设置。在这种情况下,它必须至少被设置为将被加入到该订阅者的订阅数。max_logical_replication_workers
必须至少被设置为订阅数加上保留给表同步的连接数。此外,可能需要调整max_worker_processes
以容纳复制工作者,至少为(max_logical_replication_workers
+ 1
)。注意,一些扩展和并行查询也会从max_worker_processes
中取得工作者槽。
九、快速设置
首先在postgresql.conf
中设置配置选项:
wal_level = logical
对于一个基础设置来说,其他所需的设置使用默认值就足够了。
需要调整pg_hba.conf
以允许复制(这里的值取决于实际的网络配置以及用于连接的用户):
host all repuser 0.0.0.0/0 md5
然后在发布者数据库上:
CREATE PUBLICATION mypub FOR TABLE users, departments;
并且在订阅者数据库上:
CREATE SUBSCRIPTION mysub CONNECTION 'dbname=foo host=bar user=repuser' PUBLICATION mypub;
上面的语句将开始复制过程,它会同步表users
以及departments
的初始表内容,然后开始复制对那些表的增量更改。