上次在客户现场碰到这么个事儿:一个核心生产库,跑得好好的,突然告警说磁盘使用率到了 97%。查了一圈,发现不是数据量涨了,也不是索引碎片,而是pg_wal目录快把磁盘撑爆了。当时第一反应是“归档怎么又没清理”,结果一查,归档进程正常,归档目录也稳得很。最后定位到根因,是一个早就废弃掉的复制槽,在后台默默积压了几个月的 WAL 日志。
在“海量数据库”这个级别,日志清理从来不是删几个文件那么简单。尤其在 PostgreSQL 里,复制槽(Replication Slot)和日志清理是强绑定的,处理不当,轻则磁盘告警,重则直接宕机、主从断裂。这篇就来聊聊复制槽删除与日志清理这件事:它到底是怎么把磁盘吃掉的、安全删除前要确认什么、完整流程怎么操作,以及我在实战里踩过的坑和总结的排查套路。
1. 复制槽的来历与日志堆积原理
1.1 复制槽为什么会“锁住”日志
先理解一个基本机制。PostgreSQL 的 WAL(Write-Ahead Logging)日志,是事务提交前先写的一份“流水账”。主库生成 WAL,备库拉走 WAL 然后回放,从而保持数据一致。正常情况下,WAL 用完了之后,可以被回收复用,这就是日志清理的底层逻辑。
但复制槽不一样。它的设计初衷,是为了防止备库跟不上的时候,主库把备库还没拿到的 WAL 给清掉了。只要复制槽存在,PostgreSQL 就认为“有个消费者还没拿到这段日志”,于是对应时间点之后的 WAL 会一直保留在pg_wal目录里,哪怕磁盘已经告急。
注意这里的关键:保留的起点是复制槽记录的restart_lsn,不是当前写入位点。备库如果断连了很久,主库的 WAL 已经写了几百 GB 甚至几个 TB,但复制槽的restart_lsn还停在旧位置,那主库就得老老实实保留从那个位置到现在的所有 WAL。这就是日志堆积的直接原因。
1.2 海量数据库里这个问题的放大效应
在海量数据库里,这个问题的破坏力是指数级放大的。日志量小的时候,复制槽积压个几百 MB,根本没人注意。但当日志生成速率是每小时 50GB 的时候,一个闲置复制槽 48 小时就能积压 1TB 级以上,磁盘说爆就爆。
而且,海量数据库通常有不止一个备库。常见的情况是:一主两从,或一主三从,每个备库都建了对应的复制槽。如果某个备库因为网络问题长期断连,或者业务侧随手建了一个槽再也没用过,它就变成了一个“定时炸弹”。更惨的是有限制条件——
SELECT * FROM pg_create_physical_replication_slot('slot_demo');如果建槽时用了pg_create_physical_replication_slot而不是pg_create_logical_replication_slot,物理复制槽的积压同样存在。逻辑复制槽更危险,因为它的消费者可能是外部系统,断开后没有任何数据库内的可见线索,排查难度更大。
1.3 复制槽和日志清理的关系梳理
日志清理的完整链路应该是这样理解的:
- 主库生成 WAL,落到
pg_wal目录。 - 如果开启了归档,WAL 会被归档进程复制一份到归档目录,可由
archive_command或备份工具管理。 - 如果没有复制槽,WAL 只要已经不再需要(已归档、已被所有备库拉取),就能被回收。
- 如果有复制槽,WAL 必须先满足“复制槽的
restart_lsn已经推进”这一条件,否则即使归档完成了,日志也永远删不掉。
所以,我们看到的现象往往很奇怪:归档目录是干净的,但pg_wal爆满。原因就是这个复制槽卡住了清理进程。理解这一点,后续的删除操作就有了明确的决策依据——要么让复制槽的位点推进(比如恢复备库),要么直接把失去意义的复制槽删掉,释放日志。
2. 动手前的排查:确认哪些复制槽能删
2.1 快速定位空闲复制槽
删除复制槽之前,一定要先回答一个问题:这个槽还在用吗?误删一个还在使用的复制槽,备库会直接报错断复制,几万条 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 retained_bytes FROM pg_replication_slots;active字段是最直接的指标:true表示当前有客户端在连接,false表示槽还存在但没有任何消费者。如果active是false,就已经很危险了,需要立刻关注。
再看retained_bytes这个值,单位是字节。在海量数据库场景下,这个值经常是几百 GB 甚至几个 TB。用这个值可以快速估算:如果它占用了pg_wal目录总量的大部分,那基本就坐实了这个槽是磁盘爆满的元凶。
2.2 怎么判断这个槽还能不能删
active=false不代表可以立刻删。还需要确认这个槽是不是被备用服务器或外部的解码程序依赖着。我的判断套路分三步:
第一步:和 DBA 或负责复制的人员确认,有没有对应这个槽名的备库或数据管道任务。如果查出来是一个早就停掉的旧链路,那基本可以确定可以删除。
第二步:检查槽的创建时间。pg_replication_slots没有直接的创建时间字段,但可以通过restart_lsn大致推断。如果restart_lsn比当前插入位点落后了非常多,说明这个槽已经很久没动了,大概率是废弃的。
第三步:如果是逻辑复制槽,还要检查发布端和订阅端的对应关系。pg_replication_slots里的database字段可以告诉我们这个槽属于哪个库,再去对应库里查发布订阅的配置。
这里有个实战经验可以分享:不要只看active字段就下结论。我见过一种情况,后台有程序用pg_recvlogical连着逻辑复制槽,但由于连接池配置问题,偶尔连上又断开,active一会儿 true 一会儿 false。这种情况只凭一次查询是判断不准的,要多看几次。
2.3 用“观察窗口”代替“拍脑袋决策”
在大规模数据库上,我推荐用一个短时间的观察窗口来确认,而不是凭一次查询就删除。
操作方法是:隔 5 到 10 分钟,再查一次同一个槽的restart_lsn。如果两次查询中restart_lsn没有任何推进,同时active一直是false,那删除的风险就低很多。
具体可以这样记录:
SELECT slot_name, restart_lsn, active, pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn) AS retained FROM pg_replication_slots;比如第一次查询时restart_lsn是0/3002A48,五分钟后再查,还是0/3002A48,而且主库的 WAL 写入位点已经往前走了几百 MB,这就说明这个槽的消费者早就断气了。此时删除,属于“止损”,不是“误杀”。
3. 安全删除复制槽的完整操作流程
3.1 删除前必做的三个准备动作
首先,要确认磁盘空间现状。删除复制槽后,日志不会立即全部消失,PostgreSQL 的 WAL 回收机制是异步清理的。所以删除前先记录一下pg_wal目录的大小,便于之后对比效果:
du -sh /var/lib/postgresql/data/pg_wal/其次,要做好删除失败或误删的兜底。如果业务依赖关系没查清楚,删错了槽,备库重搭是一个极其耗时的过程,所以在执行删除前,务必把当前复制关系都记录下来:
SELECT * FROM pg_stat_replication;这条查询会列出当前正在复制的备库信息和对应的slot_name,保存查询结果就能知道哪些槽正在被使用。
最后,尽可能在业务低峰期执行。删除复制槽这个动作本身非常轻量,通常毫秒级完成,但后续引发的 WAL 清理可能导致大量文件删除操作,这会增加磁盘 IO。海量数据库的磁盘 IO 一旦被占满,主库的写入性能就会受影响,所以低峰期操作是基本素养。
3.2 执行删除的几种方式
PostgreSQL 提供了两个内置函数来删除复制槽:
-- 删除物理复制槽 SELECT pg_drop_replication_slot('slot_name'); -- 删除逻辑复制槽 SELECT pg_drop_replication_slot('slot_name');实际用法是一样的,逻辑复制槽和物理复制槽都走这个函数。
不过在高版本 PostgreSQL(15 及以上)中,对于逻辑复制槽,建议先看看是否有活动的订阅关系:
SELECT subname, subslotname FROM pg_subscription;如果逻辑复制槽还被订阅使用,贸然删除会导致订阅端报错。正确的顺序是:先移除订阅或者禁用订阅的复制关系,再删除槽。
这里还有一个容易踩的坑:某些版本中,如果当前有事务在持有复制槽的 RWL 锁,pg_drop_replication_slot会阻塞。表现为删除语句执行后一直不出结果,看起来像卡死了。这时候不要反复去 kill 会话,耐心等待正在执行的事务结束即可,或者检查是否有长事务在跑:
SELECT pid, state, query_start, query FROM pg_stat_activity WHERE state = 'active';3.3 一次标准删除操作的完整演示
以最常见的场景为例:确认了一个叫sbx_sub_standby_01的物理复制槽已经废弃,要删掉。
-- 1. 查看复制槽状态 SELECT slot_name, slot_type, active, restart_lsn, pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn) AS retained_bytes FROM pg_replication_slots WHERE slot_name = 'sbx_sub_standby_01';结果可以看到retained_bytes可能是721899921408,即约 672GB,这就是它积压的 WAL 总量。
-- 2. 删除复制槽 SELECT pg_drop_replication_slot('sbx_sub_standby_01');返回(1 row),删除成功。
-- 3. 删除后立即确认 SELECT slot_name, active FROM pg_replication_slots WHERE slot_name = 'sbx_sub_standby_01';返回零行,说明槽已经不存在。
3.4 为什么删除后磁盘空间没有立刻释放
有人删完槽以后马上看磁盘,发现空间并没有变化,就以为操作没生效。实际上这是正常的。PostgreSQL 在复制槽删除后,只是把 WAL 的保留条件解除。真正的文件删除由后台的checkpointer或后续的 WAL 切换触发。在处理海量数据库时,这个延迟可能会比较明显,积压了几百 GB 日志的话,清理过程可能需要几分钟到几十分钟。
如果想加速清理,可以通过强制切换 WAL 来触发周期性的检查点:
SELECT pg_switch_wal();注意:这条命令会立即切换到一个新的 WAL 文件,触发归档。在归档状态正常的前提下,可以加速旧 WAL 的回收。如果归档落后或归档不可用,强制切换反而可能造成归档堆积,需要先确认归档状态:
SELECT * FROM pg_stat_archiver;archived_count在持续增长且failed_count为 0,说明归档正常,此时再执行pg_switch_wal()比较安全。
4. 收尾与验证:确认日志清理和空间释放
4.1 判断日志清理机制是否恢复
删除复制槽之后,更关键的是确认整个日志清理机制已经恢复。如果还有别的复制槽依然堆积,问题并没有完全解决。
我的收尾检查清单一般是这样:
- 查询
pg_replication_slots,确认所有在用复制槽的restart_lsn都在持续推进。 - 确认
pg_wal目录大小开始下降。 - 确认归档目录没有异常增长。
- 观察
pg_stat_archiver的archived_count稳定递增。
最直观的命令是在数据库里持续观察:
SELECT slot_name, active, pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn) AS retained_bytes FROM pg_replication_slots;正常情况下,所有active=true的槽,retained_bytes应该在一个合理范围内波动,不会无限上涨。如果某个槽的retained_bytes还在持续线性增长,说明它的消费端可能出了问题,需要进一步排查。
4.2 空间释放的验证方法
在确认复制槽全部正常后,回到操作系统层面看空间是否确实释放。
如果数据库是运行在 LVM、云盘、或者裸设备上,du和df的结果会有差异。df -h显示的是文件系统层面的占用,如果删了 WAL 文件但 PostgreSQL 进程仍在持续写入新 WAL,空间释放可能看起来不明显。要精准地看 WAL 目录的变化趋势,还是用:
du -sh /var/lib/postgresql/data/pg_wal/间隔十分钟再看一次。如果目录大小在稳步下降,说明清理已生效。如果在删除复制槽半小时后,pg_wal大小纹丝不动,则需要检查是否有其他复制槽还在限制清理,或者归档队列是否卡住了。
这里还有一个容易忽略的点:如果开启了wal_keep_size参数,也会导致 WAL 被额外保留。有些环境为了保险,设置了wal_keep_size = 1024(单位是 MB),即使复制槽删完了,仍然最多保留 1GB 的 WAL。如果你的磁盘真的很紧张,可以临时调低这个参数,但要注意它会影响新加入的备库的启动过程,不建议长期低配。
4.3 从清理效果反推槽的健康水位
处理完一次事故后,我会顺手把“正常水位”记录下来,方便后续对比。比如:
- 正常情况下,一个健康备库对应的复制槽,
retained_bytes应该在 0 到几十 MB 之间波动。 - 如果
retained_bytes持续超过 1GB,说明备库回放落后,需要关注备库负载。 - 如果
retained_bytes达到几百 GB,基本可以判定复制链路已经彻底中断。
用这个水位线,后续可以通过监控系统设置告警阈值,不用等磁盘爆了才被叫醒。
5. 常见问题与避坑实录
5.1 “删不掉”的复制槽:会话持有与依赖关系
删除复制槽时最常见的报错是:
ERROR: replication slot "slot_name" is still active这个报错的意思是:当前有客户端正通过这个槽在复制,不能直接删。如果是物理复制槽,说明备库还连着主库。如果备库确实已经下线,但主库不知道(网络闪断、备库异常关机),需要去确认备库状态后,在备库侧停止 walreceiver 进程,或者在主库侧等待超时后强制清理。
强制清理的方式是:
-- 找到使用该槽的进程 SELECT pid, application_name, state FROM pg_stat_replication WHERE slot_name = 'slot_name'; -- 确认无误后,终止该进程 SELECT pg_terminate_backend(pid);执行完pg_terminate_backend后,再执行pg_drop_replication_slot,通常就能成功。但注意,这个操作会断开对应备库的复制连接,一定要事先确认该备库可以接受重连。
5.2 主备切换后的复制槽残留问题
还有一个非常隐蔽的坑:主备切换后,老主库上的复制槽不会自动清理。原主库上为原备库创建的复制槽,在角色转换后依然存在,但它们已经没有任何实际消费者,成为了永久性的日志保留锚点。如果不及时清理,这些“幽灵槽”会在新主库上持续积压 WAL。
处理方式也很直接,登录到已经降级为备库的实例上,检查:
SELECT slot_name, active, restart_lsn FROM pg_replication_slots;所有active=false且确认没有新备库在使用的槽,一律删除。这里要强调一下,主备切换后,立刻检查复制槽是必要的步骤,很多日志爆满事故就发生在切换后的第二天。
5.3 逻辑复制槽的订阅关系残留
逻辑复制槽的删除,要多检查一层订阅关系。如果pg_subscription里还挂着对应的subslotname,直接删槽会导致订阅端报错,甚至需要重建订阅。
最安全的步骤是先在订阅端移除订阅:
ALTER SUBSCRIPTION sub_name DISABLE; ALTER SUBSCRIPTION sub_name SET (slot_name = NONE); DROP SUBSCRIPTION sub_name;然后再回到发布端删除槽。这条流程在数据迁移和灾备链路切换时尤其重要。
5.4 清理后归档目录反而爆满
删除复制槽后,可能出现另一种现象:pg_wal释放了,但归档目录迅速膨胀。原因是积压的 WAL 在复制槽存在期间一直无法被清,现在复制槽删除了,PostgreSQL 会积极推进 WAL 清理,但归档进程可能需要逐一归档这些历史 WAL,形成突发 IO 和存储压力。
在海量数据库上,这种“先释放主库,再压垮归档”的情况并不少见。预防手段是:删除复制槽前,先确认归档目录有足够的冗余空间;删除后,密切监控pg_stat_archiver的archived_count增量。如果归档速度明显跟不上,可以考虑临时提高归档进程的并行度,或适当延长归档超时时间。
5.5 监控告警的最佳实践
经过这次事故,我的建议是把复制槽的核心指标接入监控,而不是只看磁盘使用率。建议至少监控以下几个指标:
- 每个复制槽的
restart_lsn与当前 WAL 插入位点的差值,换算成字节。 - 每个复制槽的
active状态。 pg_wal目录大小。- 归档目录大小。
- 主库 WAL 写入速率。
用这些指标配置告警,比如复制槽积压超过 10GB 就触发警告,超过 100GB 就触发严重告警。等磁盘到了 97% 再处理,成本完全不一样。
写在最后的实操心得
日志清理这件事,在 PostgreSQL 里从来都不只是清理日志本身,而是在管理复制链路的健康状态。我最深的体会是:排查日志爆满时,永远要先查复制槽,再查归档,最后才考虑pg_wal里的文件是不是有问题。顺序反了,会浪费很多无谓的时间。
另外一个小技巧,日常巡检时,可以把复制槽信息做成一个定时任务,每天自动跑一遍,把积压超过阈值的槽发到告警群里。我自从把这一步自动化之后,再也没有被“半夜磁盘告警”的电话叫醒过。
最后再提醒一句:删除复制槽前,多花两分钟查清楚它背后的消费者是谁,比什么都重要。这个动作看着简单,但做对了,能帮你省掉一整个通宵的重建备库时间。