PostgreSQL WAL文件膨胀排查:归档失败与僵尸复制槽的处置指南
2026/9/7 19:00:18 网站建设 项目流程

深夜的磁盘告警,永远是数据库运维最不想收到的消息。前阵子值班,一台PostgreSQL实例的数据盘使用率三小时内从70%冲到95%,监控指向的罪魁祸首是pg_wal目录。WAL文件大小失控这件事,几乎每个PG运维都会遇到一次,而且绝大多数时候,问题根源并不在参数配置上,而是整个保留链路的某个环节悄悄断掉了。

WAL的全称是Write-Ahead Logging,预写日志。它存在的意义很朴素:事务真正修改数据页之前,必须先把变更写入日志文件。这个机制让数据库可以延迟刷数据页而不用牺牲持久性——崩溃时只要从检查点开始重放WAL,就能把所有已提交事务找回来。正因为WAL承载着"崩溃恢复、流复制、时间点恢复"这三条命脉,它的保留和清理规则远不是"写满就删"那么简单。文件该留多少、什么时候删、谁在拦着不让删,每一环都可能出问题。

这篇文章我打算从WAL的运行机制讲起,把影响文件大小的所有参数逐个拆开,再把我亲身经历的一次WAL把磁盘写满的完整排查过程原样写出来,最后给你一套能直接抄走的监控巡检方案。刚接触PG的读者可以先看前两节,已经背过不少参数的老手,可以直接跳到第三节和第四节,里面那些坑你大概率也会踩到。

1. WAL文件的运行机制:为什么pg_wal目录会自己"膨胀"

1.1 WAL在PostgreSQL里的真实角色

WAL可以类比成店铺的收银小票,数据文件则是总账本。每一笔交易发生时,先把小票写好放好,回头再誊账本。如果誊到一半停电了,第二天拿着小票就能把所有账目重新誊一遍,不会漏账也不会重复记账。PostgreSQL在事务提交时,并不需要把数据页立刻刷到磁盘,只要确保对应的WAL记录落盘就行,这就是它能在普通硬件上做到较高写入吞吐量的根本原因。崩溃恢复时,数据库从最近一次检查点留下的重放位置开始,把WAL里的变更逐条重放到数据页,这个过程叫前滚。

在PG 10之前,WAL目录叫pg_xlog,PG 10之后改成了pg_wal。你翻老博客如果看到pg_xlog,注意这是版本差异。这个目录默认在数据目录下,里面放的是连续的WAL段文件,每个段默认16MB。段文件命名规则是24位十六进制,比如000000010000000000000001:前8位是时间线timeline,中间8位是逻辑日志ID,最后8位是段ID。排查"哪些WAL比较旧"的时候,先看懂这个命名规则能省不少时间。

1.2 16MB的段文件是怎么循环利用的

PostgreSQL的WAL不是写一个无限大的文件,而是一段一段推进,默认每段16MB。这里有个关键认知:对运维来说,真正会变的是"段文件的数量",而不是单个文件的大小。单个文件大小在initdb初始化时就被定死了,运行中无法修改,这个后面细说。

正常状态下,pg_wal目录会保持一定数量的段文件,形成一个动态平衡。数据库后台的行为可以理解成一个水盆:检查点之后,系统会把WAL总量尽量收缩到min_wal_size附近;当累积WAL量超过max_wal_size时,触发一次"及时检查点";检查点完成后,不再被引用的旧段会被回收,要么删除,要么改名复用。所以在负载平稳的实例上,pg_wal目录大小通常是在一个区间内反复波动的,不是恒定不变。只有负载突增、检查点跟不上、或者有外部因素一直引用WAL不让清理时,目录大小才会连续爬升,爬到让你后背发凉。

1.3 哪些操作最容易让WAL总量失控

结合我见过的案例,WAL暴涨基本逃不出下面几类:

  1. 大批量更新或删除。一次UPDATE影响几百万行、重建索引、大表ALTER,都会产生大量WAL记录。
  2. 长时间不结束的事务。事务开着不提交,数据页的修改对应WAL会一直处于需要保留的状态,同时会拖住最老的快照和重放位置,间接导致检查点无法推进、旧WAL无法清理。
  3. 检查点过于频繁或迟迟完不成。检查点本身要刷大量脏页,如果磁盘I/O能力不足,检查点迟迟不结束,LSN还在不断往前走,WAL量自然越积越多。
  4. 归档失败或复制槽不消费。这是最容易被忽略的原因,后面我用一整节讲。
  5. full_page_writes带来的整页镜像。每次检查点之后,每个数据页第一次被修改时,PG会把整个8KB页面写入WAL。检查点越频繁,整页镜像越多,WAL总量越大。

