PostgreSQL 的复制槽(replication slot)这东西,我敢说一大半 DBA 第一次注意到它,不是在做主从复制规划的时候,而是在某个周一的早上被磁盘报警叫醒:pg_wal 目录暴涨,WAL 文件把数据盘塞得只剩几个 G。一查 pg_replication_slots,某个复制槽的 restart_lsn 好几天没动过,备库早就断开了,但主库还在傻乎乎地保留它需要的 WAL。这种事情我自己就踩过,而且踩得不轻。
所以关于 PostgreSQL 16.3 里复制槽的配置,我打算把物理复制槽、逻辑复制槽、监控清理、故障排查一次讲透。内容不是我抄文档抄来的,而是实际在 16.x 环境里配主从、做逻辑同步、对接外部工具时一步步试出来的。这篇东西适合第一次搭流复制的新手,也适合已经在生产环境维护 PostgreSQL、但一直对复制槽细节没完全吃透的人。你只要能跟着把 SQL 和配置跑一遍,基本就不会再被复制槽坑到半夜起来删 WAL。
1. 复制槽到底在解决什么问题,为什么容易踩坑
1.1 没有复制槽时,从库重启为什么可能跟不上
先回到最原始的问题:主库产生 WAL,备库通过网络拉取 WAL 并回放。如果没有复制槽,备库追主库完全是靠主库“自觉”保留 WAL。PostgreSQL 主库保留哪些 WAL 由 wal_keep_size 决定,默认是 0,意味着只要 checkpoint 把某个 WAL 段标记为可复用,主库就可能直接覆盖它。
这时候备库如果断了几分钟,网络恢复后一连接,发现主库要的起始 WAL 早就没了,只能重新做基础备份,也就是 pg_basebackup 全量拉一遍。生产环境几 TB 的库,重做一次基础备份往往要几个小时,期间还涉及数据目录切换,对业务影响非常大。
复制槽解决的就是这个“主库不知道备库需要哪个 WAL”的问题。它相当于在主库上记了一笔账:这个复制槽已经消费到哪个 LSN 了,所以这些 WAL 必须留着,哪怕 checkpoint 想清理也不行。可以说,复制槽是流复制能稳定工作的核心机制之一,也是 WAL 堆积风险的根源。这两个特性是一体两面。
1.2 复制槽的分类:物理槽与逻辑槽
PostgreSQL 的复制槽分两类,理解清楚它们的差别,后面配置才不会乱。
- 物理复制槽(physical replication slot):它记录的是备库或
pg_basebackup、pg_receivewal这类进程已经收到的 WAL 位置。物理复制走的是 WAL 原始字节流,备库拿到什么就回放什么,主备库版本必须一致或接近。物理复制槽最常见的用途就是流复制和高可用,它直接跟primary_slot_name参数绑定。 - 逻辑复制槽(logical replication slot):它记录的不是原始 WAL 字节位置,而是逻辑解码的确认点
confirmed_flush_lsn。逻辑复制会把 WAL 解析成 INSERT/UPDATE/DELETE 这类行级变更,可以跨大版本、跨数据库类型同步。最常见的场景是发布订阅、Debezium、Flink CDC、自研的同步中间件。
两类复制槽的创建函数、状态字段、清理方式都不一样,但它们的共同点是:一旦创建了,如果长时间没人消费,主库 WAL 都会被无限保留,直到磁盘爆掉。
1.3 PostgreSQL 16.x 里复制槽的变化
PostgreSQL 16.3 是 16 系列的一个补丁版本,复制槽的整体机制跟 16.0 保持一致,但这个版本里有一些需要留意的细节。16 在逻辑复制上做了不少增强,比如订阅端支持并行回放、支持从发布端进行初始数据同步时用 binary format、还有新增的 pg_createsubscriber 工具可以把物理备库转换成逻辑订阅库。这些功能其实都依赖复制槽机制,所以 16 里逻辑复制槽的使用频率比老版本高很多。
另外 Postgres 16 对复制槽的 WAL 保留上限控制也更完善,max_slot_wal_keep_size 参数从 13 就有,但 16 的校验逻辑更成熟,当复制槽落后到超出限制时,会在 pg_replication_slots 里把 wal_status 标记为 lost,而不是像老版本那样直接把实例搞崩。这个字段对我来说特别重要,后面监控部分我会专门讲。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 配置复制槽之前,建议你先确认这几件事
2.1 参数基线:wal_level、max_replication_slots、max_wal_senders
复制槽不是装完就能用的,它依赖几个核心 GUC 参数。我这里直接给出我常用的生产环境基线,16.3 同样适用:
| 参数 | 建议值 | 说明 |
|---|---|---|
wal_level |
replica(物理复制)或 logical(逻辑复制) |
默认是 replica,做逻辑复制必须改成 logical,改完要重启 |
max_replication_slots |
至少 10,按实际复制任务数评估 | 包含物理槽和逻辑槽的总数上限,默认 10 |
max_wal_senders |
至少 10,建议大于复制槽数+备份进程数 | 每个复制槽被消费时都要占用一个 walsender 进程 |
wal_keep_size |
0 或较小值 | 这是不用复制槽时的兜底保留量,有复制槽时通常不需要太大 |
max_slot_wal_keep_size |
建议设一个值,如 10GB | 防止复制槽异常导致 WAL 无限堆积,16 里设置后状态切换更明确 |
这里有个容易混淆的地方:max_replication_slots 和 max_wal_senders 是两个不同的东西。复制槽只是一个“记录消费位置”的逻辑概念,真正把 WAL 发给备库的是 walsender 进程。所以复制槽可以建很多,但如果 max_wal_senders 不够,备库连上来也会被拒绝。反过来,一个复制槽如果没有 walsender 在消费它,那它就是一个只占位置、还会卡 WAL 的“僵尸槽”。
2.2 账号权限与 pg_hba.conf
复制槽的创建、删除和消费都需要专门的复制账号。官方推荐的最小权限是 REPLICATION 权限,逻辑复制还需要 LOGIN 权限。我通常这样建账号:
sql复制CREATE ROLE replicator WITH LOGIN REPLICATION PASSWORD 'StrongPass123';
GRANT CONNECT ON DATABASE yourdb TO replicator;
pg_hba.conf 里要放行复制连接,注意 replication 和普通数据库连接是两种规则。我的配置长这样:
code复制# replication connections
host replication replicator 192.168.1.0/24 scram-sha-256
# logical replication needs normal db connection too
host yourdb replicator 192.168.1.0/24 scram-sha-256
这里有个坑:发布订阅场景中,订阅端的 walreceiver 连接发布端时,走的不是 replication 连接协议,而是普通的数据库连接协议,所以光配 replication 规则还不够,必须允许该账号能连接到具体的数据库。否则 CREATE SUBSCRIPTION 会报权限不够或者连不上。
2.3 复制槽类型与插件怎么选
物理复制槽不需要额外插件,但逻辑复制槽必须指定输出插件。PostgreSQL 16 内置了 pgoutput,发布订阅就是用这个插件;老的 test_decoding 主要用于测试和调试,不适合生产;第三方工具常见的有 wal2json、decoderbufs,需要自己编译安装,而且在大版本升级后必须重新编译。
我在生产环境的原则是:能用内置 pgoutput 就绝不用第三方插件。原因很简单:升级不用额外处理,而且 pgoutput 的协议是 PostgreSQL 官方维护的,Debezium、Flink CDC 这些工具都原生支持。只有当你明确需要 JSON 格式输出、且不想改装客户端时,才考虑 wal2json。
3. 物理复制槽配置:从主库到备库的完整步骤
3.1 在主库创建物理复制槽
在 PostgreSQL 16.3 里创建物理复制槽很简单,但我建议你在创建之前先确认参数已经改好:
sql复制-- 查看当前参数
SHOW wal_level;
SHOW max_replication_slots;
SHOW max_wal_senders;
如果 wal_level 不是 replica 或 logical,改 postgresql.conf 后必须重启。我见过有人改完参数不重启,然后创建复制槽报了 replication slots can only be used if wal_level >= replica,原地卡了半天。
然后执行:
sql复制SELECT * FROM pg_create_physical_replication_slot('standby_slot');
返回结果里会看到 slot_name 和 lsn 字段。注意,新创建的物理复制槽默认不会立即保留 WAL,它的 restart_lsn 会初始化为当前 WAL 插入位置,但只有第一次有 walsender 连接消费它之后,这个位置才真正“锁住” WAL。所以创建完复制槽,下一步必须是让备库或备份进程真正用它,否则这个槽就是个空壳。
3.2 pg_basebackup 配合复制槽做基础备份
物理复制槽最常见的用法之一是配合 pg_basebackup,避免备份期间 WAL 被清理导致备份不一致。命令大致是这样:
bash复制pg_basebackup -h 192.168.1.10 -U replicator -D /data/pgdata \
-S standby_slot -X stream -P -R
这里的 -S standby_slot 表示在 basebackup 期间使用这个复制槽来确保 WAL 不丢,同时它也会把 primary_slot_name 写入生成的 postgresql.auto.conf,也就是告诉备库:“你起来之后继续用这个复制槽”。
-R 参数会生成 standby.signal 文件并写好 primary_conninfo。这个参数组合是搭建流复制最省事的路径。需要注意,-S 指定的槽必须提前存在,如果复制槽不存在,pg_basebackup 会直接报错,不会自动帮你创建。
3.3 备库的 primary_slot_name 配置
如果你不是用 pg_basebackup -R 生成配置,而是手工搭备库,那需要在备库的 postgresql.conf 或 postgresql.auto.conf 里配:
code复制primary_conninfo = 'host=192.168.1.10 port=5432 user=replicator password=xxx application_name=standby1'
primary_slot_name = 'standby_slot'
PG12 之后已经没有 recovery.conf 了,这些参数要么写在 postgresql.conf,要么写在 postgresql.auto.conf。我个人的习惯是写在 postgresql.auto.conf,因为它是 ALTER SYSTEM 命令维护的文件,不容易被手工编辑弄乱。
确认备库配置没问题后,启动备库,观察日志:
code复制2024-05-20 10:00:01.123 CST [12345] LOG: started streaming WAL from primary at 0/50000000 on timeline 1
看到这行说明备库已经开始通过复制槽拉取 WAL。如果日志里出现 could not receive data from WAL stream: FATAL: requested WAL segment ... has already been removed,那就说明复制槽没生效,或者基础备份期间 WAL 已经断了。
3.4 验证配置是否真的生效
光看日志还不够,我一般用一个查询确认复制槽状态:
sql复制SELECT slot_name, slot_type, active, restart_lsn,
pg_current_wal_lsn() AS current_lsn,
pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn) AS lag_bytes
FROM pg_replication_slots;
active 为 true,说明有 walsender 正在消费这个槽。lag_bytes 是当前写入位置到这个槽重新开始位置的 WAL 距离,正常情况下应该很小,几 MB 以内。
再配合 pg_stat_replication 看备库的实际回放情况:
sql复制SELECT client_addr, application_name, state, sync_state,
sent_lsn, write_lsn, flush_lsn, replay_lsn
FROM pg_stat_replication;
replay_lsn 跟上 flush_lsn,说明备库回放没有落后。这两个查询是我排查复制问题时的“第一板斧”,信息量比日志大多了。
4. 逻辑复制槽配置:从发布订阅到外部同步工具
4.1 什么场景必须用逻辑复制槽
物理复制槽做的是全实例级别的同步,没法挑库、挑表,也没法跨大版本做数据迁移。逻辑复制槽就不一样了,它可以把某几张表的变更解析出来,发给另一个数据库,甚至发给 Kafka。PostgreSQL 16 里逻辑复制有几个高频使用场景:
- 同一套数据分流到多个业务库,只同步部分表;
- 从旧版本 PostgreSQL 平滑迁移到新版本,比如从 14 升到 16;
- 对接 CDC 工具,消费增量变更写入数仓或消息队列;
- 做主备之外的“分析副本”,让分析应用直接读逻辑解码后的数据。
这些场景如果没有逻辑复制槽,做起来会很痛苦,只能靠轮询或触发器同步,效率和一致性都不行。
4.2 手动创建逻辑复制槽并解码
先确认 wal_level 已经是 logical,然后创建逻辑复制槽:
sql复制SELECT * FROM pg_create_logical_replication_slot('logical_slot', 'pgoutput');
这个函数第二个参数是输出插件名称。pgoutput 是 PG10+ 内置的,16 里直接用就行。
创建完之后,你可以用 pg_logical_slot_peek_changes 看看有没有数据变更:
sql复制SELECT * FROM pg_logical_slot_peek_changes('logical_slot', NULL, NULL);
如果有人更新了表里的数据,这个查询应该能看到一堆二进制编码的变更内容。真正在生产环境,你不会这样手工消费,而是通过发布订阅或 CDC 工具连上这个槽。
4.3 通过 CREATE SUBSCRIPTION 自动管理复制槽
最常用的逻辑复制槽使用方式是发布订阅。在发布端创建发布:
sql复制-- 发布端
CREATE PUBLICATION mypub FOR TABLE orders, users;
在订阅端创建订阅,同时指定复制槽名称:
sql复制-- 订阅端
CREATE SUBSCRIPTION mysub
CONNECTION 'host=192.168.1.10 port=5432 dbname=yourdb user=replicator password=xxx'
PUBLICATION mypub
WITH (slot_name = 'logical_slot');
执行这个命令后,PostgreSQL 会在发布端自动创建 logical_slot 这个逻辑复制槽,并且由订阅端的 walreceiver 进程消费它。这是最稳妥的使用方式,因为复制槽的生命周期由 PG 自己管理:订阅删除时槽也会被清掉(前提是 with (drop_slot_on_error) 之类的行为你理解清楚)。
这个场景里有一个常见坑:如果在发布端手动创建了 logical_slot,再用 CREATE SUBSCRIPTION 指定同一个名字,PG 会认为槽已存在并要求复用。复用规则是“同一个数据库、同一个插件、同一个 publication”,任何一个对不上都会报错。所以我的建议是:如果数据库同步工具能自动建槽,就不要手工创建;如果必须手工创建,一定要保证插件和数据库名完全匹配。
4.4 对接数据同步工具时要注意的参数
很多 CDC 工具,比如 Debezium,会强制要求指定一个逻辑复制槽,并且默认把 max_slot_wal_keep_size 设为无限,意思是不限制 WAL 保留。工具连不上或者消费速度慢,WAL 就会一直堆。
我在对接工具时的建议是:
- 给逻辑复制槽单独设
max_slot_wal_keep_size,不设 0,比如设 20GB。这样如果工具挂了很久,复制槽不会无限拖垮主库磁盘。 - 监控
pg_replication_slots里confirmed_flush_lsn的变化。如果这个值一直不动,说明消费端没有确认消息,主库 WAL 在无脑堆积。 - 配置多个复制槽时,每个工具用独立槽,不要共享。共享会互相干扰,A 工具消费慢了 B 工具也跟着遭殃。
5. 复制槽的监控、告警与安全清理
5.1 一张 SQL 看清所有复制槽状态
PG 的 pg_replication_slots 视图在 16 里字段很多,但真正值得盯的就几个。我日常用的是这条 SQL:
sql复制SELECT slot_name,
slot_type,
active,
wal_status,
safe_wal_size,
restart_lsn,
confirmed_flush_lsn,
pg_current_wal_lsn() AS current_lsn,
pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), coalesce(restart_lsn, confirmed_flush_lsn))) AS lag
FROM pg_replication_slots;
active:是否有 walsender 正在消费。wal_status:reserved表示 WAL 保留正常;extended表示 WAL 保留超过max_slot_wal_keep_size;lost表示这个槽要求的 WAL 已经被删了,槽基本废了。safe_wal_size:距离 WAL 被强制删除还有多少空间/大小,只有wal_status='reserved'时有意义。lag:物理槽看restart_lsn,逻辑槽看confirmed_flush_lsn。
我建议把这条 SQL 做成一个监控脚本,每分钟跑一次。如果 active 一直是 false 且 wal_status 变为 extended,就要立刻报警。这就是典型的“复制槽活着,但消费者死了”的状态。
5.2 如何判断 WAL 堆积是不是复制槽引起的
WAL 堆积的原因不止复制槽一种:archive_mode 开了但 archive_command 挂了、wal_keep_size 配太大、checkpoint 参数不合理,都会导致 pg_wal 目录膨胀。要快速定位是不是复制槽的锅,看磁盘占用的时候顺手查一下:
sql复制SELECT slot_name,
pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS slot_lag
FROM pg_replication_slots
WHERE slot_type = 'physical'
OR (slot_type = 'logical' AND confirmed_flush_lsn < pg_current_wal_lsn());
如果任何一个槽的 slot_lag 值在持续增长,而 active=false,那问题基本就是它。这时候的第一反应是“恢复消费”,而不是“删槽重来”。因为直接删槽虽然能立刻释放 WAL,但备库或工具会失去消费位置,可能需要全量重来。
5.3 正确删除复制槽,以及自动清理策略
确认某个复制槽确实不需要了,再执行删除,语法很简单:
sql复制SELECT pg_drop_replication_slot('standby_slot');
但要注意,如果复制槽正在被消费(active=true),pg_drop_replication_slot 会直接报错:“replication slot is active for PID”。你需要先停掉对应的 walsender 进程,通常是停备库或停 CDC 工具,然后再删。
自动清理策略上,我的经验是分两层:
- 对于物理复制槽,备库在
primary_slot_name里配置好了,一般不需要自动清理。但如果你的备份脚本用的是临时复制槽(比如pg_basebackup每次创建临时槽),备份结束会自己清理。PG16 里创建复制槽时可以显式指定temporary => true,临时槽在会话结束或备份结束时自动清理,非常省心。 - 对于逻辑复制槽,如果某个工具已经下线了,应该由运维流程负责删除,不要留着。我见过太多“先留着吧,万一以后用得上”的逻辑槽,最后全变成磁盘炸弹。
5.4 备份与归档场景下的复制槽细节
如果你用 pg_receivewal 做实时 WAL 归档,我强烈建议给它单独建一个复制槽:
bash复制pg_receivewal -h 192.168.1.10 -U replicator -D /wal_archive -S archive_slot
这样归档进程断线时,主库会保留它没接收的 WAL,不会因为归档缺口导致后续恢复失败。它的原理跟流复制一样,只是消费端变成了本地 WAL 文件。
归档场景的监控重点不再是 pg_stat_replication,而是复制槽的 restart_lsn 与最新 WAL 的差距。如果归档目录增长正常,但复制槽 lag 不降,多半是归档进程没跑起来。
6. 常见故障排查与我的避坑记录
6.1 备库重启后追不上主库
这个场景我遇到过两次,典型的错误信息是:
code复制FATAL: requested WAL segment 0000000100000000000000A0 has already been removed
问题根源通常是复制槽没生效。备库配置了 primary_conninfo,但 primary_slot_name 没写对,或者写进了一个不存在的槽名。排查顺序我建议这样:
- 看备库
pg_replication_slots:备库是从库,一般不建槽,直接看主库。 - 看主库
pg_stat_replication:如果state=streaming,说明连接正常。 - 看主库
pg_replication_slots:确认active=true且restart_lsn在备库要求的 LSN 之前。 - 如果槽是
active=false,说明备库发了连接,但没用这个槽,大概率primary_slot_name没生效。
一个容易被忽略的地方:pg_basebackup -R 生成的 postgresql.auto.conf 里如果没有 primary_slot_name,可能是因为你用了 -S 但指定的槽名不存在。备份过程中 pg_basebackup 会报错,但有些脚本吞掉了错误,最终配置就是残缺的。
6.2 复制槽状态变成 invalid 或 wal_status 为 lost
PG 16 里,如果复制槽要求的 WAL 已经被清理,pg_replication_slots 里的 wal_status 会变成 reserved -> extended -> lost。一旦变成 lost,这个复制槽基本救不回来了,你想让它恢复消费都做不到,因为数据缺口太大。restart_lsn 指向的 WAL 段已经不存在,备库只能重新做 pg_basebackup。
应对措施就两条路:
- 如果这个槽还在被备库使用,删掉旧槽,重新创建新槽,然后备库全量重做。
- 如果是逻辑复制槽,而且工具对数据一致性要求极强,那必须重新初始化订阅,不能硬接。
我还有一个操作习惯:创建完任何复制槽,都会顺手记下 restart_lsn 和期望的消费端。万一出了问题,至少知道这个槽对应的是哪套环境,不会误删。
6.3 级联复制场景的复制槽配置
PostgreSQL 14 之后支持从备库再级联下游备库,16 同样支持。级联复制的复制槽配置有个细节:中间备库需要开启 wal_level=replica 或更高,并且要为下游备库创建物理复制槽。
操作是这样:在中间备库执行 pg_create_physical_replication_slot,然后下游备库的 primary_conninfo 指向中间备库,primary_slot_name 指向刚创建的槽。这里一定要理解,级联场景下,复制槽建在中间备库,而不是主库。主库的复制槽只负责把 WAL 发给中间备库,中间备库再通过自己的复制槽把 WAL 发给下游。
级联复制排查起来比普通主从麻烦,因为任何一层出问题都会表现为最下面那层 lag。我建议每层都单独查 pg_stat_replication,不要只查最终端。
6.4 几个实战经验总结
最后分享几条我压箱底的经验,不是文档里能直接抄到的:
- 创建复制槽前先确认
max_slot_wal_keep_size有没有设成 0。 生产环境我建议设成一个明确大小,比如 20GB。它不会影响正常运行,只会在复制槽异常时兜底,避免磁盘写满。 - 逻辑复制槽的
confirmed_flush_lsn比物理槽的restart_lsn更值得关注。 物理槽只要连接活着,一般不会出大事;逻辑槽即使连接活着,如果消费端逻辑卡住(比如慢 SQL、网络阻塞),confirmed_flush_lsn也会停住,WAL 照样堆。 - 不要手贱去删
pg_wal里的文件。 磁盘满的时候最上头的操作就是rm -rf pg_wal/*,这会让整个实例崩溃且无法恢复。正确做法是定位问题槽、停 walsender、删槽,让 PG 自己清理 WAL。 - 每次 PostgreSQL 大版本升级,逻辑复制槽需要重新验证。 物理槽一般跟着实例走,逻辑槽里的解码状态在大版本升级后可能不兼容,最好在升级窗口里重建。
- 数据库同步软件(Debezium、Flink CDC 之类)建议每个任务单独一个槽。 不要图省事复用槽,否则一个任务重启会阻塞其他任务的解码进度,问题极其难排查。
复制槽这东西,配置本身不难,真正难的是理解它背后“谁在消费、消费到哪里、WAL 保到哪”这三个问题。你把这三个问题想清楚了,PostgreSQL 16.3 的复制槽配置对你来说就是顺手的事。后面如果再遇到备库追不上主库的情况,先深呼吸,然后翻开这篇文章,按 5.1 的 SQL 查一遍,你大概率已经找到答案了。
