← 返回首页

PostgreSQL 复制槽吃光磁盘:18.6 实测与 6 个真实报错

PostgreSQL数据库运维

TL;DR:复制槽是 PostgreSQL 给订阅者留的一份「承诺」——你说会回来读,主库就把 WAL 一直留着。只要没人读,这份承诺就没有上限。

说明:本文不含任何商品或服务的推荐。页面底部若出现推广区,由站点模板统一渲染,与本文结论无关。

一个没人读的槽,是怎么把磁盘喂满的

复制槽(replication slot)解决的问题很朴素:订阅端掉线了,主库不能因为「你没在听」就把 WAL 删掉,否则订阅端回来时会发现中间那段日志没了,只能整套重来。所以主库为每个槽记了一个 restart_lsn——「这个槽最远可能要读的位置」——checkpoint 清理 WAL 时,凡是比所有槽的 restart_lsn 还新的段,一律不能删。

这条约束本身没错。坏就坏在它默认没有边界。

我实际测试的环境是这样搭的(读者可以原样复现):从 EDB 官方站点下载 PostgreSQL 18.6 的 Windows 二进制包,initdb 起一个实例,把 wal_level 调成 logical,然后建一个逻辑槽,故意一直不去读它。

initdb.exe -D pgdata -U postgres --auth=trust -E UTF8 --locale=C
psql -c "ALTER SYSTEM SET wal_level = 'logical';"
pg_ctl -D pgdata restart
psql -d pub_demo -c "SELECT * FROM pg_create_logical_replication_slot('stale_sub','pgoutput');"

实测一:默认 -1,WAL 一路涨,checkpoint 一点用都没有

先把关键 GUC 的出厂值打出来,这是整件事的起点:

SELECT name, setting, unit, boot_val FROM pg_settings
 WHERE name IN ('max_slot_wal_keep_size','wal_level','logical_decoding_work_mem');

实测输出(boot_val 是编译进二进制的默认值):

namesettingunitboot_val
max_slot_wal_keep_size-1MB-1
logical_decoding_work_mem65536kB65536
wal_levellogicalreplica

注意两件事:max_slot_wal_keep_size 的出厂值是 -1,wal_level 的出厂值是 replica。前者是本文的主角,后者意味着你不开 logical 就建不了逻辑槽。

接下来往发布表里灌数据,每批 120000 行(行里带一个 200 字符的文本列,保证 WAL 有体积),每批结束跑一次 CHECKPOINT,然后看槽到底扣住了多少 WAL:

SELECT slot_name, wal_status,
       pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS retained,
       pg_size_pretty(safe_wal_size) AS safe_wal
  FROM pg_replication_slots;
阶段retainedwal_statussafe_wal_size
建槽时56 bytesreservedNULL
第 1 批后 + CHECKPOINT40 MBreservedNULL
第 2 批后 + CHECKPOINT79 MBreservedNULL
第 3 批后 + CHECKPOINT119 MBreservedNULL
第 4 批后 + CHECKPOINT159 MBreservedNULL

四次 checkpoint,回收量是零。 wal_status 从头到尾是 reserved,safe_wal_size 从头到尾是 NULL。官方文档对 safe_wal_size 的说明是「还能写入多少 WAL 而不至于把这个槽置为 lost;当 max_slot_wal_keep_size 为 -1 时为 NULL」——换句话说,默认配置下这个安全阀根本不给你读数。

这就是「磁盘半夜被 pg_wal 撑爆」的标准成因:不是写入量突然暴涨,是有一份谁也没在消费的承诺,安静地躺在那里按写入速率线性增长。

实测二:给它一个 16 MB 上限,看 wal_status 走完全程

我把 max_slot_wal_keep_size 设成 16 MB,重跑同样的实验,但这次每批之间不主动 checkpoint,好抓到中间态:

ALTER SYSTEM SET max_slot_wal_keep_size = '16MB';
SELECT pg_reload_conf();
阶段retainedwal_statussafe_wal_size
建槽时112 bytesreserved28 MB
第 1 批后(无 checkpoint)15 MBreserved13 MB
第 2 批后(无 checkpoint)23 MBreserved5169 kB
第 3 批后(无 checkpoint)31 MBunreserved-3305 kB
第 5 批后(无 checkpoint)48 MBunreserved-20 MB
第 7 批后(无 checkpoint)64 MBunreserved-36 MB
执行一次 CHECKPOINT—lostNULL(reason = wal_removed)

三个值得记住的点:

第一,safe_wal_size 会变成负数。它不是「剩余空间」的严格上限,而是「上限减去当前保留量」的差值,越线之后就是负的。所以监控里看到它变负,就已经在危险区了——只是还没到 checkpoint,WAL 还没被物理删除。

第二,官方文档给 wal_status 定义了四个取值:reserved(保留量在 max_wal_size 之内)、extended(超过 max_wal_size 但仍被槽或 wal_keep_size 留住)、unreserved(已越线,部分 WAL 将在下个 checkpoint 被删)、lost(槽废了)。我这次实测跑到了 reserved → unreserved → lost;extended 需要保留量同时越过 max_wal_size(我这里是 1GB),我没有实测到这一档,这里只引用文档定义。