这里有个反直觉的结论:max_wal_size设得太小,反而可能让WAL总量变大。因为检查点频繁触发,full_page_writes产生的整页镜像变多,写放大更严重。很多人以为"把max_wal_size调小一点,WAL文件就不会占那么多空间",这基本是南辕北辙。这个原理我在第二节展开讲。

1.4 一个健康的pg_wal目录应该长什么样

没有任何复制、归档、长事务的纯开发库里,pg_wal目录一般只有几个到几十个段文件,总体积在min_wal_size(默认80MB)到max_wal_size(默认1GB)之间波动。如果你打开目录,看到几百上千个WAL文件,而且时间戳横跨好几天甚至几周,基本可以断定有东西在"钉住"这些文件不让清理。这时候排查引用者,比删除文件重要得多。

2. 决定WAL文件大小的参数,以及它们真正的作用边界

2.1 先分清"单个文件大小"和"目录总大小"

经常有人问"WAL文件大小怎么设置",这个问题其实分两层:

  • 单个WAL文件的大小:由wal_segment_size决定,默认16MB,在initdb时固定。
  • pg_wal目录的总大小:由"检查点机制希望保留多少"和"外部依赖强制保留多少"共同决定,对应参数是max_wal_size、min_wal_size、wal_keep_size,以及归档、复制槽、备份进程等运行时状态。

搞清楚这个区别,再看下面的参数表就不会混。

2.2 影响WAL文件数量的参数清单

参数默认值作用修改方式
wal_segment_size16MB单个WAL段文件的大小initdb时指定,运行中不可改
min_wal_size80MB检查点后WAL回收的目标下限动态修改
max_wal_size1GB触发及时检查点的软阈值,检查点后期望控制的上限动态修改
checkpoint_timeout300s两次检查点的最大间隔动态修改
checkpoint_completion_target0.9检查点目标完成时间占总间隔的比例动态修改
wal_keep_size0MB(PG15+)即使没有备库连接也强制保留的WAL量动态修改;旧版本为wal_keep_segments
archive_modeoff是否开启WAL归档修改后需重启
archive_command归档命令,归档成功才会清理被归档的段动态修改
wal_levelreplica控制WAL中记录的冗余信息量修改后需重启
full_page_writeson检查点后首次修改的数据页是否整页写入WAL动态修改,一般不建议关闭

wal_level有三个级别:minimal、replica、logical。如果开启逻辑复制或使用了某些逻辑解码工具,WAL里要记录更多元数据和变更信息,总量会比replica更高。生产环境一般至少要设为replica,因为流复制依赖它。minimal级别下某些操作甚至可以跳过WAL,但代价是无法做流复制和某些恢复操作,一般只在初始化或某些特殊场景下用。

2.3 max_wal_size为什么是"软限制"

max_wal_size的官方定义是"在检查点之间允许WAL增长到的软限制"。实际机制是:两次检查点之间累积的WAL量超过了这个值,系统会提前触发一次"及时检查点"。注意"软"这个字——如果磁盘I/O跟不上,检查点完成需要很长时间,WAL会继续增长,超过max_wal_size是完全正常的现象。

很多人把这个参数当成硬上限,一看pg_wal目录超过了1GB就紧张,其实未必有问题。真正需要担心的是"持续不回落",而不是"瞬间超过"。在I/O能力足够的机器上,用更小的max_wal_size来控制WAL文件总量是个错误方向,因为检查点越频繁,full_page_writes产生的整页镜像越多,写放大越严重,WAL总量反而可能更高。理解这个反馈回路,比死记参数值重要得多。

2.4 wal_keep_size:一个容易被配大的"保留参数"

PG 15之前,这个参数叫wal_keep_segments,单位是段数;PG 15开始改成wal_keep_size,单位是MB,语义也更直白:不管你系统里有没有备库连上来,都会在pg_wal里强制保留这么多量的WAL文件。默认是0,表示不做额外保留。

它的本意是给偶尔断连的备库留出追赶空间。但很多人配置时习惯写个偏大的值,比如wal_keep_size=1024,意味着pg_wal里永远至少有1GB的WAL不会被清理。如果磁盘本来就紧张,或者备库长期断开,这个"善意保留"就会变成磁盘压力来源。我在生产里见过一台主库,因为历史原因配了wal_keep_size=5120,配合逻辑复制槽和归档,pg_wal常年稳定在10GB以上。后来确认没有备库依赖,把它调成0,同时清理了失效复制槽,pg_wal直接缩到几百MB。检查配置的时候,这类"保留型参数"值得多看一眼。

