暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

PostgreSQL 16 双向逻辑复制与事务回环控制

对逻辑复制的支持在五年前随着Postgres 10的发布正式进入Postgres。自那时以来,这一功能持续获得了诸多改进,但总体而言,逻辑复制主要用于数据迁移或单向的变更数据捕获工作流程。而今天刚刚发布的Postgres 16,为Postgres利用逻辑复制实现主动-主动架构奠定了更为坚实的基础。

什么是逻辑复制和双活?

如果您对逻辑复制或“active-active”(主动-主动)的概念不熟悉,我们为您做出如下解释:

逻辑复制是一种基于数据库逻辑内容(而非物理层面,即磁盘上的字节)复制数据变化的方法。简言之,您可以将其理解为对INSERT、UPDATE、DELETE等语句的复制。逻辑复制允许您根据预定义的复制规则,选择性地复制特定表、特定列,甚至是特定行。这种灵活性使得逻辑复制尤其适用于仅需复制数据子集或在复制过程中进行转换的场景。

主动-主动复制(针对数据库而言)是指具备向两个或更多个Postgres实例中任意一个写入数据的能力,且每个实例都拥有完整的实时数据集。主动-主动架构通常吸引人之处在于其能显著提升可用性。然而,这也通常伴随着复杂性增加这一显著权衡。到目前为止,使用逻辑复制实现双向复制颇具挑战,即便实现也往往效率不高。

事务回环

在PostgreSQL 16之前,要让这一功能在PostgreSQL中正常运作,不得不进行一些特殊的处理来避免事务回环问题。所谓事务回环,是指某个事务从源节点被复制到目标节点后,又再次被复制回到源节点。而在PostgreSQL 16中,引入了一项特性来解决这一问题。当创建订阅时,订阅者会要求发布者忽略那些已通过复制应用过程应用过的事务。这得益于WAL(Write-Ahead Log)流中的源头信息。

如果到现在您还跟得上我们的讲解,那就让我们深入其中,实际动手在PostgreSQL 16中配置这一功能吧。

来源过滤器

在WAL(Write Ahead Log)流中包含了一些被称为“origin messages”的信息。这些消息用于标识事务的来源,即该事务是本地产生的还是由复制应用过程引入的。下面通过分析示例来进一步了解这些消息。

以下是pg_waldump输出的一个本地事务片段:

  1. 1rmgr: Standby len (rec/tot): 50/ 50, tx: 0, lsn: 0/47000028, prev 0/46000A40, desc: RUNNING_XACTS nextXid 900 latestCompletedXid 899 oldestRunningXid 900

  2. 2rmgr: Heap len (rec/tot): 114/ 114, tx: 900, lsn: 0/47000060, prev 0/47000028, desc: HOT_UPDATE off 17 xmax 900 flags 0x10 ; new off 18 xmax 0, blkref #0: rel 1663/5/24792 blk 0

  3. 3rmgr: Transaction len (rec/tot): 46/ 46, tx: 900, lsn: 0/470000D8, prev 0/47000060, desc: COMMIT 2023-06-20 16:43:03.908882 EDT

现在对比一下来自逻辑复制应用过程的COMMIT记录:

  1. 1rmgr: Heap len (rec/tot): 54/ 54, tx: 901, lsn: 0/47000108, prev 0/470000D8, desc: LOCK off 18: xid 901: flags 0x00 LOCK_ONLY EXCL_LOCK KEYS_UPDATED , blkref #0: rel 1663/5/24792 blk 0

  2. 2rmgr: Heap len (rec/tot): 117/ 117, tx: 901, lsn: 0/47000140, prev 0/47000108, desc: HOT_UPDATE off 18 xmax 901 flags 0x10 KEYS_UPDATED ; new off 19 xmax 901, blkref #0: rel 1663/5/24792 blk 0

  3. 3rmgr: Transaction len (rec/tot): 65/ 65, tx: 901, lsn: 0/470001B8, prev 0/47000140, desc: COMMIT 2023-06-20 16:43:17.412369 EDT; origin: node 1, lsn 6/A95C2780, at 2023-06-20 16:43:17.412675 EDT

