逻辑复制从 Postgres 10 起就是内置功能,但很少有人注意到 CREATE SUBSCRIPTION 语法里那个不起眼的 WITH 子句。它展开后有十多个选项,其中几个直接决定逻辑复制如何消耗存储资源。默认情况下,不带任何参数的订阅完全能正常工作,所以大多数生产环境的订阅都从未调整过这些选项。
问题往往在不知不觉中出现:生产系统挂着多个下游逻辑副本,某天磁盘监控突然报警,pg_replslot 目录被成千上万个匿名文件塞满。这些文件是逻辑解码过程中产生的 spill 文件——当解码事务超出 logical_decoding_work_mem(默认 64MB)的内存预算时,剩余内容会被写入磁盘。每个复制槽对应一个 walsender 进程,各自维护独立的 reorderbuffer,N 个订阅就意味着 N 份解码、N 份 spill,互不共享。
根本原因在于 walsender 必须等待事务提交记录出现,才能把完整事务交给输出插件发送给订阅端。WAL 并非按提交顺序写入,并发事务的 WAL 记录交错排列,还可能中途回滚,所以解码出的变更只能先暂存在 reorderbuffer 里。内存放不下就溢出到 pg_replslot 目录,长事务或大事务会让 spill 文件迅速膨胀。Postgres 17 及更早版本中 streaming 参数默认 off,发布端必须先把整个事务解码并存储完毕才能传输。
实测中,一个 30 万行的单事务在三个订阅节点上各产生 114MB spill 文件,发布端 pg_replslot 目录总计占用 342MB。同一份 WAL 被解码三次、写盘三次。把 logical_decoding_work_mem 调低到 64kB 后,单事务 spill 次数高达 1775 次。更隐蔽的是,事务提交后目录立即清空,只剩累计计数器,现场证据几乎为零。
Postgres 18 已把 streaming 默认值从 off 改为 on,发布端解码后直接转发给订阅端,不再本地落盘。但 TOAST 大字段仍可能触发本地 spill,只是暴露面大幅缩小。Postgres 16 起还支持 parallel 模式,变更直接交给订阅端的并行 apply worker,连临时文件都不需要。pg_stat_subscription 视图里能看到 parallel apply worker 的行,其 leader_pid 为空。
Postgres 17 及更早版本的用户只需一条 ALTER SUBSCRIPTION ... SET (streaming = on) 即可启用流式传输,无需重启,apply worker 会自动重启生效。Postgres 13 及更早版本不支持 streaming 选项,建议尽快升级。文档对 reorderbuffer 和 spill 风险的描述相当简略,DBA 排查时很难联想到这个参数。检查一下自己的订阅配置,pg_subscription 表里 substream 列一眼就能看出当前设置。
#开发者 #工具 #Postgres #逻辑复制 #数据库 #DBA #流式复制
@DevToolboxHub