原文地址:https://www.postgresql.fastware.com/blog/how-to-handle-logical-replication-conflicts-in-postgresql
原文作者:Takamichi Osumi
逻辑复制被广泛用作一种简单灵活的数据复制方式。现在 PostgreSQL 通过提供一些机制来处理复制冲突,使它变得更加容易,本文将为大家介绍操作方法。
引言
逻辑复制是一种选择性数据复制的方法。虽然物理复制复制整个集群并在需要时在备用数据库上接受只读查询,但逻辑复制对数据复制提供了更细粒度和更灵活的控制。逻辑复制中可用的一些功能包括:
- 选择目标表和操作
- 直接写入订阅者
- 发布和订阅之间的复杂拓扑
在逻辑复制中,订阅工作者进程将数据应用到订阅者上,该进程以与在节点上执行 DML 操作类似的方式运行。因此,如果新的传入数据违反了订阅者的任何约束,则复制会因错误而停止。
这称为冲突,需要用户手动干预才能继续。
目录导读
PostgreSQL 15 有哪些变化呢?
在 PostgreSQL 15 中,PostgreSQL 社区正在引入有助于解决逻辑复制冲突的改进和新功能。我将描述这些改进以及如何应用它们来处理冲突。
在这篇文章中,解决方案是通过跳过与现有数据冲突的事务来实现的。关于这篇文章中描述的自动禁用功能(disable_on_error 选项),我也是社区中的开发人员之一。下图说明了在 apply 过程中如何发生冲突。
免责声明:在这篇文章中,我使用了 PostgreSQL 的开发版本,社区可以决定更改其设计或完全恢复这些设计。
对逻辑复制和新功能的改进
社区一直在努力确保 Postgres 在逻辑复制方面提供可靠、高效和简单的度量。这项工作的一部分是提交以下功能和改进:
-
订阅统计的 新系统视图 pg_stat_subscription_stats
此视图的每条记录都指向一个订阅。在这个视图中,我们实现了两种类型的失败计数器:一种用于初始表同步失败,另一种用于应用更改失败。 -
新订阅选项 disable_on_error
当发生冲突时,逻辑复制工作者默认会陷入错误循环。原因是当worker无法应用更改时,它会出错退出,重新启动,并尝试在后台重复应用相同的更改。但是有了这个新选项,订阅工作者可以通过在出错时自动禁用订阅来打破循环。之后,用户可以选择下一步做什么。初始表同步期间的失败也会禁用订阅。默认值为 false 并将其设置为 false 会在冲突时重复相同的错误。 -
订阅工作者错误的扩展错误上下文信息
错误上下文消息现在包含 2 条有条件的新信息:- 完成 LSN。通常,LSN 是指向 WAL 中某个位置的指针。在这里,finish LSN 将指示 commit_lsn 表示已提交的事务,并表示 prepare_lsn 表示准备好的事务。
- 复制源名称。这将包含跟踪复制进度的复制源的名称。对于逻辑复制,会自动创建每个对应的复制源以及订阅定义。
当用户需要使用 pg_replication_origin_advance 函数时,上述两条信息都会很有用。
通过跳过失败的事务来解决冲突
我们已经检查了此主题的每个增强功能。因此,在本节中,我将模拟一个冲突场景。请记住,通过调用 pg_replication_origin_advance 跳过事务只是用户可以选择的解决方案之一,用户还可以更改订阅者的数据或权限以解决冲突。
1、在发布者端,创建一个表和一个发布。
postgres=# CREATE TABLE tab (id integer);
CREATE TABLE
postgres=# INSERT INTO tab VALUES (5);
INSERT 0 1
postgres=# CREATE PUBLICATION mypub FOR TABLE tab;
CREATE PUBLICATION
复制
我们现在有一个用于初始表同步的记录。
2、在订阅者端,创建一个具有唯一约束和订阅的表。
postgres=# CREATE TABLE tab (id integer UNIQUE);
CREATE TABLE
postgres=# CREATE SUBSCRIPTION mysub CONNECTION '…' PUBLICATION mypub WITH (disable_on_error = true);
NOTICE: created replication slot "mysub" on publisher
CREATE SUBSCRIPTION
复制
我们现在创建了一个启用了 disable_on_error 选项的订阅。同时,这个定义会导致后台的初始表同步,这将成功而没有任何问题。
3、在发布端,表同步后连续执行三个事务。
postgres=# BEGIN; -- Txn1
BEGIN
postgres=*# INSERT INTO tab VALUES (1);
INSERT 0 1
postgres=*# COMMIT;
COMMIT
postgres=# BEGIN; -- Txn2
BEGIN
postgres=*# INSERT INTO tab VALUES (generate_series(2, 4));
INSERT 0 3
postgres=*# INSERT INTO tab VALUES (5);
INSERT 0 1
postgres=*# INSERT INTO tab VALUES (generate_series(6, 8));
INSERT 0 3
postgres=*# COMMIT;
COMMIT
postgres=# BEGIN; -- Txn3
BEGIN
postgres=*# INSERT INTO tab VALUES (9);
INSERT 0 1
postgres=*# COMMIT;
COMMIT
postgres=# SELECT * FROM tab;
id
----
5
1
2
3
4
5
6
7
8
9
(10 rows)
复制
可以成功重放 Txn1。但是 Txn2 的第二条语句包含与表同步相同的重复值(在第一个命令部分)。在订阅者上,这违反了表的唯一约束。因此,它会导致冲突并禁用订阅。结果,订阅将在此处停止。按照以下步骤,在解决冲突之前不会重播 Txn3。
4、在订阅方,检查当前复制状态。
postgres=# SELECT * FROM pg_stat_subscription_stats;
subid | subname | apply_error_count | sync_error_count | stats_reset
-------+---------+-------------------+------------------+-------------
16389 | mysub | 1 | 0 |
(1 row)
postgres=# SELECT oid, subname, subenabled, subdisableonerr FROM pg_subscription;
oid | subname | subenabled | subdisableonerr
-------+---------+------------+-----------------
16389 | mysub | f | t
(1 row)
postgres=# SELECT * FROM tab;
id
----
5
1
(2 rows)
复制
在跳过事务之前,我们先看看当前状态。
在初始表同步期间没有失败,但在应用阶段有一个失败。这就是 pg_stat_subscription_stats 到目前为止所显示的。此外,由于我们创建了将 disable_on_error 选项设置为 true 的订阅,因此订阅 mysub 由于失败而被禁用。表选项卡将数据复制到 Txn1,我们已成功重放。
5、在订阅方,检查此冲突的错误消息和 disable_on_error 选项的日志消息。
ERROR: duplicate key value violates unique constraint "tab_id_key"
DETAIL: Key (id)=(5) already exists.
CONTEXT: processing remote data for replication origin "pg_16389" during "INSERT"
for replication target relation "public.tab" in transaction 730 finished at 0/1566D10
LOG: logical replication subscription "mysub" has been disabled due to an error
复制
上面我们可以看到复制源名称和表示commit_lsn的LSN。我将利用这些来跳过 Txn2,如下所示。
6、在订阅者端,执行 pg_replication_origin_advance 然后启用订阅。
postgres=# SELECT pg_replication_origin_advance('pg_16389', '0/1566D11'::pg_lsn);
pg_replication_origin_advance
-------------------------------
(1 row)
postgres=# ALTER SUBSCRIPTION mysub ENABLE;
ALTER SUBSCRIPTION
postgres=# SELECT * FROM tab;
id
----
5
1
9
(3 rows)
复制
使原始数据提前后,我启用了订阅以重新激活它 - 立即,我们可以看到 Txn3 的复制数据。
在这里,请注意与 Txn2 中冲突的直接原因无关的其他一些数据,无论同一事务中的时间如何(请记住,我们在 Txn2 中执行了其他插入),没有被复制 - 整个事务 Txn2 是跳过。
这里的事件顺序如下:
- 我们使用了 pg_replication_origin_advance 并启用了订阅。
- 启用订阅会启动应用工作者,并将通过 pg_replication_origin_advance 传递的 LSN 发送到发布者上的 walsender 进程。
- 这个 walsender 进程通过比较相关的 LSN 来评估是否应该在解码提交时发送或跳过事务 Txn2。
- walsender 得出结论,应该跳过事务 Txn2。
最后,我强调我们必须注意将适当的 LSN 传递给 pg_replication_origin_advance。尽管由于本博客中描述的新社区的改进,这种可能性变得非常低,但如果被滥用,它可以很容易地跳过与冲突无关的其他事务。
如果我指定了错误的参数,会出现什么问题?
作为参考,我们提供了一个示例,说明如果我们错误地使用 pg_replication_origin_advance 会发生什么。
下面,我重新执行了上述场景,在 Txn3 之后再插入一个事务 Txn4 以插入 10。然后,作为 pg_replication_origin_advance 的参数,我设置了一个大于 Txn3 的提交记录但小于 Txn4 的提交记录的 LSN(由 pg_waldump 检索)。启用订阅后,我得到了没有 Txn3 值的复制数据。
错误使用 pg_replication_origin_advance 的订阅方结果如下:
postgres=# SELECT * FROM tab;
id
----
5
1
10
(3 rows)
复制
如上所示,在手动干预复制以解决冲突时,我们再怎么小心也不为过。
在这一点上,社区已经单独引入了一个不同的功能(ALTER SUBSCRIPTION SKIP)。在处理逻辑复制冲突方面,此功能比 pg_replication_origin_advance 领先一步。
请看我下一篇描述细节的帖子。
总结
随着逻辑复制在企业中的应用越来越广泛,处理冲突等实际问题的需求变得越来越重要。出于这个原因,添加到 PostgreSQL 的改进是必不可少的。
PostgreSQL 社区一直在强化数据库,在这篇博文中我描述了一种处理逻辑复制冲突的简单方法。尽管如此,我们必须小心为正在使用的工具提供正确的信息,在本例中为 pg_replication_origin_advance。
如果您想阅读更多关于 PostgreSQL 中的逻辑复制及其机制的信息,我写了一篇博客文章如何通过检查逻辑复制来深入了解 pg_stat_replication_slots 视图。我的同事 Ajin Cherian 写了一篇关于PostgreSQL 14 中两阶段提交的逻辑解码的博客文章,您可以了解更多关于逻辑解码以及 PostgreSQL 如何为两阶段提交执行它的信息。
参考文章:
- https://www.postgresql.org/docs/devel/hot-standby.html
- https://www.postgresql.org/docs/devel/logical-replication-conflicts.html
- https://www.postgresql.org/docs/devel/monitoring-stats.html#id-1.6.15.7.13.2
- https://www.postgresql.org/docs/devel/sql-createsubscription.html
- https://www.postgresql.org/docs/devel/error-message-reporting.html
- https://www.postgresql.org/docs/current/replication-origins.html
- https://www.postgresql.org/docs/devel/functions-admin.html#id-1.5.8.33.8.5.2.2.20.1.1.1
- https://www.postgresql.org/docs/devel/pgwaldump.html