请注意COMMIT条目中的origin消息。这表明该事务起源于“node 1”,其源LSN为“6/A95C2780”。在PostgreSQL 16中,通过在订阅者上设置origin=none 标志,意味着发布者应当只向订阅者发送那些没有包含“origin messages”(即源头信息)的WAL记录。这类记录代表的是直接在发布者本地执行的事务,而不是通过复制应用过程从其他节点传来的事务。

示例环境

让我们快速测试搭建一个主动-主动复制环境。首先创建两个PostgreSQL 16实例。设置以下PostgreSQL参数以配置实例以支持逻辑复制:

  1. wal_level = 'logical'

  2. max_worker_processes = 16

将WAL级别设置为’logical’会启动逻辑解码。由于我们在两侧都添加了多个进程来提取和重放数据,因此我也将最大工作进程数增加,以免干扰其他复制活动。设置完上述参数后,重启PostgreSQL。在这个示例中,这两个PostgreSQL实例分别称为pg1和pg2。

在pg1中,执行以下命令以配置示例数据库对象。

  1. #创建一个名为 emp_eid_seq 的序列,初始值设定为 1,并且每次递增 2:

  2. CREATE SEQUENCE emp_eid_seq

  3. START 1

  4. INCREMENT 2;


  5. CREATE TABLE emp (eid int NOT NULL DEFAULT nextval('emp_eid_seq') primary key,

  6. first_name varchar(40),

  7. last_name varchar(40),

  8. email varchar(100),

  9. hire_dt timestamp);


  10. INSERT INTO emp (FIRST_NAME,LAST_NAME,EMAIL,HIRE_DT) VALUES ('John', 'Doe', 'johndoe@example.com', '2021-01-15 09:00:00'),

  11. ('Jane', 'Smith', 'janesmith@example.com', '2022-03-20 14:30:00'),

  12. ('Michael', 'Johnson', 'michaelj@example.com', '2020-12-10 10:15:00'),

  13. ('Emily', 'Williams', 'emilyw@example.com', '2023-05-05 08:45:00'),

  14. ('David', 'Brown', 'davidbrown@example.com', '2019-11-25 11:20:00'),

  15. ('Sarah', 'Taylor', 'saraht@example.com', '2022-09-08 13:00:00'),

  16. ('Robert', 'Anderson', 'roberta@example.com', '2021-07-12 16:10:00'),

  17. ('Jennifer', 'Martinez', 'jenniferm@example.com', '2023-02-18 09:30:00'),

  18. ('William', 'Jones', 'williamj@example.com', '2020-04-30 12:45:00'),

  19. ('Linda', 'Garcia', 'lindag@example.com', '2018-06-03 15:55:00');

在pg2中,我们使用稍有不同的sql:

  1. #创建一个名为 emp_eid_seq 的序列,初始值设定为 2,并且每次递增 2:

  2. CREATE SEQUENCE emp_eid_seq

  3. START 2

  4. INCREMENT 2;


  5. CREATE TABLE emp (eid int NOT NULL DEFAULT nextval('emp_eid_seq') primary key,

  6. first_name varchar(40),

  7. last_name varchar(40),

  8. email varchar(100),

  9. hire_dt timestamp);

注意,特殊的设计考量已经开始显现。为了避免主键冲突,pg1生成奇数作为主键值,而pg2则使用偶数。

最后的设置环节是在两套系统中创建用于复制的用户。

  1. CREATE ROLE repuser WITH REPLICATION LOGIN PASSWORD 'welcome1';

  2. GRANT all ON all tables IN schema public TO repuser;

发布

使用发布/订阅模型,可以从一个Postgres实例(发布者)捕获并复制到多个Postgres实例(订阅者)中的更改。使用以下命令在每个实例上创建发布者:

pg1:

  1. CREATE PUBLICATION hrpub1

  2. FOR TABLE emp;

pg2:

  1. CREATE PUBLICATION hrpub2

  2. FOR TABLE emp;

尽管每个实例上的发布名称可以相同,但使用不同的名称将有助于我们在以后使用自定义心跳表测量延迟时进行区分。

订阅

