ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

PostgreSQL复制槽引发WAL日志膨胀:从磁盘告警到彻底清理的实战复盘

2026/9/28 6:49:15 拓冰建站 浏览量
PostgreSQL复制槽引发WAL日志膨胀:从磁盘告警到彻底清理的实战复盘 事情发生在周三下午两点半我正在例会里汇报本周的数据同步进度同事突然在工作群甩了一张监控截图生产库磁盘使用率已经冲到87%。当时第一反应是慢查询或者临时表膨胀没想到最后一路排查下去问题竟然出在一个已经“死掉”的PostgreSQL复制槽上——这个槽的客户端早就断开了但主库还在为它保留WAL日志而且保留得越来越多眼看着就要把整块数据盘写满。这类事故在PostgreSQL运维里其实不算罕见但很多人没经历过等真遇到了往往手忙脚乱。我这次从告警到恢复前后折腾了大半天中间还踩了一个“清理顺序”的坑差点把活数据弄出问题。这篇复盘把完整的过程、原理和排查命令都整理出来希望能帮大家少走点弯路。不管你是刚接手PG的运维还是已经在生产环境跑了一段时间这篇文章都值得从头读到尾。1. 事故现场磁盘告警与WAL堆积1.1 监控告警与初步排查先交代一下环境背景。这是一个跑在云主机上的PostgreSQL 13集群单机主库没有配置流复制备库逻辑复制也只是临时用过一个迁移任务。数据盘是300G的云盘日常占用大概60%左右WAL目录长期维持在2到3G。这个基线值很重要因为后续所有判断都依赖“正常状态下的样子”。收到告警后我第一步是登录主机跑df -h看磁盘挂载情况确认是数据目录所在分区告警。接着看了下监控面板发现过去两个小时里占用率从62%一路涨到87%增长曲线几乎是一条直线。这个增长速度很吓人正常业务写入不可能产生这么规律且持续的增长。第二步通过du -sh /data/pgdata/pg_wal看了WAL目录的大小不看不知道pg_wal已经膨胀到了47G。WAL就是事务日志PostgreSQL每次写入数据前都会先记录WAL然后由检查点机制把脏页刷到数据文件。正常情况下WAL是循环使用的写完旧的会被复用或清理。如果WAL目录持续变大说明有东西在阻止清理。1.2 WAL日志暴涨背后的异常信号很多刚开始做PG运维的朋友有个误区WAL目录大就是业务写入量大。其实不完全对。WAL增长确实跟写入量有关但如果只涨不清理那问题大概率出在“保留机制”上。PG里有三种东西会强制保留WAL一是未完成的归档二是wal_keep_size参数三是复制槽。我们这套环境归档是关闭的wal_keep_size设的默认值0PG 13用这个参数替代了旧的wal_keep_segments所以嫌疑范围一下就缩小到了复制槽。我立刻在主库上执行了下面这条SQL结果一眼就看到了问题SELECT slot_name, slot_type, database, active, active_pid, restart_lsn, confirmed_flush_lsn, wal_status FROM pg_replication_slots;返回结果里有一个叫logical_sync_20240415的逻辑复制槽active是falseactive_pid是空restart_lsn已经推进到了当天下午的最新位置wal_status显示reserved。这个槽的用途我认得是四月份做一次数据迁移时建的迁移结束后整个流程就停了。大家都以为迁移完成就没事了谁也没想到重建的复制槽还留在主库上而且一直在“吃”WAL。2. 揪出真凶认识PostgreSQL复制槽2.1 复制槽的工作原理复制槽Replication Slot是PostgreSQL在9.4版本引入的机制核心作用是为下游消费方备库或逻辑解码客户端提供一个WAL保留的承诺。简单说主库会记录下游消费者已经确认消费到的WAL位置只要这个位置存在主库在WAL清理时就会跳过被占用的部分。它跟“背锅侠”很像系统想把旧日志删掉复制槽拦下来说“等等我下游还没读完呢”。这个机制在正常运行时非常有用能保证流复制环境下备库即使临时宕机也不会因为主库WAL被覆盖而断档。复制槽有两种类型物理复制槽physical用于流复制备库逻辑复制槽logical用于逻辑解码和逻辑复制。它们占用的保留方式略有区别但“限制WAL清理”这个本质是一样的。我把复制槽理解为快递柜里的“预留包裹”。客户说好下午来取快递员就先把包裹放在柜子里即使柜子满了也不能拿出来塞别的。问题在于客户可能永远不来取了但快递员不知道继续把柜子占着。复制槽就是那个“柜子”下游客户端就是那个“客户”。2.2 复制槽为什么会“僵尸化”复制槽变“僵尸”的原因总结下来有三种场景我都见过。第一种是逻辑复制任务结束后没有清理。很多数据迁移工具比如用pgoutput或test_decoding插件做逻辑解码的脚本会在连接时自动创建复制槽但断开后不一定自动删除。如果代码里没有显式调用pg_drop_replication_slot这个槽就成了“无主之物”。第二种是物理备库被删除但槽还在主库上。有些高可用方案在切换或重建备库时主库上的物理复制槽没被同步删除。尤其在使用pg_basebackup重建备库的场景里旧备库对应的槽还留在主库备库IP都已经不在了槽却一直挂着。第三种是客户端异常断开且连接参数里没有清理逻辑。如果客户端用的是流复制协议但被kill掉比如wal_keep_size不够用了或者客户端进程崩溃槽会在pg_replication_slots里显示为activefalse但重启或重连后如果程序没有重新绑定这个槽它就一直处于“没人在消费”的状态。关键点来了复制槽只要存在不管有没有客户端在用主库都会一直保留从restart_lsn开始的WAL。restart_lsn是主库认为下游还需要的最旧位置如果下游一直没有消费它就一直往前走导致WAL只增不减。我们的案例里logical_sync_20240415这个槽的restart_lsn已经跟当前写入位置非常接近了这就意味着它不再是从头开始保留而是从近期某个点持续保留——问题在于从头到尾没有任何客户端在消费它保留的WAL就是纯浪费。3. 定位与清理一次完整的实战复盘3.1 三步定位僵尸复制槽定位过程其实不难难的是敢不敢下手清理。我当时按三步走每一步都有明确的目的。第一步是确认所有复制槽的状态。不只是看active字段还要看restart_lsn和wal_status。wal_status有几种取值reserved表示WAL仍在保留lost表示复制槽需要的WAL已经被覆盖了这种情况更危险下游会断流unreserved表示保留状态被解除。我们那个槽是reserved说明还在实打实地占着WAL。第二步是确认这个槽是否还有业务在依赖。我先在代码仓库里搜了“logical_sync”这个关键字确认没有脚本在引用又查了应用日志确认这个槽对应的逻辑解码消费者进程已经消失很久。这一步非常重要因为盲删一个还有业务使用的复制槽会导致下游逻辑复制直接断流而且重新搭建成本极高。第三步是量化一下这个槽带来的WAL占用。用下面这条SQL可以按槽分别统计已保留但未被消费的WAL体积SELECT slot_name, pg_size_pretty(pg_current_wal_lsn() - restart_lsn) AS retained_wal_size FROM pg_replication_slots;当时这个差值算出来是43G左右跟pg_wal目录的膨胀体积基本吻合。也就是说这47G的WAL里43G都是被这个僵尸槽“锁”住的。注意pg_current_wal_lsn() - restart_lsn的结果单位是字节如果restart_lsn落后太多计算出来的数字会非常大建议用pg_size_pretty格式化后再看。3.2 清理复制槽的正确姿势确认完毕后清理本身只需要一条SQLSELECT pg_drop_replication_slot(logical_sync_20240415);但这里有个大坑我必须详细说一说。PG 13及以下版本逻辑复制槽不能在事务块里删除而且必须满足“槽当前没有消费者进程在持有”的条件。如果返回“replication slot is active”的报错说明还有进程在使用这个槽要先用pg_terminate_backend(active_pid)杀掉对应进程再执行删除。我当时遇到的情况更隐蔽第一次执行删除时报错“replication slot is active”但我能看到active_pid是空的。后来查了PG源码相关讨论才知道逻辑复制槽的active状态跟active_pid不一定完全同步某些情况下插件持有的资源会导致槽被判定为active。解决方法是临时停掉所有使用该数据库的逻辑解码会话或者直接重启一次实例生产环境谨慎。我在测试环境先验证了删除流程然后在生产环境执行删除返回(1 row)。删除完成后我立刻看了WAL目录大小注意这时候WAL并不会马上降下来——因为删除槽只是解除了保留约束已经生成的WAL文件还在目录里需要等后续的检查点触发清理或者等pg_wal里的文件被复用覆盖。3.3 WAL空间释放与验证很多教程到这里就结束了但实际运维里空间释放才是关键。如果生产环境磁盘已经告警等检查点自动跑可能要很久。我当时做了两步加速第一步手动触发检查点CHECKPOINT;这一步会把WAL里的脏页刷到数据文件生成新的检查点位置。检查点完成后pg_wal里那些已经不需要的WAL文件会被标记为可复用。第二步如果磁盘还是没释放需要查看是否有归档进程或备份工具在占用。我们当时没有归档检查点后WAL目录从47G降到了8G虽然没完全回到基线但已经脱离危险区。剩下的6到7个G是最近几个小时的活跃WAL属于正常保留范围。释放之后一定要验证我列了一个自己的验证清单检查项命令/方法预期结果复制槽已删除SELECT * FROM pg_replication_slots;返回空集WAL目录恢复du -sh $PGDATA/pg_wal接近日常基线磁盘占用下降df -h占用率回落业务无异常观察应用日志无写入失败、无连接报错这个过程走下来我心里最深的感触是删除复制槽很容易但要确认它“该删”很难。很多人在第一步就卡住了不是不会SQL而是怕删错。我的经验是不管多紧急至少留十分钟做“业务归属确认”这一步省不得。4. 防患于未然复制槽的监控与管理4.1 建立复制槽监控体系事故复盘完之后我第一件事就是补监控。之前我们只监控了磁盘使用率、连接数和慢查询完全没覆盖复制槽这个维度。补监控的核心是盯两个点一是是否存在长期activefalse的槽二是单个槽保留的WAL体积是否异常增长。先说第一个点建一个定时任务比如每分钟跑一次执行下面的SQL把结果写入监控平台SELECT slot_name, slot_type, database, active, restart_lsn, pg_current_wal_lsn() - restart_lsn AS wal_retained_bytes, now() - pg_stat_replication.backend_xmin AS backends_behind FROM pg_replication_slots s LEFT JOIN pg_stat_replication r ON s.slot_name r.slot_name;如果发现activefalse的槽持续存在超过一定时间比如10分钟就触发告警。注意逻辑复制槽在消费间隔内短暂处于activefalse是正常的所以告警阈值要留有余量我建议用“持续N分钟”这种条件而不是一看到就报警。再说第二个点结合磁盘容量设置WAL占用告警。当时我们加的规则是单个复制槽保留的WAL超过20G就警告超过50G就严重告警。这个阈值可以根据业务量调原则是每个槽占用的WAL体积不能超过可用磁盘容量的一个安全比例比如5%到10%。4.2 复制槽生命周期管理规范监控能发现问题但解决不了“槽是怎么多出来的”这个根源。我复盘后总结了三条管理规范每条都是用代价换来的。第一条是明确复制槽的创建审批流程。以前谁想建槽就建建完也没人记录。现在所有复制槽必须通过工单创建注明用途、负责人、预计使用周期和清理责任人。我在sql中发现最危险的复制槽从来不是“没人知道它在干嘛”的而是“所有人以为别人在管”的。第二条是自动化清理机制。对于临时逻辑复制任务在客户端代码里加上显式的清理逻辑。Python生态里用psycopg2的示例是这样import psycopg2 def consume_and_cleanup(conn, slot_name): cur conn.cursor() cur.execute(SELECT pg_replication_slot_advance(%s, pg_current_wal_lsn());, (slot_name,)) conn.commit() cur.execute(SELECT pg_drop_replication_slot(%s);, (slot_name,)) conn.commit()这段代码的思路是消费完数据后先把槽的确认点推进到最新WAL位置再删除槽。如果不先推进就直接删可能会因为WAL保留范围的突变引发复制槽删除失败或下游数据丢失。踩过一次坑的教训是先推进再删顺序不能反。第三条是定期检查复制槽清单。我把这个检查做成每两周一次的例行巡检项用一条SQL拉出所有槽和它们的历史信息。我会特别关注那些slot_name里带日期、带sync、带tmp之类的临时槽这些往往是“一次性任务”留下的僵尸。5. 复盘思考与避坑指南5.1 这次事故的深层教训复盘的时候往深挖了一步发现我们其实错过了一次“救命”的机会。看监控数据那个复制槽早在事故前两周就已经activefalse了但当时没有任何告警因为我们的监控体系里根本没有复制槽的指标。而且WAL目录从3G涨到47G不是一夜之间的事情它是一天一天累积起来的中间跨过好几个检查点。PostgreSQL的WAL清理机制是“有条件地删除”。即使触发检查点只要复制槽还挂着需要保留的WAL就不会被清理。有些DBA喜欢在磁盘紧张时手动删pg_wal目录里的文件这个操作极度危险——如果删掉了复制槽还需要保留的WAL下游备库或逻辑订阅会直接报“requested WAL segment has already been removed”然后被迫重建备库或重新初始化订阅。我在测试环境验证过这个场景删掉需要的WAL之后恢复的代价远比磁盘告警大得多。说句实话这次事故最大的教训不是“忘了删复制槽”而是整个团队对复制槽的认知停留在“知道它存在”的层面没人真正理解它会在什么情况下成为定时炸弹。生产环境上任何一个数据库原生机制只要管理不当都可能变成事故的导火索。5.2 更多容易踩的坑最后整理几个实操中容易遇到的问题做成一个速查表方便大家直接对照。现象可能原因处理方式pg_drop_replication_slot报active下游进程持有槽或槽状态与实际不符先查pg_stat_replication杀掉对应进程后重试仍不行则确认是否有插件持有资源删除槽后WAL体积没降检查点未触发手动执行CHECKPOINT等待维护进程清理wal_statuslost复制槽需要的WAL已被提前覆盖下游需重新初始化无法就地恢复备库断连后主库WAL暴涨物理复制槽activefalse但restart_lsn持续推进确认备库状态必要时删除无效槽逻辑解码任务结束后槽还在客户端未调用pg_drop_replication_slot审查客户端代码增加显式清理逻辑还有一个容易踩的坑是用ALTER SYSTEM修改了max_replication_slots但忘了重启。PG里max_replication_slots是重启生效的这个参数限制的是整实例可创建的复制槽总数。如果设过小后续创建槽会报错设过大会有一定的内存开销。我建议值不要超过实际需要的两倍频繁增删槽的环境除外。另外所有复制槽操作建议在事务外执行尤其是在逻辑复制场景下。有些版本里复制槽和事务的交互行为不一致最省心的做法就是连BEGIN都不写直接执行单条语句。我个人的操作习惯是清理完复制槽之后顺手在postgresql.conf里把log_replication_commands设为on再reload。这样以后所有复制槽相关命令都会写进日志出问题时有据可查。这个小改动不费什么资源但排查问题时能省不少时间。这个复盘写下来我最大的体会是数据库事故往往不是某个瞬间的失误而是长期忽视某个“安静机制”的必然结果。复制槽不会主动报警它只是默默地为永远不会到来的下游保留着WAL直到把磁盘撑爆。幸好这次只是磁盘告警没有彻底写满导致数据库hang住否则恢复起来就真不是半天能搞定的事了。如果你手头也有正在跑的PostgreSQL环境建议现在就登录上去执行一遍文章里的查询SQL看看有没有属于你的“僵尸复制槽”正安静地躺在那里——发现问题的最好时间永远是现在。