2.5 归档和复制槽才是"最终决定者"

真正决定WAL能不能被清理的,是引用链。归档进程会引用"尚未成功归档"的WAL段;复制槽会引用restart_lsn之后的WAL段;正在运行的pg_basebackup会在backup_label里记录起始LSN,也需要保留相应范围的WAL。

所以哪怕你把max_wal_size调得再小,只要archive_command一直失败,或者复制槽长时间不被消费,pg_wal里的旧文件依然不会回收。这就像你把水龙头的出水量调小了,但下水道堵了,水池还是会满出来。第三节的排查案例,就是归档失败和僵尸复制槽同时出问题的典型场景。

3. 一次真实的"WAL把磁盘写满"排查过程

3.1 第一层:目录浏览,先分清"新文件"和"旧文件"

那次告警的数据盘使用率已经到92%,还在往上走。我第一件事不是去调参,而是先看清楚pg_wal里到底堆了什么东西。

du -sh $PGDATA/pg_wal ls -lt $PGDATA/pg_wal | head -20

输出显示pg_wal已经58GB,文件数量接近3700个,按16MB一个段算,数字对得上。更关键的是,目录里文件的修改时间跨度拉到了3天以上。正常负载平稳的实例,pg_wal里的文件时间跨度应该以小时计,跨度超过一天,说明有大量旧文件在排队等待清理。

接着查了当前WAL位置,确认写入侧没有异常:

SELECT pg_current_wal_lsn(), pg_walfile_name(pg_current_wal_lsn());

LSN推进正常,说明数据库写入没有问题,问题出在"清理"这一侧。到这里,方向已经收敛到"谁在引用旧WAL"。

3.2 第二层:归档状态,一眼看出命令是不是"假归档"

我打开了归档统计视图:

SELECT archived_count, failed_count, last_archived_time, last_failed_time, last_failed_reason FROM pg_stat_archiver;

结果很扎眼:archived_count停留在很久之前的数字,failed_count却在持续增加,last_failed_reason显示归档命令退出非零。再看配置:

SHOW archive_command;

命令指向了一个已经失效的备份脚本路径,等于每个WAL归档都在失败。归档失败意味着什么?PostgreSQL必须在某一段WAL被成功归档之后,才允许把这一个段从pg_wal回收。命令一直失败,旧文件就被全部挂在那里,越积越多。

这里有个容易忽略的点:archive_command失败不会让数据库停摆,也不会往应用侧报错,只在postgresql日志里反复输出归档失败信息。很多环境根本没看日志,这个问题可以潜伏非常久。另外要注意,last_failed_reason字段是PG 12才加入的,老版本只有失败计数,没有失败原因,排查起来更费劲。

3.3 第三层:复制槽,比归档更隐蔽的"钉子户"

修归档之前我顺手查了复制槽,因为归档和复制槽经常同时出问题:

SELECT slot_name, slot_type, active, pg_size_pretty(pg_current_wal_lsn() - coalesce(restart_lsn, '0/0')) AS lag, restart_lsn, pg_current_wal_lsn() FROM pg_replication_slots;

结果确实有一条老备用复制槽,active=false,restart_lsn停在好几天前的LSN,和当前LSN的差距换算下来超过20GB。这是个典型的僵尸复制槽——对应备库早就删了,但主库上的slot记录没清理。PostgreSQL会一直认为"可能有一个备库还需要这些WAL",所以restart_lsn之后的段全部保留。

复制槽比归档更隐蔽的地方在于,归档失败至少在日志里有痕迹,僵尸复制槽几乎完全静默。你不主动去查pg_replication_slots,它就在那里安安静静地让磁盘慢慢满掉。它和归档失败叠加的效果就是:旧WAL既不能因为"归档成功"被清理,也不能因为"槽消费完"被清理,只能无限堆积。

3.4 第四层:长事务和检查点,排掉最后几个可能

继续排查长事务和检查点,是为了避免遗漏其他引用者:

SELECT pid, state, now() - xact_start AS xact_age, left(query, 100) AS query FROM pg_stat_activity WHERE xact_start IS NOT NULL AND state <> 'idle' ORDER BY xact_start LIMIT 10;

没有超过半小时的长事务。接着看检查点压力:

SELECT checkpoints_timed, checkpoints_req, buffers_checkpoint FROM pg_stat_bgwriter;

checkpoints_req并不异常,说明检查点不是主要矛盾。到这里根因基本锁定:归档失败加上僵尸复制槽,两个问题叠加导致WAL只增不减。

