一次数据库磁盘写满事故,WAL 被失效复制槽和归档失败双重保留,暴涨的日志又吃掉了最后一点空间,最终把可写数据库推成只读。
某业务集群的数据库主机在业务高峰期出现写入失败。应用侧大量请求返回 5xx,日志里出现 No space left on device;数据库日志先报无法写入 WAL 段,随后触发 PANIC 并重启。重启后数据库虽然能接受只读查询,但任何写入都失败,DBA 临时打开 default_transaction_read_only,让写请求快速失败而不是继续堆积。
主机上 df -h 显示数据盘使用率 100%,df -i 的 inode 仍有余量,说明不是小文件耗尽。业务表数据量近期没有明显增长,慢查询也未同步恶化;增长的是 WAL 和日志目录。监控只在整盘使用率到 95% 时告警,未按目录拆分,也未覆盖复制槽和归档。
df -h /var/lib/postgresql /var/log
df -i /var/lib/postgresql /var/log
du -x --max-depth=2 /var/lib/postgresql | sort -h | tail -20
du -x --max-depth=1 /var/log | sort -h | tail -20
psql -Atc "SELECT slot_name, active, restart_lsn,
pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS retained
FROM pg_replication_slots;"
psql -Atc "SELECT archived_count, failed_count, last_failed_wal, last_failed_time
FROM pg_stat_archiver;"
psql -Atc "SELECT pg_current_wal_lsn(), pg_walfile_name(pg_current_wal_lsn());"
pg_replication_slots 中存在一个 active = false 的槽,restart_lsn 停留在很久以前,意味着它仍然钉住大量 WAL;pg_stat_archiver 的 failed_count 持续增长,说明归档也没有成功。两条证据解释了 WAL 只增不减。
VACUUM FULL 试图回收空间。VACUUM FULL 需要额外的临时空间并产生新的 WAL,剩余空间被迅速耗尽。pg_wal 占用最大,远超业务表数据量。数据库写入能力中断约 45 分钟,其中约 30 分钟处于人工只读状态。所有依赖该库写入的业务都受到影响,应用侧出现 5xx、事务回滚和消息积压;读请求在只读模式下基本可用,但报表查询与在线事务争抢 I/O,延迟明显上升。事务在提交阶段失败,未观察到已提交数据丢失;但存在 fsync 失败风险,恢复后仍需校验关键表。
只读副本因为主库无法写 WAL 而停止推进,恢复后需要追赶大量日志;依赖复制的报表、搜索索引和异步消费者在这段时间内数据陈旧。日志暴涨还影响了同机的日志采集和监控代理,部分告警在故障中期缺失。故障没有演变成主从切换,是因为数据目录仍然完整、释放空间即可恢复。
第一个误导是“表膨胀”。监控里某张大表确实很大,慢查询也偶尔出现,于是 VACUUM FULL 看起来是合理的空间回收手段。但 VACUUM FULL 需要额外的磁盘空间来重写表,还会产生大量 WAL;在接近满盘时执行只会雪上加霜。目录统计显示 pg_wal 增长远大于表数据,表膨胀并非主因。
第二个误导是“删掉旧 WAL 就能立刻恢复”。WAL 是崩溃恢复和复制的核心文件,直接在文件系统里删除 pg_wal 下的段文件,可能让数据库无法完成恢复,甚至造成数据损坏。正确做法是先找出谁在保留 WAL,再通过数据库提供的机制释放,而不是把 WAL 当成普通日志清理。pg_replication_slots 和 pg_stat_archiver 的输出,正是推翻“WAL 可以随便删”的证据。
第三个误导来自“归档失败不影响主库”的经验。归档命令失败通常不会立刻阻塞业务,但在开启归档的情况下,WAL 段在成功归档前不能被回收;如果归档目标也不可写,WAL 就会持续堆积。监控只报磁盘使用率,未报 failed_count,这条线索长期被忽略。另一个误判是“删了日志但空间没释放”:进程仍持有已删除文件时 df 不会下降,可用 lsof +L1 确认。
直接原因是 WAL 保留失控。一个已经下线的分析消费者曾经使用逻辑复制槽订阅数据,下线时只停了消费者,没有删除复制槽。复制槽的 restart_lsn 因此长期停在旧位置,PostgreSQL 必须保留从该位置开始的全部 WAL,无法按正常策略回收。与此同时,归档命令指向的目标不可写,archive_mode 开启后 WAL 段在归档成功前也不能删除,两条保留链路叠加,WAL 目录持续增长。
空间被吃光的加速器是日志。数据库配置了记录所有语句的日志级别,故障期间连接失败和重试又产生大量错误日志,logging_collector 把日志写在同一个数据盘上。磁盘越满,写入失败越多,日志越多,形成正反馈。VACUUM FULL 在错误的时间执行,进一步消耗了本已紧张的剩余空间和 WAL 额度。
管理层面缺少按对象的容量监控与下线流程。监控只看整块盘的使用率,没有看 WAL 目录增速、复制槽延迟、归档失败次数和日志目录大小;消费者下线没有强制检查复制槽、订阅和权限;磁盘告警只有 95% 一档,没有在 70% 或 80% 时留出处置窗口。几个缺口叠加,把可提前发现的问题拖成了写入中断。
恢复顺序是“先止血、再释放、最后恢复写入”。先停止写入流量或将数据库置为只读,避免持续产生 WAL 和错误日志;保留现场,不要直接删除 pg_wal 或数据目录中的任何文件。然后清理可以安全删除的旧文本日志、轮转日志和临时文件,为后续操作腾出最小可用空间。
确认复制槽对应的消费者确实已经下线后,在数据库中删除失效槽:
psql -c "SELECT pg_drop_replication_slot('slot_name');"
只允许删除 active = false 且业务方确认不再需要的槽;如果槽仍可能被使用,应先恢复消费者或让其跳过到最新位置,而不是直接删除。归档命令要先用数据库运行用户手工验证,确认目标路径可写、权限正确、空间充足,再让 PostgreSQL 继续归档。修复后观察 pg_stat_archiver.failed_count 是否停止增长、WAL 目录是否稳定、复制槽是否清空。
psql -Atc "SELECT pg_switch_wal();"
psql -Atc "SELECT archived_count, failed_count FROM pg_stat_archiver;"
du -sh /var/lib/postgresql/pg_wal
如果数据库曾因写 WAL 失败而 PANIC,释放空间后按正常流程重启,并检查启动日志和关键表一致性;不要使用强制恢复参数跳过检查。写入恢复后,先限制写入速率,确认归档、复制和检查点都正常,再逐步放开业务流量。最后扩容数据盘或迁移日志目录,给后续增长留出余量。
把 WAL 相关的“隐性容量”纳入监控:WAL 目录大小与增速、最老复制槽的落后字节数、归档失败次数与最近失败时间、复制延迟、checkpoint 频率。磁盘告警至少分两级,70% 到 80% 进入观察和清理,90% 以上自动限制低优先级写入并通知值班。日志目录单独挂盘,配置轮转和保留天数,避免业务日志与 WAL 争抢同一块盘。
下线流程要覆盖数据库对象:停消费者、删订阅、删复制槽、回收账号、移除监控,每一步都可检查。归档目标要纳入容量和可达性监控,定期用测试文件验证写入;归档失败必须告警,不能只看主库是否可写。定期演练副本追平和从备份恢复,确认更坏情况下仍有可执行路径。
合上页面,先回答,再展开对照。
pg_wal 下的文件?pg_wal 是崩溃恢复和复制的核心。直接删除可能让数据库无法完成恢复,造成数据损坏或复制中断。应通过删除失效复制槽、修复归档、让数据库正常回收 WAL 来释放空间。restart_lsn 会钉住其后的所有 WAL,直到消费者确认收到;开启归档后,WAL 段在成功归档前也不能删除。两者叠加时,WAL 会持续增长,直到磁盘耗尽。
小程序版
微信扫码,随时查命令