干PostgreSQL运维这些年,我越来越确定一件事:复制槽配得好不好,直接决定你半夜会不会被监控报警电话叫醒。尤其是16.x这个版本线,逻辑复制、流复制、高可用集群全都在用复制槽,可不少刚接触PG的DBA对它还停留在“知道有这个东西、会创建、会删除”的层面。今天就用16.3这个版本,结合我自己在生产和测试环境里反复踩过的坑,把复制槽的配置从头到尾捋一遍。这篇文章不聊虚的,从参数规划、物理复制槽、逻辑复制槽、监控维护到故障排查,全部给出可以直接抄作业的操作。
1. 复制槽到底是什么,没它为什么不行
1.1 一次备库故障引发的思考
先说个真实案例。有次我维护的一套主备环境,备库所在机房做网络割接,断连了大概三个小时。恢复网络后,备库启动,结果日志里刷出一行致命错误:requested WAL segment has already been removed。备库起不来,只能重新做一次全量pg_basebackup,整整折腾了一个多小时。当时主库的wal_keep_size配的是1GB,主库业务压力大,三个小时产生的WAL早就超过了这个值,旧WAL被循环复用掉了。备库想追也追不上。
这个问题的根源,就是主库并不知道备库到底消费到了哪个WAL位置。它只知道“我保留最近1GB的WAL”,超了就删。复制槽就是来解决这个问题的:它像一个书签,把每个消费者的当前位置记录在案。只要这个书签还存在,主库的WAL清理机制就永远会保留从书签位置到当前最新位置之间的所有WAL,哪怕是几十个GB,页不会删。
1.2 物理复制槽和逻辑复制槽的分工
复制槽分两种,工作机制有本质区别。物理复制槽服务于流复制场景,它记录的是备库回放到的WAL位置(restart_lsn),作用很纯粹:保证备库需要的WAL不被回收。逻辑复制槽除了干这件事,还要额外处理一个问题——逻辑解码需要访问系统目录的历史版本。
我以前调试逻辑复制时犯过一个错误:在发布端建了逻辑复制槽,然后手动执行VACUUM清理了一张频繁更新的表,结果消费者直接报错:could not map filenumber "xxx" to relation OID。原因就是复制槽里记录了一个catalog_xmin,它保护的是解码所需的历史系统目录快照。VACUUM在清理元组时,会检查这个catalog_xmin,只要某个旧版本元组还被它引用着,就不会被物理清理。物理复制槽完全没有这个维度,它只管WAL。
所以理解复制槽,要抓住三条线:WAL保留线(restart_lsn)、逻辑解码快照线(catalog_xmin)、消费者确认线(confirmed_flush_lsn)。这三条线分别回答了三个问题:WAL能不能删,系统目录旧版本能不能清,消费者处理到哪了。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 配置复制槽之前的参数准备
2.1 三个核心参数,一个都不能少
复制槽不是装个插件就能用的,它依赖几个GUC参数。我在测试环境里改配置的时候,经常在postgresql.conf里看到这些参数被注释掉或者设成默认值,导致复制槽功能起不来。下面这三个是必须明确的:
| 参数名 | 默认值 | 说明 |
|---|---|---|
wal_level |
replica | 必须设为replica才能用物理复制槽,设为logical才能用逻辑复制槽。minimal级别不支持复制槽 |
max_replication_slots |
10 | 允许创建的最大复制槽数量,同时包含物理和逻辑复制槽 |
max_wal_senders |
10 | 允许并发运行的WAL发送进程数量,流复制、逻辑复制、pg_basebackup都会占用 |
wal_level是静态参数,修改后必须重启实例才能生效。max_replication_slots和max_wal_senders也是静态参数,同样需要重启。我见过有人用ALTER SYSTEM SET在线修改max_replication_slots,结果发现没生效,再看文档才发现这哥俩属于postmaster级别的参数。
这三个值的规划,我给一个参考思路:max_replication_slots应该覆盖“物理备库数量 + 逻辑订阅数量 + 临时用途余量”。比如你有两个物理备库、三个逻辑订阅,设成10是比较合理的,余量给够。max_wal_senders通常要比max_replication_slots略大一些,因为pg_basebackup做全量备份时也会占用一个WAL发送进程,如果正在做备份、备库又在追WAL、逻辑复制又在解码,槽位就不够用了,新连接会直接报FATAL: number of requested standby connections exceeds max_wal_senders。
2.2 16版本里容易忽略的max_slot_wal_keep_size
这个参数我单独拎出来说,因为它在16.x版本里真的太重要了。max_slot_wal_keep_size的默认值是-1,意思是“复制槽要保留的WAL不做大小限制”。啥概念呢?你的备库或者逻辑订阅端挂了三天,主库就帮你留着三天的WAL,直到磁盘写满。
生产环境里我见过最离谱的一次,主库磁盘200GB,一个逻辑复制槽对应的订阅端因为业务变更停了五天,pg_wal目录直接涨到187GB,数据库差点宕掉。后来我在所有主库上都把这个参数设成了合理值,比如20GB或50GB,这样即使有人误操作把消费者停了,WAL堆积到阈值后,主库会把这个复制槽标记为lost状态(pg_replication_slots视图的wal_status字段会变成lost),后续即使消费者恢复,也无法继续解码,必须重建复制槽。这听起来有点激进,但总比磁盘写满导致整个实例宕机强。
另外,hot_standby_feedback这个参数虽然不直接属于复制槽配置,但在物理备库上建议打开。它解决的是主备之间因VACUUM清理导致查询冲突的问题,和复制槽配合能显著提高备库查询的稳定性。不过要注意,它也会延长主库VACUUM清理的周期,和逻辑复制槽的catalog_xmin是同一个道理,都是靠“最小活跃快照”来约束VACUUM。
3. 物理复制槽配置实操:主备流复制场景
3.1 主库上创建物理复制槽
物理复制槽的创建非常简单,一条SQL搞定:
sql复制SELECT * FROM pg_create_physical_replication_slot('standby_slot');
执行后的返回类似于:
code复制slot_name | lsn
-------------+-----------
standby_slot | 0/3002F60
返回的lsn是当前WAL写入位置,这个新槽就从当前位置开始记录。从这一刻起,主库会为这个槽保留从0/3002F60开始的所有WAL,直到这个槽被删除或者WAL大小超过max_slot_wal_keep_size的限制。
这里有一个经验点:如果你准备用pg_basebackup搭建备库,完全不需要手动先创建槽,直接在建备库的时候让pg_basebackup自动创建就行。命令是这样的:
bash复制pg_basebackup -h 192.168.1.10 -D /data/pgsql16 -U repuser -R -X stream --slot=standby_slot --create-slot
--create-slot让pg_basebackup在源库上创建复制槽,--slot指定槽名,-R自动生成备库所需的standby.signal和postgresql.auto.conf文件。这样搭出来的备库,复制槽自动绑定,省去手工建槽、再手动配置的繁琐过程。
3.2 备库端配置primary_slot_name
备库要使用复制槽,必须在postgresql.conf里指定primary_slot_name。如果你用了上面的pg_basebackup -R参数,这个值会自动写进postgresql.auto.conf。手动配的话,在备库的配置文件里加一行:
code复制primary_slot_name = 'standby_slot'
配好之后重启备库,或者直接SELECT pg_reload_conf()重载配置。为了确认复制槽真的生效了,在主库执行:
sql复制SELECT slot_name, slot_type, active, restart_lsn
FROM pg_replication_slots WHERE slot_name = 'standby_slot';
重点看active字段,如果是true,说明备库已经连上来并且正在使用这个槽。如果一直是false,大概率是备库没起来,或者primary_slot_name写错了。
3.3 验证主备同步延迟
复制槽只能保证WAL不被清理,但备库能不能跟上,还需要单独看延迟。我常用的监控SQL是用pg_stat_replication视图:
sql复制SELECT pid, application_name, state,
pg_wal_lsn_diff(pg_current_wal_lsn(), write_lsn) AS write_lag,
pg_wal_lsn_diff(pg_current_wal_lsn(), flush_lsn) AS flush_lag,
pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn) AS replay_lag
FROM pg_stat_replication;
write_lag、flush_lag、replay_lag分别表示主库写入、备库接收、备库回放三个层面的延迟。生产环境里,只要replay_lag持续稳定地增长,说明备库回放能力跟不上,这时候加复制槽也解决不了问题,要从磁盘IO、CPU、大事务角度去排查。
有一个细节值得注意:备库回放完的WAL位置和当前主库WAL位置之间的差距,其实不体现在复制槽的restart_lsn上。restart_lsn是主库视角下“备库不需要更早的WAL了”。我在带新人时经常强调,复制槽和延迟是两回事,搞混了会误判集群健康状况。
4. 逻辑复制槽配置实操:从整库到单表
4.1 发布端参数与表准备
逻辑复制槽的配置要比物理复制槽多一步,它依赖逻辑解码输出插件和发布端对象。首先确认wal_level是logical,这个在前面说过。然后选择输出插件,16.3版本内置了pgoutput和test_decoding两种,生产环境做逻辑复制首选pgoutput,它是发布订阅功能的标准输出插件;test_decoding主要用于测试和调试。
接下来在源表上设置副本标识(Replica Identity)。默认情况下,PG使用主键作为复制标识,这在大多数场景下够用了。但如果表没有主键,DELETE和UPDATE操作在逻辑复制时就会报错:
code复制ERROR: cannot delete from table "users" because it does not have a replica identity and publishes deletes
解决办法是给表设置副本标识为FULL:
sql复制ALTER TABLE users REPLICA IDENTITY FULL;
FULL模式下,逻辑解码会把整行旧值作为WHERE条件传给下游,这样即使没有主键也能定位更新目标,代价是WAL体积增大,因为每个UPDATE都会记录完整的旧值和新值。在16.3里,我一般建议能加主键还是加主键,REPLICA IDENTITY FULL只是兜底方案。
然后是创建发布:
sql复制CREATE PUBLICATION pub_users FOR TABLE users;
或者发布所有表:
sql复制CREATE PUBLICATION pub_all FOR ALL TABLES;
4.2 创建逻辑复制槽
手动创建逻辑复制槽的SQL长这样:
sql复制SELECT * FROM pg_create_logical_replication_slot('slot_logical_01', 'pgoutput');
返回结果:
code复制slot_name | lsn
------------------+-----------
slot_logical_01 | 0/4A2D1B8
这个lsn是逻辑解码的起始位置。创建一个逻辑复制槽之后,如果没有消费者来消费,WAL会从起始位置开始无限累积(受max_slot_wal_keep_size约束),这点和物理复制槽一模一样。
用pg_recvlogical客户端工具可以直接测试逻辑解码:
bash复制pg_recvlogical -h 192.168.1.10 -p 5432 -U repuser -d testdb --slot=slot_logical_01 --start -f -
运行后,对users表执行一条INSERT,终端里会立即输出解码结果。看到输出,说明逻辑复制链路是通的。测试完毕记得Ctrl+C退出,然后用下面的SQL删除测试槽:
sql复制SELECT pg_drop_replication_slot('slot_logical_01');
这里踩过一个坑:pg_drop_replication_slot在PG 16.x版本中,如果这个槽有活跃的消费进程,直接执行会报错。必须先断开消费者进程,再删槽。后面第6章会详细说。
4.3 订阅端如何绑定复制槽
实际生产环境里,通常不会手动创建逻辑复制槽,而是通过CREATE SUBSCRIPTION自动完成。PG的发布订阅模型里,订阅端在建立订阅时可以指定槽名,也可以让系统自动生成。16.3版本中,自动生成的槽名默认是sub_<订阅名>。
sql复制CREATE SUBSCRIPTION sub_users
CONNECTION 'host=192.168.1.10 port=5432 user=repuser dbname=testdb'
PUBLICATION pub_users
WITH (slot_name = 'sub_users_slot');
执行这条语句后,主库会自动创建一个名为sub_users_slot的逻辑复制槽。这个槽的生命周期和订阅绑定,删除订阅时,如果指定了DROP SUBSCRIPTION ... WITH (drop_slot = true),槽会被自动清理;如果没指定,槽会残留,需要手动删除。我遇到过不少案例,订阅都删了半年了,主库的复制槽还占着位置、留着WAL,导致磁盘持续报警,就是这个原因。
16版本在逻辑复制并行应用上做了不少优化,订阅端可以通过max_parallel_apply_workers_per_subscription参数控制并行apply的并发度。默认是2,如果你的下游是大事务较多、订阅表没有依赖关系的场景,可以适当调大。但这个参数不影响主库复制槽的功能,只是改善消费速度,配置时要根据订阅端CPU核数来定。
5. 复制槽的监控与日常维护
5.1 巡检必备的几张视图和SQL
复制槽配置完之后,真正的考验是日常维护。我每次巡检,第一件事就是看复制槽的状态。核心视图就是pg_replication_slots。在16.3中,这个视图的关键字段包括:
| 字段名 | 含义 |
|---|---|
| slot_name | 复制槽名称 |
| slot_type | physical或logical |
| database | 逻辑复制槽对应的数据库,物理复制槽为NULL |
| active | 是否有消费者在连接 |
| restart_lsn | WAL保留起点,消费者已确认的最早位置 |
| confirmed_flush_lsn | 逻辑复制槽消费者的确认刷新位置 |
| wal_status | reserved / extended / unprotected / lost |
| safe_wal_size | 按当前产生速率,预计还有多久达到max_slot_wal_keep_size |
我最常用的巡检SQL是计算每个复制槽的WAL延迟量:
sql复制SELECT slot_name,
slot_type,
active,
pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn) AS lag_bytes,
pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS lag_pretty,
wal_status,
safe_wal_size
FROM pg_replication_slots
ORDER BY lag_bytes DESC;
这个SQL能直接看出哪个复制槽积压了大量WAL。正常情况下,物理复制槽的延迟应该在几十MB以内,逻辑复制槽因为要等待消费者确认,会有一定波动,但如果持续上涨超过几GB,就要警惕了。
另外我建议在巡检脚本里加上对pg_wal目录大小的监控。复制槽失控和pg_wal目录膨胀基本是强相关关系,如果pg_wal目录大小异常,第一排查对象就是复制槽。
5.2 复制槽的清理和生命周期管理
什么时候该删复制槽?很简单:对应的备库不再需要、对应的逻辑订阅已经删除。删除物理复制槽:
sql复制SELECT pg_drop_replication_slot('standby_slot');
删除逻辑复制槽前,必须先停掉对应的订阅:
sql复制ALTER SUBSCRIPTION sub_users DISABLE;
SELECT pg_drop_replication_slot('sub_users_slot');
16.3版本里,pg_drop_replication_slot对逻辑复制槽有一个保护机制:如果槽还有活跃的消费进程,会报错:
code复制ERROR: replication slot "sub_users_slot" is still active
这种时候先查pg_replication_slots的active_pid字段,找到占用进程,用pg_terminate_backend(active_pid)终止,或者直接停掉订阅端相关进程,再删槽。
还有一个生命周期管理的问题:临时复制槽。PG 16支持创建临时逻辑复制槽,会话结束槽就自动删除,适合一次性数据迁移场景:
sql复制SELECT * FROM pg_create_logical_replication_slot('tmp_slot', 'pgoutput', true);
第三个参数temporary设为true即可。这类槽不会在实例重启后恢复,避免了忘记清理造成的WAL堆积问题,如果你只是临时做数据比对,建议用它。
6. 常见问题与排查经验实录
6.1 WAL目录爆满,复制槽是头号嫌疑
现象:pg_wal目录占用达到几十GB甚至上百GB,磁盘空间告急。排查步骤:
第一步,执行上面的延迟监控SQL,看有没有某个槽的lag_bytes特别大,同时active为false。这表示消费者断了,但槽还留着,WAL只能堆积。
第二步,查一下WAL保留总量,用这个SQL:
sql复制SELECT slot_name,
pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS slot_lag
FROM pg_replication_slots;
第三步,确认是哪个槽问题后,二选一:如果消费者还能恢复,尽快恢复让它追上进度;如果消费者已经废弃,直接删槽释放WAL。删完槽之后,pg_wal目录不会立刻减小,要等检查点之后循环复用,别着急。
这里要着重说一下wal_status字段。当复制槽的WAL因为超龄或超过max_slot_wal_keep_size限制,还没被消费者消费就被回收时,状态会变成lost。这时候复制槽已经是废的,消费者就算重连也无法继续,必须重建复制槽。我处理过一起故障,就是逻辑订阅端数据库被误删重建,主库端没有及时处理,wal_status变成lost后,整个逻辑复制链路彻底瘫痪,最后只能重新初始化订阅。
6.2 备库重连报错,复制槽也救不了
有读者可能会问:有了复制槽,备库断连再重连就没问题了吗?答案是:不一定。复制槽能保护“从当前槽位开始的WAL不被清理”,但有一种情况例外——备库断开时间太长,主库的max_slot_wal_keep_size设得太小,或者WAL产生速度远超预期,导致复制槽的WAL被物理清理掉。这时候备库重连会报:
code复制FATAL: requested WAL segment 0000000100000000000000A1 has already been removed
这种故障的处理方案没有捷径,要么从备份恢复备库,要么重新pg_basebackup。备份的重要性就在这里体现出来的,不要等出了事才想起来没做全量备份。
6.3 复制用户权限不够
复制槽的创建、删除、逻辑解码都需要对应权限。如果用普通业务账号执行pg_create_physical_replication_slot,会报:
code复制ERROR: permission denied to create replication slot
解决办法是专门建一个复制用户,授予REPLICATION权限:
sql复制CREATE ROLE repuser WITH REPLICATION LOGIN PASSWORD 'strong_password';
REPLICATION权限允许该用户建立流复制连接和逻辑复制连接,也允许创建复制槽。但要注意,REPLICATION权限不等于SUPERUSER,它无法读取普通表的逻辑解码数据。逻辑复制场景下,如果从发布端解码,需要额外授予访问表的权限,16版本中可以通过GRANT SELECT ON ALL TABLES IN SCHEMA public TO repuser解决。
另外还有一类比较典型的权限报错,和复制槽无关但经常被误判。在Debian系Linux上启动PG时:
code复制FATAL: could not create lock file "/var/run/postgresql/.s.pgsql.5432.lock": Permission denied
这种是/var/run/postgresql目录属主不对,一般是用了root启动或者目录权限被改过。正确做法是用postgres系统用户启动PG,或者手动把目录属主改回postgres。我遇到过新手DBA在排查这个问题时怀疑是复制槽配置错误,绕了很大一圈,其实和复制槽毫无关系。
6.4 Patroni等高可用组件里的复制槽策略
生产环境用Patroni管理PG高可用的场景很常见。Patroni默认启用了复制槽功能,但它对复制槽的管理方式和手工配置不太一样。Patroni会在主库自动为每个备库创建复制槽,槽名通常是<集群名>_<节点名>,并且通过etcd里的状态信息来同步槽位记录。这样做的目的是,当主库发生故障切换时,新主库能知道所有备库的消费位置,避免切换后WAL被过早清理。
用Patroni时有两个注意点。第一,不要手动删除Patroni创建的复制槽,否则Patroni和数据库之间的状态会不一致,导致槽管理混乱。第二,Patroni的slots配置项可以设置永久槽位,比如给逻辑复制用的槽单独预留,这样即使某个备库节点被移除,逻辑复制槽也不会被Patroni自动清理。配置类似这样:
yaml复制bootstrap:
dcs:
slots:
logical_slot_for_sync:
type: logical
database: testdb
plugin: pgoutput
这种配置的存在,就是为了解决“自动清理逻辑复制槽”的误伤问题。我见过有团队把所有复制槽都交给Patroni管理,结果逻辑订阅端业务调整,角色切换时槽被清掉,导致同步链路中断,只能重建。所以如果你是Patroni用户,一定要把逻辑复制槽的权限从Patroni手里摘出来,用上面的slots配置显式声明。
6.5 16.3版本在当前生产环境的定位反思
最后说说16.3这个版本本身。16系列是PG在逻辑复制性能上重点发力的版本,16.3又是这个系列里的稳定修订版。相比15,16版本在复制槽相关的改进主要体现在逻辑复制并行应用、大事务处理效率以及一些复制状态管理细节上。但配置层的核心逻辑没有颠覆性变化,15的经验可以平滑迁移到16。如果你的生产环境还在用12或13,升级到16之后,需要特别留意复制槽默认行为的变化——比如max_slot_wal_keep_size的默认值、复制槽视图的字段差异,这些在官方Release Notes里都有说明,升级前最好全量读一遍。
根据我个人实际操作的体会,复制槽配置最忌讳的就是“配完不管”。它不像索引,建完就能自动优化查询,复制槽是一个持续需要照顾的运行组件。建议每套PG环境都做两件事:一是把复制槽监控SQL写进巡检脚本,每天至少跑一次;二是给每个复制槽标注清楚业务用途和责任人,避免出现“这个槽不知道谁建的、能不能删”的尴尬情况。把这两件事做到位,复制槽相关的故障至少能规避八成。