3.5 处置顺序:先解引用,再让检查点回收

修复思路是"先解除引用,再触发清理",顺序不能反。

第一步,修正或临时变更archive_command。当时我一时无法确认正式的备份脚本是否恢复,又确认这台实例没有备库依赖归档,先把命令改为:

ALTER SYSTEM SET archive_command TO '/bin/true'; SELECT pg_reload_conf();

这样归档链路立刻"成功",等磁盘水位降下来再恢复真正的归档命令。生产中这样做要谨慎,先记录原值,确认临时变更的影响范围。

第二步,确认僵尸复制槽没有客户端使用后删除:

SELECT client_addr, state FROM pg_stat_replication;

确认列表里没有使用这个槽的备库,然后执行:

SELECT pg_drop_replication_slot('old_standby_slot');

第三步,让数据库做一次检查点,开始回收:

CHECKPOINT;

这一步不会瞬间删除所有文件,但会让系统重新评估引用边界。大约几分钟后,pg_wal目录开始明显下降,半小时内从58GB降到不到2GB。

第四步,把archive_command恢复为正式命令,继续观察归档成功数是否递增。整个处置过程大约40分钟,最花时间的其实是定位——归档失败在日志里有痕迹,复制槽却一点报错都没有,不去主动查很难发现。

4. 清理WAL的正确姿势:为什么"直接删文件"是事故的开始

4.1 删pg_wal文件可能引发的连锁反应

我见过不止一个团队,在磁盘告警的紧急状态下,直接rm $PGDATA/pg_wal/00000001...删掉一批"看起来很久"的WAL文件。出事的场景基本是这几种:

  • 主库崩溃后启动,PG发现需要重放某个段,但段文件已被删除,直接报requested WAL segment ... has already been removed,实例无法完成恢复。
  • 备库正在追赶进度,主库把备库还没消费的WAL删了,备库断流,只能重新做全量备份。
  • 删除文件时恰好在检查点前后,pg_control里的重放位置和实际文件不一致,数据页出现部分更新,逻辑上已经损坏。

为什么后果这么严重?因为WAL不是普通历史日志,它是数据库完整性的证据链。PostgreSQL的恢复过程按段号严格顺序读取,缺中间任何一段都没法重放到最新状态。你删掉的不是"没用的旧文件",而是可能正要被重放的起点。这也是为什么PG官方从没提供过"手动删除WAL"的工具,一切回收都交给检查点机制、归档状态、复制槽状态协同完成。

4.2 谁在"钉住"WAL不让清理

理解"为什么系统没有自己清理",比学会手动清理更有用。正常情况下检查点完成之后,旧段文件会被回收,但如果有以下引用者存在,对应范围的WAL段会被强制保留:

  • 归档进程:每一段WAL都需要被archive_command成功处理一次。
  • 复制槽:物理或逻辑复制槽的restart_lsn之后的WAL段都不得删除。
  • 正在进行的备份:pg_basebackup从开始到结束期间需要的WAL段。
  • 长事务:事务未结束,最老的快照和回放位置可能被钉住。
  • 最新检查点重放位置:redo point之后需要保留,redo point之前的段才有资格进入回收评估。

逐个排查这些引用者,才是解决WAL文件大小的正道。直接删文件等于绕过数据库协议去篡改证据链,风险完全不可控。

4.3 正确的清理流程,按顺序操作

如果磁盘真的告急,我的建议顺序是这样:

  1. 先看有没有活动备份或备库追赶:检查pg_stat_replication和pg_stat_archiver,确认有没有恢复中的备库在等待WAL。
  2. 处理归档失败:修复archive_command,让它能成功归档;实在不行先临时用/bin/true,但要记得恢复。
  3. 清理失效复制槽:确认没有备库在用后,用pg_drop_replication_slot删除。
  4. 处理长事务:联系应用提交或终止空闲事务。
  5. 再执行CHECKPOINT;,让系统按最新边界回收。
  6. 观察pg_wal目录是否回落到正常区间。
  7. 如果上述都处理完目录仍然没降,再考虑在维护窗口重启实例,让PG在启动时按当前redo point重新评估旧段。但重启是最后手段,不是在告警时第一时间做的事。

注意:任何情况下都不要直接在文件系统层删除pg_wal里的段文件。如果数据库已经因为WAL文件缺失或损坏起不来,唯一稳妥的路径是重新做全量备份并恢复。

4.4 一个"不得已"的应急场景

