一月 27, 2026
摘要:在本教程中,您将学习如何检查 PostgreSQL 中阻塞 VACUUM 的复制槽。
目录
逻辑复制延迟
逻辑复制会读取 WAL 记录、对其进行解码,并将解码后的内容发送给订阅端。其中关键一点是:解码过程需要用到生成该 WAL 记录时 “过去某个时间点” 的系统表状态。
举个例子,当解码某张表的 WAL 时,若该表此前有一列被删除,解码器必须引用 “列被删除前” 的表定义(而非当前定义)才能完成正确解码。因此,该时间点所需的系统表死元组必须保留,直到逻辑复制槽处理完对应的 WAL 为止。
接下来我们创建一个用于测试的逻辑复制槽:
SELECT pg_create_logical_replication_slot('test_slot', 'test_decoding');
pg_create_logical_replication_slot
------------------------------------
(test_slot,0/017BBC00)
(1 row)
删除一个已存在的表,并对pg_class系统表执行 VACUUM 操作:
DROP TABLE t;
VACUUM (verbose) pg_class;
INFO: 00000: vacuuming "postgres.pg_catalog.pg_class"
INFO: 00000: finished vacuuming "postgres.pg_catalog.pg_class": index scans: 0
pages: 0 removed, 14 remain, 14 scanned (100.00% of total), 0 eagerly scanned
tuples: 0 removed, 418 remain, 1 are dead but not yet removable
removable cutoff: 780, which was 1 XIDs old when operation ended
new relfrozenxid: 779, which is 19 XIDs ahead of previous value
frozen: 0 pages from table (0.00% of total) had 0 tuples frozen
visibility map: 0 pages set all-visible, 0 pages set all-frozen (0 were all-visible)
index scan not needed: 0 pages from table (0.00% of total) had 0 dead item identifiers removed
avg read rate: 0.000 MB/s, avg write rate: 16.421 MB/s
buffer usage: 45 hits, 0 reads, 4 dirtied
WAL usage: 4 records, 4 full page images, 32658 bytes, 32392 full page image bytes, 0 buffers full
memory usage: dead item storage 0.02 MB accumulated across 0 resets (limit 64.00 MB each)
system usage: CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s
消息tuples: 0 removed, 418 remain, 1 are dead but not yet removable表明有一个死元组未被回收。而removable cutoff: 780, which was 1 XIDs old when operation ended则说明事务号 780 是导致该问题的根源。
VACUUM 被 xmin 水位线阻塞
一个事务中的 MVCC 快照,存储了有关特定时刻事务 ID 可见性的信息。快照中会有一个名为xmin的组件,它表示了仍处于活动状态的最低事务 ID。所有小于xmin的事务 ID 要么提交且可见,要么回滚已死亡。VACUUM 可能会被xmin水位线阻塞。
xmin水位线告诉您 VACUUM 进程可以清理死行的最高水位。当这个值落后且不往前推进时,VACUUM 将被阻塞,并且不能清理死行。
当 VACUUM 被阻塞并且死行不能被清理时,它会导致表膨胀和查询速度变慢。
xmin水位线是所有数据库中最老的xmin,这汇总为一个服务器范围的指标。请注意,虽然某些类型的xmin水位线延迟,会影响服务器上的所有数据库,例如每个物理复制槽的xmin,但在另外一些情况下,并非服务器上的所有数据库都会受到影响,因为 PostgreSQL 仍然可以清理不相关数据库中表的死行。例如,如果xmin水位线被 1 号数据库上的长时间运行的事务所阻塞,PostgreSQL 仍然可以清理 2 号数据库中表的死行,因为 1 号数据库的事务永远不可能看到 2 号数据库。
滞后或过时的物理复制槽
在启用物理流复制并设置hot_standby_feedback时,如果复制滞后或副本服务器异常(例如,停机),则复制槽需要数据库保留的最老事务可能会“卡住”,从而阻碍了xmin水位线的推进。
一个复制槽是一种数据结构,它可以防止 PostgreSQL 服务器丢弃备用服务器仍然需要的信息,以让备用服务器赶上主服务器。如果复制延迟或备用服务器停机,则复制槽将阻止VACUUM删除旧行。
方案
您可以使用下面查询,查找所有复制槽及其xmin值:
SELECT slot_name, database, xmin
FROM pg_replication_slots
WHERE slot_type = 'physical'
ORDER BY age(xmin) DESC;
如果不再使用复制槽,可以通过运行下面命令将其删除:
SELECT pg_drop_replication_slot('<slot_name>');
滞后或过时的逻辑复制槽
使用逻辑复制槽时,当 DDL 更改(数据库迁移)未应用至一个副本服务器(订阅服务器)时,复制也会过时。当订阅端由于表结构不匹配而无法复制数据时,复制会出错并过时。这会导致主服务器(发布端)的系统表的xmin水位线停止推进,直到复制恢复。
方案
您可以使用下面查询,查找所有复制槽及其catalog_xmin值:
SELECT slot_name, database, catalog_xmin
FROM pg_replication_slots
WHERE slot_type = 'logical'
ORDER BY age(catalog_xmin) DESC;
您可能还需要检查发布端和订阅端服务器上的日志,以了解有关逻辑复制的任何错误消息,例如表结构差异或wal_sender_timeout/wal_receiver_timeout设置值过低,导致复制进程在一段时间后退出,而没有往前推进。
如果不再使用复制槽,可以通过运行下面命令将其删除:
SELECT pg_drop_replication_slot('<slot_name>');