第三,unreserved 是可以回头的——文档原话是 "This state can return to reserved or extended"。只要订阅端赶上来,槽还能救。真正不可挽回的是 lost。

实测三:逻辑解码也会把内存撑爆,落盘次数可查

复制槽的另一面是逻辑解码。一个大事务在解码时要先在内存里重组,超过 logical_decoding_work_mem 就 spill 到磁盘。这个量是可以查的,视图叫 pg_stat_replication_slots。

为了在普通机器上复现,我把 logical_decoding_work_mem 从默认 64MB 降到 1MB,然后提交一个 20000 行的 UPDATE 事务再解码:

SET logical_decoding_work_mem = '1MB';
SELECT count(*) FROM pg_logical_slot_get_changes('ord_sub', NULL, NULL);
-- 20,002 行变更被解出
SELECT slot_name, total_txns, total_bytes,
       spill_txns, spill_count, spill_bytes
  FROM pg_stat_replication_slots;
slot_nametotal_txnstotal_bytesspill_txnsspill_countspill_bytes
ord_sub14,976,004154,976,004

spill_count = 5、spill_bytes = 4,976,004——和 total_bytes 完全相等,意思是这 4.7 MB 的变更一分都没留在内存里,全落盘了。所以「解码慢」很多时候不是 CPU 问题,是 logical_decoding_work_mem 给小了。

实测四:PostgreSQL 18 的空闲槽自动失效(默认为关)

PG 18 的发布说明里有一行:「Allow inactive replication slots to be automatically invalidated using server variable idle_replication_slot_timeout」。我在 18.6 上查到它确实存在,且出厂值是 0(关闭):

ALTER SYSTEM SET idle_replication_slot_timeout = '10s';
SELECT pg_reload_conf();
-- 等一会儿,再触发一次 checkpoint
CHECKPOINT;
SELECT slot_name, wal_status, invalidation_reason FROM pg_replication_slots;
slot_namewal_statusinvalidation_reason
ord_sublostidle_timeout
stale_sublostidle_timeout
idle_demoreserved(刚建,还没到 10s)

注意它不是定时器:我等了 30 秒槽的状态没变,是手动触发 CHECKPOINT 之后才生效的。也就是说这个开关的检查发生在 checkpoint 阶段,你的 checkpoint_timeout 有多长,失效就有多迟钝。

它和 max_slot_wal_keep_size 解决的是同一类问题的两个方向:后者限制空间,前者限制时间。生产上建议两个都设。

排查三连:三条可以直接复制的 SQL

第一条,列出所有槽,按危险程度排序,一眼看出谁在扣 WAL:

SELECT slot_name, slot_type, plugin, active, wal_status,
       pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS retained,
       safe_wal_size, invalidation_reason, inactive_since
  FROM pg_replication_slots
 ORDER BY pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn) DESC NULLS LAST;

第二条,找出「已经废了但还占着位置」的槽(这些必须人工清理,它们不会自己消失):

SELECT slot_name, invalidation_reason
  FROM pg_replication_slots
 WHERE invalidation_reason IS NOT NULL;

第三条,看逻辑解码有没有在疯狂落盘:

SELECT slot_name, total_txns, total_bytes,
       spill_count, spill_bytes, stream_txns, stream_bytes, stats_reset
  FROM pg_stat_replication_slots
 WHERE spill_bytes > 0;

踩坑录:6 个真实报错与修法

下面 6 条全部是我在 PostgreSQL 18.6 上真实跑出来的原文,不是「典型报错」的复述。

报错一:槽已失效

ERROR:  can no longer access replication slot "stale_sub"
DETAIL:  This replication slot has been invalidated due to "wal_removed".

这是我上面实测二跑出来的结果。修法只有一条:删掉重建。

SELECT pg_drop_replication_slot('stale_sub');
-- 订阅端先解绑再删:ALTER SUBSCRIPTION x SET (slot_name = NONE); DROP SUBSCRIPTION x;

注意不能只删订阅。订阅删了槽还在(除非你先 ALTER SUBSCRIPTION x SET (slot_name = NONE)),这是最常见的「我明明关掉了订阅为什么 WAL 还在涨」。

报错二:槽名不能有大写字母

ERROR:  replication slot name "BadSlot" contains invalid character
HINT:  Replication slot names may only contain lower case letters, numbers, and the underscore character.

订阅名和槽名都受这条限制,从别的数据库迁移过来的人特别容易踩。

报错三:槽名重复

ERROR:  replication slot "dup_slot" already exists

槽名是集群级唯一的,不是库级。同一个实例上两个库建同名槽会撞。

报错四:发布不存在的表

ERROR:  relation "no_such_table" does not exist

来自 CREATE PUBLICATION p2 FOR TABLE no_such_table;。写自动化脚本时记得先确认表在发布库里存在。

