资讯详情

PostgreSQL WAL堆积排查:僵尸复制槽如何撑爆磁盘及处理方案

📅 2026/10/3 9:28:06 | 华诺云谱 👁 阅读
PostgreSQL WAL堆积排查:僵尸复制槽如何撑爆磁盘及处理方案
那天凌晨我手机上的告警机器人连着弹了三条消息PostgreSQL服务器磁盘使用率88%、91%、94%。说实话看到消息的第一反应不是慌是有点懵。这套库跑了快两年当初规划磁盘空间时是按三年写入量做的预算理论上不该这么早出问题。登录服务器后我第一件事就是看WAL目录——没别的生产环境里PostgreSQL磁盘被撑爆十次有八次跟WAL文件堆积有关。du -sh /var/lib/pgsql/14/data/pg_wal一跑果然185GB整个人瞬间清醒了。但真正让我后背发凉的是接下来的发现这个WAL堆积并不是归档卡住而是一个躺在复制槽视图里、已经断连两个多月的“僵尸复制槽”。这篇文章就把当时的完整排查链路、处理动作和事后对WAL保留机制的复盘一次性讲清楚希望你在生产库里不要再踩这个坑。1. 半夜的告警磁盘使用率90%最先躺枪的是“归档”1.1 告警现场与本能反应磁盘告警这种事最忌讳的就是慌慌张张去删文件。我给自己定的规矩是先摸清三个问题——什么东西在涨、涨了多久、背后的进程是什么。当时的情况是这样的服务器上只有一套PostgreSQL 14单实例数据目录在/data/postgresqlWAL归档是开着了的归档目录放在独立挂载盘并没有满磁盘根分区df -h显示使用率94%剩余空间不到20GB。第一反应是查归档有没有卡住。因为PostgreSQL的WAL归档机制有一个特点如果archive_command长时间失败主库会不断重试归档同时WAL文件不会立刻被清理日积月累就会把磁盘吃满。我检查了pg_stat_archiver视图发现failed_count没有异常增长last_archived_time是几分钟之前——归档是正常的。归档正常那问题基本就锁定在“WAL文件本身没有被及时回收”这条线上。1.2 pg_wal目录的异常时间线接着我做了两件事查看WAL目录下的文件列表以及统计文件的时间分布。ls -lah /data/postgresql/pg_wal/ | head -50结果很有意思目录里的WAL文件最新的是当前正在写的段但更早的文件并没有按时间均匀分布——最早的一批文件停留在两个月前的一个时间点。正常情况下的pg_wal应该只保留最近几小时的WAL段除非有备库延迟或归档延迟两个月前的文件还堆在这里只有一种解释有某个机制在强制保留这些WAL。我当时在脑子里过了一遍PostgreSQL清理WAL文件的规则。WAL段在checkpoint时会被清理但必须同时满足几个条件早于当前重做点的WAL可以被清理已经被归档的WAL可以被清理不能早于wal_keep_size参数指定的保留量不能早于任何复制槽的restart_lsn。归档没问题、checkpoint也在正常跑、wal_keep_size配置的是默认值那嫌疑最大的就是第四项——复制槽。也就是从这个念头开始我正式转向了复制槽排查。2. 一张复制槽视图把悬案锁死在restart_lsn2.1 定位WAL堆积的第一条SQL在pg_wal目录和时间线异常的基础上我直接查了复制槽视图。这里提醒一下这条SQL建议每个PostgreSQL DBA都刻在脑子里SELECT slot_name, slot_type, active, restart_lsn, pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn) AS wal_lag_bytes, pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS wal_lag FROM pg_replication_slots;输出结果里躺着一条让我血压升高的记录slot_nameslot_typeactiverestart_lsnwal_lagsub_event_synclogicalf2/AC16D8C0157 GBactive f说明这个复制槽当前没有活跃的消费端restart_lsn停在两个月前的时间点它和当前LSN之间的差距是157GB。这些WAL正是pg_wal目录里那些“老古董”文件的来源。看到这里的读者可能会问物理备库的复制槽和逻辑复制的复制槽有什么不同这里先说结论不管是物理还是逻辑复制槽只要创建了主库就有义务为它保留从restart_lsn开始的全部WAL。区别在于物理复制槽消费的是整段WAL逻辑复制槽消费的是WAL里的逻辑变更但它们对WAL文件的“钉住”效果是一模一样的。2.2 复制槽的“钉子效应”是怎么发生的要理解这件事得先弄明白restart_lsn这个字段的意义。每一条WAL记录都有一个LSNLog Sequence Number相当于WAL日志里的字节偏移量。复制槽的restart_lsn表示“从这条LSN开始的WAL都不能清理因为我的下游还没消费完”。正常情况下备库或逻辑订阅端每消费一批WAL就会把确认位点推进主库也会同步更新slot的restart_lsn。但问题恰恰出在“断连”上。当备库下线、订阅端停摆、或者是用复制槽的备份工具停止运行后主库不会自动删除这个slot——它不知道下游是真没了还是只是暂时离线。于是restart_lsn停在原地主库每产生一个新的WAL段都只能“叠加”在保留列表里。日子一长没有边界地涨。我打个比方复制槽就像停车场里的一辆占了车位的车。哪怕车主几个月不来停车场管理员也不能把车拖走因为车位是租给他的。你只能看着这辆车占着位置旁边的新车不断停进来整个停车场慢慢被塞满。PostgreSQL的WAL清理逻辑就算想“拖车”也得先问过复制槽这个“长期租户”。当时看到157GB这个数字我心里大概已经明白发生了什么这套库上跑过一个逻辑订阅任务订阅端因为业务下线被回收了但主库上的slot没人清理WAL就这样默默积攒了两个多月。3. 拆除炸弹安全删除僵尸复制槽的全过程3.1 删槽之前的“三查”找到疑似元凶后不可以直接执行pg_drop_replication_slot。删除复制槽是破坏性操作删错了正在运行的备库或逻辑订阅会立刻中断而且是不可逆的。我的习惯是执行“三查”第一查确认slot没有对应活跃连接。SELECT pid, application_name, state, sync_state FROM pg_stat_replication;查出来是空结果说明当前没有任何备库通过这个槽在接收WAL。第二查确认它是否关联某个逻辑订阅。SELECT subname, subenabled, subslotname FROM pg_subscription;这里一般只有一两条订阅记录如果某个订阅的subslotname正好指向这个slot而它的subenabled是f说明订阅任务确实已经停掉了。第三查回顾变更记录确认创建slot的用途。翻了一下平台上的运维记录两个月前确实有人创建了一个逻辑复制订阅用来同步一张业务表到另一个库后来源表业务下线订阅端被回收但清理操作里漏掉了主库侧的复制槽。三条都确认后我心里就有底了这是一个标准的僵尸复制槽可以安全删除。3.2 执行删除与空间观察删除操作只有一行SQLSELECT pg_drop_replication_slot(sub_event_sync);执行成功后再查pg_replication_slots就只剩其他正常的slot了。接下来是观察磁盘和WAL目录的变化。这里有个容易被误解的点删除复制槽并不会“瞬间释放157GB空间”因为PostgreSQL的WAL清理时机是交给checkpoint和归档流程共同决定的。slot删除后WAL文件变成了“可回收”状态但要等下一次checkpoint完成、相关WAL段被归档后磁盘空间才会逐步释放。实际操作中我在删除后大概等了十几分钟再跑du -shWAL目录已经降到了日常的2GB左右。这里顺便补一句生产环境在删除slot后建议手动执行一次CHECKPOINT;加速回收。当然如果系统里有大量短事务在跑checkpoint本身会有一定开销低峰期操作更稳妥。3.3 删除后的次日观察还有个隐藏坑删除slot的第二天我又发现了一个有意思的细节日志里出现了几条关于逻辑复制worker重连失败的错误。虽然不是大事但它暴露了另一个隐患——如果你清理了slot但发布端/订阅端的配置还在下游重连时会发现“slot不存在”逻辑复制就会报错。我当时顺藤摸瓜把残留的订阅配置也一并清掉了DROP SUBSCRIPTION IF EXISTS expired_sub;这一步别忽略。很多事故复盘写到删除slot就结束但实际上slot只是“症状”配置残留才是“病根”。如果不清理订阅端定义下一次订阅端恢复运行PostgreSQL会尝试重新拉取又可能触发新的问题。4. 复盘WAL回收规则为什么它会放任空间被吃光4.1 checkpoint其实不想背锅很多刚接触PostgreSQL的同学会直觉性地怪checkpoint是不是checkpoint没跑才让WAL越积越多其实不是。checkpoint一直在正常执行但它对WAL的清理权限是有限的。PostgreSQL在清理WAL文件时会按以下顺序判断一个WAL段是否可以被删除该WAL段是否早于当前重做点REDO point该WAL段是否已被归档如果开启归档该WAL段是否落在wal_keep_size参数要求的保留范围内该WAL段是否早于所有复制槽的restart_lsn。注意最后一条只要有一个复制槽的restart_lsn比这个WAL段晚这个WAL段就必须保留。也就是说checkpoint就像一个尽职尽责的仓库管理员它早就想把旧货都清走但复制槽说“这批货我还要”管理员就只能把仓库大门留着。这个机制设计的本意是为了保证断连的备库重连后能拿到从断点开始的完整日志但对运维来说它同时也意味着“没有超时、没有上限、没有自动清理”。4.2 两个能兜底的参数为什么没兜住复盘到这个阶段我自然会想难道PostgreSQL就没有参数可以限制复制槽造成的WAL堆积吗有但从PostgreSQL 13开始才提供了真正的兜底参数max_slot_wal_keep_size。它的作用很简单当某个复制槽保留的WAL总量超过这个值主库会允许删除更早的WAL即使复制槽还没消费到那。代价是下游重连时可能发现需要的WAL已经被清掉必须重新做全量同步。默认值是-1表示不限制。另外还有一个老参数wal_keep_size13之前叫wal_keep_segments它的作用是“无条件保留最近这么多WAL”。注意它是“无条件”和复制槽无关所以设置很大反而会浪费磁盘。我当时确认了一下这套PostgreSQL 14实例的max_slot_wal_keep_size配置是默认的-1所以复制槽造成的WAL堆积没有任何上限这才让157GB的僵尸WAL悄无声息地躺了两个月。如果你的生产库版本在13以上且磁盘预算不宽裕建议认真考虑给max_slot_wal_keep_size设置一个明确的值。我的经验是把它设成“逻辑复制全量同步完成所需WAL量”的2到3倍左右既不会影响正常的备库消费又能给异常情况止血。4.3 三种常见slot来源物理备库、逻辑订阅、备份工具处理完事故后我把这套库的所有slot来源都梳理了一遍。PostgreSQL里复制槽这东西不是你手动创建才有很多日常工具都会悄悄帮你创建物理备库在用pg_basebackup做备库或者配置了流复制时如果指定了使用slot主库上会生成一个physical类型的复制槽。备库正常运行时它很乖但备库一旦下线且没人删除slot它就会变成僵尸。逻辑复制订阅创建发布订阅时订阅端会自动在主库上创建逻辑复制槽。订阅任务停止后slot不会自动消失就是我这次遇到的情况。备份工具部分备份工具例如pgBackRest支持在主库上创建专属的复制槽用来保证备份期间WAL不丢。备份作业停摆slot也可能留在原地。如果你不知道自己的库上到底有多少slot、是哪来的建议现在就去跑一下pg_replication_slots然后对照pg_stat_replication和pg_subscription把每一个slot的“用途”标注出来。凡是写不出用途的slot都值得警惕。5. 给生产库加一道“复制槽保险”5.1 30秒监控SQL把僵尸槽暴露在阳光下这次事故之后我把复制槽检查加入到了日常巡检里。复制槽的状态是动态的所以监控必须持续运行而不是拍个静态快照。推荐的监控SQL这样写把“非活跃”且“WAL滞后超过阈值”的slot列出来。SELECT slot_name, slot_type, active, restart_lsn, 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_human FROM pg_replication_slots WHERE active false AND restart_lsn IS NOT NULL AND pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn) 1073741824; -- 1GB阈值可以按库的实际写入量去调。如果一天产生50GB的WAL1GB阈值大概对应几十分钟的WAL量足够早发现如果库平时写入很小可以放宽到512MB。把这条SQL接到告警平台一旦返回记录就触发P2级别的告警基本能在磁盘被打满前留出充足的响应时间。5.2 运维流程上的三个习惯监控只能发现问题真正避免问题还是要靠流程。我复盘时发现这次事故里最核心的失误是“下线了订阅端却没有同步清理主库slot”。所以现在团队里固化了几条规矩删除任何下游之前先检查它遗留在主库上的slot无论是备库、订阅还是备份任务回收前必须执行pg_drop_replication_slot创建新的备库/订阅/备份任务时在变更记录里注明slot名称和用途没有记录的slot未来排查时就是一场灾难每次大变更后把复制槽列表和一周前的对比一次新增了没见过的slot立刻搞清楚来源。这几条说起来轻巧但真遇到业务方急着下线系统时很容易漏掉“线上还有slot”这个细节。我现在的做法是在变更单里加了一个必填字段——“是否清理主库复制槽”让流程强制大家想一遍。5.3 参数兜底把“不可能写满”变成现实流程解决人犯的错参数解决系统默认值带来的风险。对13及以上版本我建议至少做以下两个配置调整设置max_slot_wal_keep_size为一个有上限的值不要把-1留在生产环境将复制槽相关监控接入告警不要只看“磁盘使用率”这一层。另外需要说一句max_slot_wal_keep_size是一把双刃剑。设得太小可能导致备库/订阅端断线重连时找不到需要的WAL被迫重做全量同步。所以我的建议是先算出业务大促期间的峰值WAL产生速率再乘以你允许的最大断线时间最后加30%到50%的冗余。比如峰值一小时20GB允许断线4小时那设置成120GB左右比较合理。如果当前生产库版本比较旧比如11或12没有max_slot_wal_keep_size这个参数那么更要靠监控和流程来保证slot不会失控。复盘到这里我最深的体会是事故现场那一堆告警和数据真正值钱的不是“删除一条SQL”而是平时积累的“机制理解”。如果在建这套库的时候就弄懂了restart_lsn和WAL回收之间的关系就不会有后续两个月的磁盘风险。再说一个细节事故当天我其实还注意到磁盘使用率告警早在两周前就出现过一次91%当时查了归档、清了旧备份以为没事。但现在回头看那一次告警很可能就是WAL开始被钉住的表现。告警暂时消失不等于问题解决异常往往只是被另一块“还能扛住”的容量掩盖了。最后给所有看到这篇文章的DBA和运维朋友留一句现在就去跑一下那条pg_replication_slots查询看看你的库里有没有active为false、LSN滞后很大的slot。如果有趁磁盘还没满赶紧处理。十几分钟的操作能省下一次凌晨三点半的紧急救援。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