ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

PostgreSQL复制槽导致WAL日志堆积?安全删除与清理实战

PostgreSQL复制槽导致WAL日志堆积?安全删除与清理实战 上次在客户现场碰到这么个事儿一个核心生产库跑得好好的突然告警说磁盘使用率到了 97%。查了一圈发现不是数据量涨了也不是索引碎片而是pg_wal目录快把磁盘撑爆了。当时第一反应是“归档怎么又没清理”结果一查归档进程正常归档目录也稳得很。最后定位到根因是一个早就废弃掉的复制槽在后台默默积压了几个月的 WAL 日志。在“海量数据库”这个级别日志清理从来不是删几个文件那么简单。尤其在 PostgreSQL 里复制槽Replication Slot和日志清理是强绑定的处理不当轻则磁盘告警重则直接宕机、主从断裂。这篇就来聊聊复制槽删除与日志清理这件事它到底是怎么把磁盘吃掉的、安全删除前要确认什么、完整流程怎么操作以及我在实战里踩过的坑和总结的排查套路。1. 复制槽的来历与日志堆积原理1.1 复制槽为什么会“锁住”日志先理解一个基本机制。PostgreSQL 的 WALWrite-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 怎么判断这个槽还能不能删activefalse不代表可以立刻删。还需要确认这个槽是不是被备用服务器或外部的解码程序依赖着。我的判断套路分三步第一步和 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);实际用法是一样的逻辑复制槽和物理复制槽都走这个函数。不过在高版本 PostgreSQL15 及以上中对于逻辑复制槽建议先看看是否有活动的订阅关系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;正常情况下所有activetrue的槽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;所有activefalse且确认没有新备库在使用的槽一律删除。这里要强调一下主备切换后立刻检查复制槽是必要的步骤很多日志爆满事故就发生在切换后的第二天。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里的文件是不是有问题。顺序反了会浪费很多无谓的时间。另外一个小技巧日常巡检时可以把复制槽信息做成一个定时任务每天自动跑一遍把积压超过阈值的槽发到告警群里。我自从把这一步自动化之后再也没有被“半夜磁盘告警”的电话叫醒过。最后再提醒一句删除复制槽前多花两分钟查清楚它背后的消费者是谁比什么都重要。这个动作看着简单但做对了能帮你省掉一整个通宵的重建备库时间。
返回列表