你有没有遇到过这种情况:一觉醒来,监控告警邮件躺了一堆,点开一看是磁盘使用率超过90%。登上服务器,df -h一看,数据盘快满了,再切到PostgreSQL的数据目录里瞄一眼,好家伙,pg_wal目录占了二十几个GB。于是你开始思考人生:WAL文件大小为什么能涨成这样?是不是数据库要坏了?要不要手动删文件?
先说结论:千万别直接删pg_wal目录里的文件,手动删WAL导致数据库崩溃恢复失败、实例直接起不来的案例我在社区见过太多。WAL文件大小这个问题,几乎每个PostgreSQL使用者都会遇到,而且绝大多数时候,WAL膨胀不是WAL本身的问题,而是checkpoint、归档、复制槽、长事务这些上下游环节在报警。这篇文章我就从WAL的原理、关键参数、监控方法到排查案例,完整梳理一遍,希望能帮你以后遇到类似问题时,能快速定位,不用再靠猜。
适合谁来读?如果你是PostgreSQL的DBA、运维,或者后端开发自己维护数据库实例,这篇文章值得花十分钟认真看完。就算你目前只是PostgreSQL新手,还没遇到过WAL膨胀,把cp的关系理清楚,后面能省很多事。
1. WAL文件是什么,为什么它的体积让人焦虑
1.1 WAL机制:先写日志再落数据,像记草稿本
WAL的全称是Write-Ahead Logging,中文一般叫预写式日志。PostgreSQL的写数据流程可以简化成三步:事务提交时先把变更写入WAL缓冲区,随后刷到磁盘上的WAL文件;checkpoint发生时,把脏页从共享缓冲区刷到数据文件;如果数据库崩溃,重启时通过WAL重放来恢复。
用生活里的例子类比,WAL就像你办公桌上的草稿本。你接到一个任务,先在草稿本上记一笔“今天要做X”,然后才开始动手整理正式的文件。万一中途电脑死机了,你翻翻草稿本就能知道之前的进度,不至于全忘光。数据库也是一样,如果每次提交事务都直接随机写数据文件,性能会非常差,因为数据文件是散落在磁盘各个页面的;而WAL是顺序追加写,顺序写的成本比随机写低一个数量级。PostgreSQL先把变更顺序写进WAL,事务就算提交成功了,后续脏页落盘可以慢慢来。这个机制从PostgreSQL诞生起就是核心设计,至今没有变过。
所以你要有一个基本认知:WAL不是垃圾数据,它是数据库的“后悔药”和“进度条”,是保证数据不丢、崩溃可恢复的关键。WAL文件增长本身不是问题,问题在于增长失控、回收不掉,那才是需要警惕的。
1.2 WAL文件在磁盘上的形态
WAL在磁盘上位于数据目录下的pg_wal子目录(10版本之前叫pg_xlog,当年升级到10的时候很多人找半天找不到这个目录)。每个WAL文件默认16MB大小,这个值在编译时通过--with-wal-segsize指定,绝大多数发行版都用默认的16MB,正常情况不需要去改它。
文件命名是一串24位十六进制数字,比如000000010000000000000001。这串数字拆开来看,前8位是时间线ID,中间8位是逻辑日志序号(logid),后8位是段序号(segid),整体对应着一个唯一的LSN(Log Sequence Number,日志序列号)区间。一个16MB的文件里可能只有开头一小段有实际数据,其余部分都是空闲的,但文件已经占用了16MB磁盘空间。所以你在pg_wal里看到100个文件,那就是1.6GB,不管里面写满了没有。
最直观的查看方式是执行:
sql复制SELECT size, name FROM pg_ls_waldir() ORDER BY name;
pg_ls_waldir()是PostgreSQL 10以后提供的函数,直接列出pg_wal目录里的文件和每个文件的大小,比去操作系统敲ls -lh方便,而且不需要登录服务器,只要数据库连得上就能查。
1.3 为什么WAL大小是一个值得盯住的指标
WAL目录的大小,正常情况下应该在一个相对稳定的区间内波动,不会无限膨胀。一旦出现WAL持续增长、回收不掉,通常意味着以下几种情况之一:
- checkpoint迟迟不能推进,导致旧WAL无法被清理;
- 归档命令失败,WAL在归档成功之前不能删除;
- 备库或复制槽异常,主库要为新加入的备库保留WAL;
- 存在长时间未提交或未结束的事务,oldest transaction的起始LSN之前的WAL都不能清理;
- 短时间写入量巨大,WAL生成速率超过归档和checkpoint的消化速率。
WAL文件大小膨胀带来的直接后果是磁盘被占满。PostgreSQL一旦因为磁盘写不进去而出错,会直接进入PANIC状态,实例宕掉,而且很可能无法正常重启,只能通过清理磁盘、恢复WAL之后才能拉起来。这个故障级别是非常严重的,所以在监控上一定要把pg_wal目录大小当作关键指标盯住。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 决定WAL文件大小的关键参数与配置逻辑
2.1 checkpoint三兄弟:max_wal_size、min_wal_size、checkpoint_timeout
要理解WAL文件大小,绕不开checkpoint。checkpoint是PostgreSQL后台的一个动作,作用是把共享缓冲区里的脏页全部刷到磁盘,刷完之后,这个checkpoint点之前的WAL就不再需要用来恢复数据库了,可以安全删除或回收。WAL文件能不能清理,就看checkpoint推进到哪了。
控制checkpoint的三个核心参数是:
max_wal_size:触发checkpoint的软上限,默认1GB;min_wal_size:checkpoint后WAL文件至少保留的量,默认80MB;checkpoint_timeout:距离上次checkpoint的最长间隔时间,默认5分钟。
它们的关系可以这样理解:PostgreSQL在WAL总量接近max_wal_size时,或者距离上次checkpoint超过checkpoint_timeout时,就会触发一次checkpoint,哪个条件先到就先执行哪个。所以WAL文件总量并不是一个严格遵守的硬上限,而是“尽量不让WAL超过这个值”。
这里有个容易被忽略的点:max_wal_size设的是从上一个checkpoint到下一个checkpoint之间允许产生的WAL总量,这个值如果设置得太小,会导致checkpoint频繁触发。每次checkpoint都要把所有脏页刷盘,刷盘量大就造成IO峰值,业务高峰期性能会被明显拖低。反过来,max_wal_size如果设置得很大,checkpoint频率降低,但WAL目录的体量会上升,而且崩溃恢复时需要重放更多的WAL,恢复时间也会变长。这是一个典型的容量与性能的权衡。
min_wal_size的含义,很多人以为是“保留WAL的下限”,其实它更像是checkpoint后,PostgreSQL会尽量把WAL文件数量裁剪到这个大小附近,但不是严格保证。如果你的WAL生成速率很低,pg_wal目录会慢慢被清理到min_wal_size左右的规模;如果生成速率高,目录大小主要是由max_wal_size和备份/归档条件决定的。
2.2 wal_keep_size与归档:到底在保留什么WAL
在13版本之前,有一个参数叫wal_keep_segments,表示额外保留多少个WAL段,目的是让新加入的备库或者断连后重连的备库能从主库追日志。13版本开始改名为wal_keep_size,单位从“段数”变成“大小”,默认值是0,也就是不额外保留。
wal_keep_size和归档是两套独立的WAL保留机制,但经常被一起提起。archive_mode = on配合archive_command,主库会在每个WAL文件写满后调用归档命令,把文件复制到归档目录。PostgreSQL默认逻辑是:一个WAL文件如果已经被归档成功,并且它包含的数据对恢复和备库都不再需要了,才会被清理或回收。如果归档一直失败,pg_wal里的文件就会越积越多。
我遇到过不少案例,archive_command写的路径不对、权限不对,或者归档目标磁盘满了,结果主库pg_wal一路涨到几百GB。这种问题本身不难查,看一眼pg_stat_archiver视图里的failed_count和last_failed_time就能发现端倪,但很多人一开始不会往这个方向想。
另外注意,archive_mode开启后,WAL的保留策略会变得保守:即使checkpoint已经推进得很远了,只要归档还没完成,文件就不会删除。如果归档命令本身写得不好,比如没有重试机制、或者传输工具偶发失败,就会造成WAL堆积速度大于清理速度,日积月累就是一个大麻烦。
2.3 复制槽才是WAL堆积的最大“黑客”
复制槽(replication slot)是很多WAL膨胀事故的元凶,尤其当你使用了基于复制槽的流复制或逻辑复制时,这个问题几乎一定会碰到。
复制槽的工作原理是:备库或逻辑复制订阅端通过复制槽从主库读取WAL,主库会记录这个槽当前消费到了哪个LSN。只要复制槽没有推进到最新位置,主库就认为“可能还有节点需要它之前的WAL”,于是那些WAL文件必须保留。如果备库宕机了、断网了、或者客户端逻辑复制进程挂了,复制槽的restart_lsn就会停在那里不动,主库的WAL随之不断累积。
在13版本之前,复制槽对WAL的保留是没有上限的,一个失联的备库可以把主库磁盘写满,并且没有任何自动保护机制。13版本引入了max_slot_wal_keep_size参数,可以限制每个槽位最多保留多少WAL,超过之后这个复制槽会被标记为invalid,备库就不能再从这个槽继续复制了,往往需要重建备库。这个参数在一定程度上保护了主库磁盘,但也意味着你必须在“主库磁盘爆掉”和“备库需要重建”之间做一个取舍。
查看复制槽的状态,用这条SQL:
sql复制SELECT slot_name, slot_type, active, restart_lsn,
pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS retained_wal
FROM pg_replication_slots;
retained_wal这一列算出来就是当前每个槽“拖住”了多少WAL。如果这个值在持续增长,那基本可以锁定WAL膨胀的源头。
2.4 配置估算示例:一套可以套用的计算公式
实际操作中,max_wal_size到底设多少合适,很多人是拍脑袋的。我提供一个估算思路,你根据自己业务的写入量来算。
第一步,估算高峰期每秒产生的WAL量。可以用pg_stat_database的xact_commit、xact_rollback变化量,或者更直接地,取两个时间点执行pg_current_wal_lsn(),用LSN差值除以时间间隔,得到平均每秒WAL生成量。高峰期建议观察至少三天,取一个安全值。
第二步,确定期望的checkpoint间隔。一般建议checkpoint间隔不要低于5分钟,也不要超过30分钟。过短会导致频繁刷盘影响性能,过长会导致崩溃恢复时间变长。
第三步,用公式估算max_wal_size:
code复制max_wal_size ≈ 高峰每秒WAL生成量 × checkpoint_timeout × 1.5系数
举个例子,业务高峰期每秒生成10MB WAL,checkpoint_timeout设为10分钟,那10分钟会产生6GB。乘上1.5的安全系数,max_wal_size可以设置为9GB。这只是一个起点,实际运行后还要观察checkpoint频率和WAL目录大小再做微调。
另外,磁盘空间的预留必须考虑最坏情况。我的经验是,数据盘上至少要预留max_wal_size × 2 + 归档堆积余量的空间,如果你有多个备库且都配置了复制槽,还要再加一个max_slot_wal_keep_size的量。宁可空间多留,不要等到WAL涨起来才发现磁盘不够。
3. 监控WAL大小与增长趋势的实操方法
3.1 一条SQL看清当前WAL状态
处理WAL问题第一步,是先搞清楚当前pg_wal到底有多大、增长快不快。下面这条SQL可以一次拿到核心信息:
sql复制SELECT count(*) AS wal_file_count,
pg_size_pretty(sum(size)) AS wal_total_size,
pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), '0/0')) AS lsn_offset
FROM pg_ls_waldir();
count(*)是WAL文件个数,每个文件16MB,文件个数乘上16MB就是总量。pg_wal_lsn_diff(pg_current_wal_lsn(), '0/0')算的是从集群初始化到当前总共产生了多少WAL,这个值通常没什么实际意义,但如果你把两次查询的pg_current_wal_lsn()差值算出来,就能得到这段时间的WAL生成量。
如果想知道当前正写到哪个文件,可以执行:
sql复制SELECT pg_walfile_name(pg_current_wal_lsn()) AS current_wal_file;
这个函数返回当前正在写入的WAL文件名,对于排查“数据库为什么写这么慢”“当前在写哪个日志段”这类问题很有帮助。
3.2 如何判断WAL增长是否异常
看到pg_wal目录有5GB,先别慌,要判断这个大小是否正常,需要结合几个维度。
先看checkpoint是否正常推进:
sql复制SELECT checkpoints_timed, checkpoints_req,
pg_size_pretty(memory_used) AS memory_used,
checkpoint_write_time / 1000 AS checkpoint_write_seconds
FROM pg_stat_bgwriter;
在PostgreSQL 17之前,checkpoint统计信息在pg_stat_bgwriter里;17版本开始拆到了pg_stat_checkpointer。如果checkpoints_req数量远大于checkpoints_timed,说明很多checkpoint是因为达到max_wal_size而触发的,换句话说WAL生成速度太快,参数需要调整。
再看归档状态:
sql复制SELECT archived_count, failed_count,
last_archived_time, last_failed_time
FROM pg_stat_archiver;
如果failed_count在增长,或者last_failed_time非常接近当前时间,说明归档链路有问题。last_archived_time如果停在很久之前,说明已经有很长时间没有成功归档了,那pg_wal里堆着的一定是归档不出去的文件。
然后看复制状态:
sql复制SELECT client_addr, state, sent_lsn, replay_lsn,
pg_size_pretty(pg_wal_lsn_diff(sent_lsn, replay_lsn)) AS replay_lag
FROM pg_stat_replication;
replay_lag如果持续增长,说明备库跟不上主库,主库为了保留备库需要的WAL,回收会变慢。
最后看一眼有没有长事务:
sql复制SELECT pid, state, now() - xact_start AS xact_age,
pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), xact_start::pg_lsn)) AS wal_since_xact
FROM pg_stat_activity
WHERE xact_start IS NOT NULL
ORDER BY xact_start
LIMIT 5;
PostgreSQL清理WAL时,必须保留最老活动事务开始时间之后的WAL。如果一个事务开了好几个小时不提交不回滚,它之后的WAL全都删不了,和复制槽拖住WAL是同一个道理。
3.3 监控脚本与告警建议
WAL问题靠人工盯是不现实的,建议至少加入监控告警。告警策略不需要太复杂,核心就两个指标:
pg_wal目录总大小:超过max_wal_size的2倍时发警告,超过3倍时发严重告警;- WAL生成速率:按5分钟间隔采集两次
pg_current_wal_lsn(),计算差值并换算成MB/s,持续超过阈值时告警。
如果你用Prometheus + pg_exporter,官方exporter本身就带pg_wal相关指标,比如pg_wal_size_bytes,直接配告警规则就行。如果没用监控系统,写一个简单的定时脚本也可以:
bash复制#!/bin/bash
psql -t -A -c "SELECT pg_size_pretty(sum(size)) FROM pg_ls_waldir();"
配合crontab每小时执行一次,大小超过设定阈值时调用webhook或者发邮件。脚本本身很简单,关键是要坚持查、持续看,形成基线数据,你才知道什么是对你的业务来说是“正常”的。
4. 常见问题与排查实录
4.1 案例一:WAL一直涨不回收,磁盘告警
之前帮一个客户排查过这样的问题:pg_wal目录从4GB一路涨到30GB,磁盘使用率告警。我上服务器后的排查顺序是这样的:
第一步,运行pg_ls_waldir()确认总量和文件数量;
第二步,查pg_replication_slots,发现有两个逻辑复制槽,其中一个active是false,restart_lsn停在三天前的位置。这就是问题所在——这个逻辑复制槽对应的订阅端早就连不上了,但槽还一直存在,主库为了保护它,三天内的WAL一个都不能删。
第三步,和业务确认这个订阅端已经不再使用后,执行pg_drop_replication_slot('slot_name')删掉槽。槽一删,WAL回收立刻恢复,加上手动执行一次CHECKPOINT,pg_wal目录很快就降下来了。
这个案例里最值得记住的一点是:不要凭感觉判断“这个槽好像没用了”就直接删,一定要和业务确认。删复制槽是不可逆的,如果备库或订阅端还在使用,删了之后只能重建,代价很大。
4.2 案例二:备库断连导致主库WAL堆积
还有一个常见场景:主备架构中,备库因为机房网络故障或者主机宕机,和主库断连了几个小时甚至一天。等主库的WAL被写满几百个文件,备库还停留在断连时刻的LSN,主库会因为备库追不上而保留大量WAL。
这种场景下,如果配置了synchronous_standby_names,情况会更严重——主库的写入可能直接hang住,因为同步备库确认不了提交。如果是异步复制,只是WAL堆积,业务不受影响。
处理方式有两种:
- 快速恢复备库,让备库重新连上主库追日志,追平之后WAL自然回收;
- 如果备库短期内无法恢复,且主库磁盘空间紧张,可以考虑删除对应的复制槽,代价是备库恢复后可能要从头做一次
pg_basebackup。
在PostgreSQL 13以上,max_slot_wal_keep_size参数提供了一个缓冲手段:WAL超过槽位上限后槽自动失效,主库不会被拖垮,但备库必须重建。这个参数我建议一定要设置上,具体值可以根据你的磁盘空间来定,常见做法是max_slot_wal_keep_size = max_wal_size或者略大一些。
4.3 案例三:checkpoint太频繁导致IO抖动
有时候WAL大小并不夸张,但业务侧反馈数据库有规律的“卡顿”,每隔几分钟就出现一次IO尖峰。查了pg_stat_bgwriter,发现checkpoints_req非常高,说明大部分checkpoint都是因为max_wal_size太小而触发的。
这种业务通常写入量大,但max_wal_size还停留在默认的1GB,checkpoint_timeout也维持默认的5分钟。结果是每过几十秒就达到1GB的WAL量,触发一次checkpoint,每次checkpoint都要刷大量脏页,IO自然剧烈抖动。
调整方案是把max_wal_size从1GB调大到8GB甚至更高,checkpoint_timeout可以保持5分钟或者延长到10分钟,让checkpoint的频率降下来。改完参数后记得reload配置,然后观察checkpoints_req是否明显下降。这类调整一般不需要重启实例,pg_ctl reload或者执行SELECT pg_reload_conf();即可生效。
需要注意,max_wal_size调大之后,崩溃恢复时间会变长。如果你在云上托管数据库,实例宕机自动拉起的时间会有所增加,这个trade-off要在改动前想清楚。
4.4 问题速查表
| 现象 | 可能原因 | 处理建议 |
|---|---|---|
| WAL持续增长,磁盘告警 | 复制槽inactive或备库断连 | 确认无用的槽直接删除,恢复备库,或设置max_slot_wal_keep_size |
| WAL增长且归档目录没新文件 | archive_command失败或卡住 |
检查pg_stat_archiver,修复归档命令、路径、权限 |
| WAL增长且存在长事务 | 事务迟迟不提交或回滚 | 联系业务处理长事务,必要时pg_terminate_backend |
| checkpoint频繁,IO规律抖动 | max_wal_size过小 |
调大max_wal_size,延长checkpoint_timeout |
| WAL总量不大但目录很大 | wal_keep_size设置过大 |
按备库追日志的实际需要下调wal_keep_size |
| 一次性大量数据导入后WAL暴涨 | 正常现象 | 预留磁盘空间,导入完成后checkpoint,WAL自动回收 |
5. 一点调优心得
WAL文件大小这件事,说到底不是“文件太大怎么办”的问题,而是“为什么WAL该回收却没回收”的问题。我个人的习惯是,新接手一个PostgreSQL实例,第一件事就是把pg_replication_slots、pg_stat_archiver、pg_stat_bgwriter这三个视图翻一遍,确认WAL的出口和回收通道都是通的,再谈别的优化。
另外,WAL目录大小一定要有基线数据,不要等到告警响了才开始排查。建议平时没事就记录一下pg_wal目录大小和LSN的位置,积累一两个星期的数据后,你对自己的业务写入特征会有非常直观的认识。哪天WAL异常增长,扫一眼就能判断出大概哪个环节出了问题。
如果非要给一个优先级排序,排查WAL膨胀时先查复制槽,再查归档,再查长事务,最后才看checkpoint参数。因为前三个更多是异常状态导致的,而checkpoint参数可能是配置不合适,调整起来相对温和,不急着动手。最后再分享一个小技巧:在维护窗口执行CHECKPOINT;可以让WAL快速回收一部分,但前提是复制槽和归档链路都正常,否则手动checkpoint并不能解决根本问题。
