解释 PostgreSQL 15 中引入的改进,以解决逻辑复制通信中的以下两个问题:
- 如果事务中的所有 DML 未根据订阅过滤器发布,则 walsender 发送空事务的情况。这会造成 CPU/内存/网络等资源的浪费。
- 在处理大事务时,如果walsender忙于处理事务中未发布的DML,可能会长时间无法与 walreceiver 通信。即使 walsender 按预期工作,这也可能导致意外超时错误。
逻辑复制中的通信
在进一步讨论之前,我想简单介绍一下逻辑复制中关于通信的两个概念:通信消息的类型和过滤。
通讯信息的类型
在逻辑复制中,walsender 向 walreceiver 发送的消息有两种:
- Keep-alive 消息 此消息用于告诉 walreceiver walsender 正在按预期工作。 用户可以使用 wal_receiver_timeout 参数(默认为 60 秒)设置 walreceiver 的超时 GUC。在 wal_receiver_timeout 内,如果 walreceiver 没有收到来自 walsender 的任何类型的消息,它将以超时错误退出。
- 逻辑复制协议消息 有许多类型的逻辑复制协议消息,但在本博客中,我们只关注其中两种:
- DML 消息,包括 INSERT、UPDATE、DELETE 和 TRUNCATE
- 定义事务开始和结束的消息,例如 BEGIN 和 COMMIT 消息。 可以在PostgreSQL 网站上查看逻辑复制协议的完整列表。
过滤
您可能没有意识到这一点,但并非交易中的所有 DML 都会发送到 walreceiver。这是因为在创建发布时可以在表、行或操作类型上指定筛选器。因此,如果满足过滤条件,将不会发送某些 DML。
改进概述
现在,我将分别介绍本文开头提到的两个问题是如何改进/修复的。
对空事务的改进
如果一个事务中的所有 DML 在解码过程中都被过滤掉了,那么我们称这个事务为空事务。在 PostgreSQL 15 之前,标准逻辑解码插件 pgoutput(PostgreSQL 中默认使用的逻辑复制插件)会将每个事务发送到 walreceiver。即使对于空事务,虽然不会发送与 DML 相关的消息,但仍会发送定义事务开始和结束的消息。构建/传输这些空事务是浪费 CPU 周期和网络带宽。我们可以在下面看到这个过程
为了解决这个问题,我们让 walsender 推迟 BEGIN 消息,直到发送第一个 DML 消息。在解码结束时,如果没有发送 BEGIN 消息,则也不发送 COMMIT 消息。详细的开发信息可以在GitHub中查看。
至于非空交易,发送消息的时间也有变化。让我们举个例子来看看这个变化——假设我们有以下事务 T1:
postgres=# BEGIN;
BEGIN
postgres=*# INSERT INTO tab_not_publish VALUES (1); -- This DML will be filtered out
INSERT 0 1
postgres=*# INSERT INTO tab_publish VALUES (1); -- This DML will be published
INSERT 0 1
postgres=*# COMMIT;
COMMIT
BEGIN 消息和 COMMIT 消息的发送时序如下图所示。
正如我们在上面看到的,只有 table tab_publish 的 INSERT 消息可以从 walsender 发送到 walreceiver。 如上图,修改后,如果一个事务中的所有DML都被过滤掉了,那么BEGIN消息和COMMIT消息就不会发送了。这样,walsender 可以跳过发送空事务。
但是,在同步逻辑复制中,在PostgreSQL 15之前,为了确认数据已经同步,当walreceiver收到空事务的COMMIT消息时,会同步本地数据,并向walsender发送反馈消息,确认数据已同步。
walsender 只有在收到 walreceiver 的反馈消息后才会继续,否则 walsender 将阻止客户端后端提交事务。因此,在 PostgreSQL 15 中,walsender 在同步逻辑复制中跳过一个空事务后,会向 walreceiver 发送 keep-alive 消息并请求反馈消息。然后 walreceiver 会同步数据,并根据这个消息向walsender 发送反馈消息。这样我们就可以避免同步逻辑复制中事务延迟响应的情况。下图显示了同步逻辑复制中通信的变化。
修复意外超时错误
在 PostgreSQL 15 之前,如果一个事务有很多连续的 DML 没有发布,这会导致 walsender 长时间无法与 walreceiver 通信,因为 walsender 将忙于解码这些未发布的 DML。在这种情况下,即使 walsender 按预期工作,由于 walreceiver 在指定的超时时间内没有收到来自 walsender 的任何消息,这会导致 walreceiver 收到意外的超时错误。
在 PostgreSQL 15 中,为避免此错误,walsender 会定期与 walreceiver 保持通信。因此,当 walsender 处理一定阈值的 DML 时(无论这些 DML 是否已发布),它会在需要时尝试向 walreceiver 发送 keep-alive 消息以保持通信。有关此开发的详细信息可在GitHub中访问。
假设我们有一个事务 T2,并且在 T2 中有很多 DML,但它们都不会被发布。如下图所示,当交易 T2 被处理时,walsender 和 walreceiver 之间的通信发生了变化。
如上可见,我们在PostgreSQL内核中将阈值设置为100(经过性能测试,确认这个阈值可以解决这个超时错误,并且不会降低性能2)。这样,当walsender按预期工作时,walreceiver就不会出现这个意外超时错误。
性能影响
最后,我将分享对空事务改进的性能测试结果。
如前所述,在对空事务的处理进行改进后,walsender 不再向 walreceiver 发送只包含 BEGIN 和 COMMIT 消息的空事务。这减少了网络流量并提高了性能。在我的测试中,我发现经过改进后,在解码空事务时,在异步逻辑复制中,walsender 会少传输 97 个字节;在同步逻辑复制中,虽然 walsender 会额外传输一条 keep-alive 消息,但总传输量仍然减少了 79 个字节。接下来,我们通过测试结果来看看网络带宽消耗方面的性能提升。
显然,walsender 传输的交易中空交易的比例影响了测试结果,所以我们测试了五种不同的空交易比例。另外,改进后,在同步逻辑复制中,如果walsender跳过一个空事务,会额外发送一条keep-alive消息,所以同步逻辑复制和异步逻辑复制都进行了测试。如下所示,异步逻辑复制和同步逻辑复制的性能都有所提高。(x 轴表示空交易在传输交易中的比例。y 轴表示 walsender 向 walreceiver 传输的数据总量,以字节为单位。)
从测试结果可以看出,空事务比例越高,提升越大。当空事务比例为 25%、50%、75% 和 100% 时,网络传输分别减少了约 8%、22%、40% 和 84%。当没有空事务时,也没有性能下降。
为未来
在这篇文章中,我解释了由于跳过空事务和修复意外超时错误而导致的逻辑复制性能和功能的改进。但是,跳过空事务的机制仍然存在一些限制。
例如,两阶段提交不会跳过空事务。这是因为如果 walsender 在准备事务和提交准备事务之间重新启动,我们无法清楚地知道准备事务是否在提交时被跳过。由于同样的问题,进行中事务的流式传输(由subscription_parameter 的参数“流式传输”设置)也没有得到改进。很快,我们可能会尝试以更好的方式改进对上述两种交易中空交易的处理。
此外,为了提高在 walreceiver 上应用正在进行的交易流的效率,我们的团队在 walreceiver 3中共享了并行应用交易的补丁,并一直在社区中积极讨论以持续改进。
原文标题:How PostgreSQL 15 improved communication in logical replication 原文作者:Wei Wang