有了发布者准备就绪,下一步是创建订阅者。默认情况下,逻辑复制会从发布者处获取初始快照,并将数据复制到订阅者。由于我们进行的是双向复制,我们允许从pg1到pg2的快照,但不需要反向复制,因此将禁用初始复制。

pg1:

  1. CREATE SUBSCRIPTION hrsub1

  2. CONNECTION 'host=pg2 port=5432 user=repuser password=welcome1 dbname=postgres'

  3. PUBLICATION hrpub2

  4. WITH (origin = none, copy_data = false);

pg2:

  1. CREATE SUBSCRIPTION hrsub2

  2. CONNECTION 'host=pg1 port=5432 user=repuser password=welcome1 dbname=postgres'

  3. PUBLICATION hrpub1

  4. WITH (origin = none, copy_data = true);

关键在于订阅中的origin设置(origin = none)。origin的默认值为’any’,这会指示发布者将所有事务发送到订阅者,无论其来源如何。对于双向复制,这是不利的。如果设置为’any’,在pg1上执行的更新会被复制到pg2(到目前为止一切正常)。但是,该复制的事务会被捕获并回传到pg1,如此反复。这就是我们所说的事务回环。

通过将origin设置为none,订阅者会请求发布者仅发送不带源头的更改,从而忽略复制的事务。现在,Postgres已经准备好进行双向逻辑复制。

几秒钟后,验证emp表的初始副本已经在pg1和pg2之间完成。

复制测试

在配置好复制之后,分别在两侧更新数据并验证复制是否生效。

pg1:

  1. SELECT * FROM emp WHERE eid=1;

  2. UPDATE emp SET first_name='Bugs', last_name='Bunny' WHERE eid=1;

pg2:

  1. SELECT * FROM emp WHERE eid=1;

  2. SELECT * FROM emp WHERE eid=3;

  3. UPDATE emp SET first_name='Road', last_name='Runner' WHERE eid=3;

别得意忘形

虽然设置双向复制很容易,但这并非没有风险。在你对生产环境发起拉取请求之前,有许多事项需要考虑,比如监控、限制、变更量、应用程序行为、备份与恢复、数据对账等。让我们通过一个快速练习来演示导致数据完整性问题的应用程序行为。对于以下示例,请同时打开到pg1和pg2的数据库连接。

在每个会话中使用以下命令开始一个事务,并记下email和last_name的值。

  1. BEGIN;

  2. SELECT * FROM emp WHERE eid=1;

在pg1中更新EID为1的员工的电子邮件地址,但不要提交。

  1. UPDATE emp SET email='bugs.bunny@acme.com' WHERE eid=1;

在pg2中更新姓氏,但同样不要提交。

  1. UPDATE emp SET last_name='Jones' WHERE eid=1;

预期在提交后,last_name应为Jones,email应为<a href="mailto:bugs.bunny@acme.com" "="" style="touch-action: manipulation; color: rgb(65, 131, 196); background-image: initial; background-position: 0px 0px; background-size: initial; background-repeat: initial; background-attachment: initial; background-origin: initial; background-clip: initial; outline: 0px; cursor: pointer; transition: color 0.3s ease 0s;">bugs.bunny@acme.com。先在pg1中提交事务,然后在pg2中提交。会发生什么呢?

pg1:

  1. SELECT * FROM emp WHERE eid=1;

  1. eid | first_name | last_name | email | hire_dt

  2. -----+------------+-----------+-------------------------+---------------------

  3. 1 | Bugs | Jones | johndoe@example.com | 2022-09-25 16:04:47

  4. (1 row)

pg2:

  1. SELECT * FROM emp WHERE eid=1;

  1. eid | first_name | last_name | email | hire_dt

  2. -----+------------+-----------+---------------------+---------------------

  3. 1 | Bugs | Bunny | bugs.bunny@acme.com | 2022-09-25 16:04:47

  4. (1 row)

现在两行数据已经不同步。在pg1中,电子邮件的更新丢失了;在pg2中,姓氏的更新丢失了。这是因为逻辑复制期间发送的是整行数据,而不只是更新过的字段。在这种情况下,即使最终一致性也无法实现。

作者:Elizabeth Christensen
译文:凯恩


文章转载自开源软件联盟PostgreSQL分会,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论