如果你面对的是纯开发或测试库,没有备库、没有归档、没有PITR需求,磁盘又真的要被灌满了,有没有更快的方法?有,但同样要按流程走:先确认没有长事务,再干净停库,然后启动。其实正常停库再启动一次,PG本身就会把大量不再需要的WAL段清理掉。这么做比重启风险小,也更符合数据库的运行逻辑——它自己知道哪些文件没人要了。

还要强调一句:数据库恢复或备份进程正在跑的时候,不要去动pg_wal。你看着某个文件时间戳很早,但它可能恰好是pg_basebackup在备份期间需要保留的起点。动之前用pg_stat_activity和pg_stat_progress_basebackup看一下,几十秒就能确认。

5. 把WAL文件大小纳入日常巡检和监控

5.1 直接抄走的巡检SQL组合

日常巡检不需要打开一堆视图,把下面几条SQL按顺序跑一遍,基本能覆盖WAL健康度的关键指标。

  1. 当前WAL目录总大小和文件数:
SELECT pg_size_pretty(sum(size)) AS total_wal_size, count(*) AS wal_file_count FROM pg_ls_waldir();

注意pg_ls_waldir()需要superuser或pg_monitor角色权限。给监控账号授权时建议直接赋予pg_monitor,不要给superuser。

  1. 归档状态:
SELECT archived_count, failed_count, last_archived_time, last_failed_time, last_failed_reason FROM pg_stat_archiver;

重点观察failed_count是否在多次巡检之间持续增加,last_archived_time是否长时间不更新。

  1. 复制槽健康度:
SELECT slot_name, slot_type, active, pg_size_pretty(pg_current_wal_lsn() - coalesce(restart_lsn, '0/0')) AS lag, restart_lsn, pg_current_wal_lsn() FROM pg_replication_slots;

lag越大,说明这个槽要求保留的WAL越多。lag超过一定阈值就要人工确认。

  1. 长事务:
SELECT pid, state, now() - xact_start AS xact_age, left(query, 80) AS query FROM pg_stat_activity WHERE xact_start IS NOT NULL AND state <> 'idle' ORDER BY xact_start LIMIT 10;
  1. 检查点统计:
SELECT checkpoints_timed, checkpoints_req, checkpoint_write_time, checkpoint_sync_time, buffers_checkpoint FROM pg_stat_bgwriter;

checkpoints_req持续增长,说明经常因为达到max_wal_size而触发"及时检查点",可以结合WAL总量判断是否需要调整参数。PG 14之后可以看pg_stat_checkpointer视图,字段更细,包含num_timed和num_requested。

5.2 命令行和监控系统侧的快速定位

如果你在SSH终端里应急,几条命令就够了:

du -sh $PGDATA/pg_wal find $PGDATA/pg_wal -name "0*" -mtime +1 | wc -l

第二行统计超过1天没被回收的段文件数量。正常情况下这个数字应该非常小,如果数量很大,基本可以确定存在引用者问题。

在有监控系统的环境里,建议把这些指标接入告警:

  • pg_wal目录大小,或占数据盘比例。
  • archive_command失败次数的增量。
  • 复制槽restart_lsn与当前LSN的滞后字节数。
  • pg_stat_replication中state='streaming'的备库数量,突然变0要引起注意。
  • 最老活动事务年龄,超过阈值要告警。

5.3 告警阈值和调参建议

告警阈值没有统一标准,我这边常用的做法:

  • pg_wal目录超过max_wal_size的1.5倍并持续30分钟以上,说明可能有堆积。
  • archive failed_count在轮询周期内有增长,立即告警。
  • 复制槽lag超过max_wal_size的2倍,或者超过2GB,需要人工确认。
  • 数据盘使用率超过80%进入重点观察,超过90%要准备应急方案。

如果经过排查确认没有引用问题,只是单纯写负载高、检查点跟不上,再考虑调参。顺序是:先确保归档和复制槽正常,再调整checkpoint_timeout或max_wal_size,每次调整幅度不要太大,观察几小时。max_wal_size从1GB调到8GB跨度看起来不大,但会让崩溃恢复时间明显变长。务必先在测试环境做一次崩溃恢复演练再上生产,别拿生产环境做实验。

最后说点个人体会。我做PG运维这些年,WAL文件大小报警十次里有八次不是配置问题,而是某条链路断了——归档命令改了没生效、复制槽建了忘删、备份脚本挂了没人知道。如果你只盯着max_wal_size去调,等于看着压力表去调温度旋钮,方向从一开始就错了。先把归档、复制、备份这些"引用者"捋顺,再回来看参数,你会少熬很多夜。另外,每隔一段时间清一次僵尸复制槽和失效归档命令,比任何花哨的监控配置都管用。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询