PostgreSQL 逻辑复制为何吃满磁盘:从 pg_replslot 泄漏到 streaming 调优

2026-09-11 20 预计阅读时间: 1 分钟
来源: postgr.es AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:10 分钟

PostgreSQL 的逻辑复制通常安静得让人忘记它的存在,直到发布端的 pg_replslot 目录突然出现成千上万个文件,磁盘告警随之响起。更棘手的是,这些文件可能在大事务提交后立即消失,只留下一个恢复正常的目录和几项累计统计。

问题往往不是复制槽失效,也不一定是订阅端落后,而是逻辑解码正在为尚未提交的大事务暂存数据。订阅越多,同一份 WAL 就会被重复解码和暂存越多次。

目录里究竟存了什么

发布端会为每个复制槽维护一个 pg_replslot/<slot_name> 子目录。除了记录槽位置的状态文件,逻辑解码还可能在这里写入 spill 文件,也就是无法继续放在内存里的事务变更。

原因在于 WAL 并不按事务提交顺序排列。并发事务的 WAL 记录会交错出现,其中某个事务还可能最终回滚。逻辑解码器必须把每条变更归入对应事务,等到看到提交记录后,才能把完整事务交给输出插件和订阅端。

这批未提交变更由每个 walsender 自己的 reorder buffer 管理。它使用 logical_decoding_work_mem 作为内存预算,默认值通常为 64MB。超过预算的部分就会落盘:

WAL
  -> replication slot / walsender
  -> logical decoding
  -> reorder buffer
       -> memory, within logical_decoding_work_mem
       -> pg_replslot spill files, after exceeding the budget

目录忽然恢复为空也不代表告警是误报。事务提交后,spill 文件会被消费并删除,因此事故后的现场很容易消失。

一个订阅就是一套解码成本

逻辑复制不会让多个订阅共享同一个解码器。通常每个订阅对应一个复制槽、一个 walsender 和一个 reorder buffer。

假设一个未提交事务解码后需要暂存 120MB 数据,发布端有三个独立订阅。在旧版默认配置下,它可能被解码三次,并分别产生三套 spill 文件。WAL 只写了一次,逻辑解码和暂存成本却按订阅数量放大。

初始同步还会临时启动表同步 worker,并创建额外复制槽。这意味着容量评估不能只看稳定运行时的订阅数量,还要考虑并行初始化、重新同步和维护窗口。

可以在发布端先检查槽状态及累计的 spill、stream 指标:

SELECT
    r.slot_name,
    r.active,
    r.wal_status,
    r.safe_wal_size,
    s.spill_txns,
    s.spill_count,
    pg_size_pretty(s.spill_bytes)  AS spill_bytes,
    s.stream_txns,
    s.stream_count,
    pg_size_pretty(s.stream_bytes) AS stream_bytes
FROM pg_replication_slots AS r
LEFT JOIN pg_stat_replication_slots AS s USING (slot_name)
ORDER BY s.spill_bytes DESC NULLS LAST;

active = true 只能说明槽正在被使用,并不能证明它没有大量落盘。safe_wal_size 为空也未必异常:当 max_slot_wal_keep_size = -1 时,槽可保留的 WAL 没有该参数施加的上限。

在数据库主机上,还可以直接查看目录占用。以下命令需要以有权读取数据目录的操作系统用户运行:

du -sh ${PGDATA}/pg_replslot
du -sh ${PGDATA}/pg_replslot/* 2>/dev/null | sort -h

不要手工删除其中的文件。它们属于 PostgreSQL 的复制槽状态和逻辑解码流程,直接删除可能损坏复制槽。

streaming 决定压力落在哪里

订阅的 streaming 参数控制大事务是否必须在发布端等到提交后再发送。

  • off:发布端先完整解码并暂存事务,看到提交记录后再发送。PostgreSQL 17 及更早版本创建订阅时通常采用这一默认值。
  • on:发布端边解码边发送未提交事务。订阅端先暂存这些变更,并在收到提交后按提交顺序应用。
  • parallel:PostgreSQL 16 起可用。流式变更可以直接交给空闲的并行 apply worker;没有可用 worker 时,订阅端仍可能回退到临时文件。

PostgreSQL 18 将新订阅的默认值改为 parallel,但升级集群不会自动改写旧订阅的目录项。因此,即使数据库已经升级到 18,也应检查实际配置。

在订阅端执行下面的查询,可以找出沿用旧行为的订阅:

SELECT
    subname,
    subslotname,
    CASE substream
        WHEN 'f' THEN 'off'
        WHEN 't' THEN 'on'
        WHEN 'p' THEN 'parallel'
        ELSE substream::text
    END AS streaming_mode
FROM pg_subscription
ORDER BY subname;

确认版本和订阅端资源足够后,可以逐个调整:

-- PostgreSQL 14+ 可考虑使用 on
ALTER SUBSCRIPTION orders_sub SET (streaming = on);

-- PostgreSQL 16+ 可考虑使用 parallel
ALTER SUBSCRIPTION orders_sub SET (streaming = parallel);

orders_sub 替换为真实订阅名。该修改不需要重启数据库;订阅 worker 会重新启动并采用新配置。执行前仍应确认 max_logical_replication_workersmax_worker_processes 和并行 apply worker 配额,避免只修改模式却没有可用 worker。

可以用 pg_stat_subscription 观察 worker。分析该视图时应区分主 apply worker 与并行 worker:并行 worker 的 relid、接收 LSN 和部分消息时间字段可能为空,不能简单把每一行都当作独立订阅。

内存不是免费的替代品

提高 logical_decoding_work_mem 可以减少 spill,但它不是无成本修复。这个预算会被每个解码连接使用,还可能被初始表同步 worker 同时消耗。订阅很多时,把它全局调得很高可能将磁盘问题变成内存压力。

如果只有某个复制用户承载可预期的大事务,可以按角色设置:

ALTER ROLE logical_replication_user
SET logical_decoding_work_mem = '256MB';

将角色名和大小替换为经过压测的值。该设置作用于后续建立的复制连接;调整后应确认相关 walsender 已重新连接,并同时监控主机内存、spill 统计和复制延迟。

启用 streaming 也不等于发布端绝不会再 spill。包含大型 TOAST 值的宽行等场景,仍可能要求逻辑解码在本地暂存部分数据。正确目标不是让 spill_bytes 永远为零,而是限制峰值、避免按订阅数量失控放大,并确保告警能保留足够的取证信息。

上线前的检查清单

  1. 在订阅端读取 pg_subscription.substream,不要根据 PostgreSQL 主版本猜测配置。
  2. 在发布端监控 pg_stat_replication_slotsspill_bytesstream_bytes 和变化速率。
  3. pg_replslot 纳入磁盘监控,同时记录大事务持续时间和订阅数量。
  4. PostgreSQL 16 及以上优先评估 parallel;worker 资源不足时评估 on
  5. 调高 logical_decoding_work_mem 前,按复制槽和同步 worker 的最大并发数计算内存上界。
  6. 用包含真实行宽、TOAST 数据和长事务的压测验证配置,不要只测试许多小事务。
  7. PostgreSQL 13 及更早版本没有该 streaming 选项,应把升级作为根本治理方案。

逻辑复制的危险点不在于某一个目录,而在于资源成本的乘法关系:大事务乘以复制槽数量,再乘以它保持未提交的时间。streaming 能把主要暂存压力从发布端移走,parallel 还能进一步减少订阅端落盘,但它们都不能替代事务治理、容量计算和持续监控。


相关推荐