报错五:跨库访问槽

ERROR:  replication slot "ord_sub" does not exist

逻辑槽绑定在具体 database 上(pg_replication_slots.database 列)。你在 postgres 库里查一个建在 pub_demo 里的槽,看到的就是「不存在」,而不是「没有权限」——这个误导性很强,排查时先确认自己连对了库。

报错六:用 pgoutput 槽去调 pg_logical_slot_get_changes

ERROR:  option "proto_version" missing
CONTEXT:  slot "ord_sub", output plugin "pgoutput", in the startup callback

这是我第一次做解码实测时直接撞上的。pgoutput 是给订阅端用的协议插件,必须带 proto_version / publication_names 这些选项,手工调不了。想手工看解码内容,建槽时用 test_decoding:

SELECT * FROM pg_create_logical_replication_slot('ord_sub','test_decoding');

上限到底该设多少

给一个能落到数字上的算法。设你要容忍订阅端最长掉线 T 小时,业务峰值写入速率是 R MB/小时,那么:

max_slot_wal_keep_size ≈ T × R × 1.5

那个 1.5 是余量——safe_wal_size 的计算是相对 restart_lsn 的,而 restart_lsn 的推进有滞后,实测里它就一直比 confirmed_flush_lsn 靠后。宁可让槽失效(重建订阅是可恢复的),也不要让磁盘满(那是不可恢复的)。

监控上,我最建议盯的不是磁盘剩余空间,而是 pg_replication_slots 里的 retained 字节数排序——磁盘告警响的时候你已经在救火了,retained 涨起来的时候你还有几个小时窗口。

常见问题

Q:物理复制槽也会这样吗?

会。pg_replication_slots 里 slot_type = 'physical' 的槽同样持有 restart_lsn,同样受 max_slot_wal_keep_size 约束。备库长时间掉线的后果一模一样。

Q:wal_keep_size 能不能替代这个参数?

不能。wal_keep_size 是无条件保留的 WAL 量,跟槽无关;max_slot_wal_keep_size 是给单个槽设的天花板。两者叠加生效,都要算进磁盘预算。

Q:槽失效后订阅端会自动重连吗?

不会自动恢复。订阅端的 apply worker 会一直重试并报错,你必须删掉订阅和槽,重新做一次初始同步。

Q:只看 pg_stat_replication 够不够?

不够。pg_stat_replication 只显示当前连着的发送进程,真正把磁盘吃掉的恰恰是那些不在里面的槽。查槽必须查 pg_replication_slots。

Q:我能不能直接把 pg_replslot 目录删掉?

不要。槽的元数据在共享内存和磁盘两处,pg_drop_replication_slot() 才是唯一正确的清理入口。手工删目录会让实例在下次 checkpoint 时行为异常。

结语

复制槽这件事的教训不在技术细节,而在默认值:PostgreSQL 默认信任你会回来读。这个信任在测试环境无害,在生产环境就是一颗按写入速率计时的炸弹。花两分钟把 max_slot_wal_keep_size 和 idle_replication_slot_timeout 设上,比事后半夜起来删 WAL 划算得多。

如果你正在做版本升级,可以先看我这篇 PostgreSQL 17 升级实战:逻辑复制免重同步与增量备份——pg_upgrade 从 17 起能保留逻辑槽,正好和本文的槽生命周期接得上。已经在线上遇到卡顿的,PostgreSQL 线上卡顿排查:用 pg_wait_events 找出「谁在等谁」 讲的是另一半故事:槽扣住 WAL 之后,会话排在锁上的样子。

截至发稿,本文所有数字均取自本机 PostgreSQL 18.6(PostgreSQL 18.6 on x86_64-windows, compiled by msvc-19.44.35228, 64-bit)实跑输出,官方文档口径引自 PostgreSQL 18 文档的 pg_replication_slots 视图页与 Release 18 发布说明。版本与行为请以你自己的实例为准。

👉 立即参与小米 MiMo 开放平台:国内领先的 AI 大模型开放平台,高性价比推理服务

👉 立即参与阿里云 AI:汇集爆款 AI 产品,热门模型专属权益优惠券助力企业创新加速

📌 本文由 AI 辅助生成并经人工审核发布 | TechPassive — AI 驱动的内容测试站点,专注于效率工具与 SaaS 真实评测

🔗 精选推荐工具

使用以下链接支持我们持续产出高质量内容(点击可直接前往购买):

☁️ DigitalOcean 云服务器 ⚡ Vultr 高性能 VPS 🤖 QoderWork 中国版(推荐有奖) ☁️ 阿里云爆款 AI 产品 📚 WordPress 实用书单 🔍 WordPress SEO 书单 🌐 虚拟主机书单 🐳 Docker 书单 🐧 Linux 书单 🐍 Python 书单 💰 联盟营销书单 💵 被动收入书单 🖥️ 服务器书单 ☁️ 云计算书单 🚀 DevOps 书单 🤖 小米 MiMo 开放平台
← 返回首页