提到PostgreSQL,绕不开wal日志,无论你是刚装好PostgreSQL准备跑第一个业务,还是已经在维护几十套实例,wal都是迟早要面对的。不少人在安装PostgreSQL之后遇到数据目录膨胀、主从同步断开、数据库突然crash,排查到最后基本都会回到wal日志上来。这篇文章是我维护PostgreSQL多年的一个总结,目标就是把这套东西讲透,从概念层面和操作层面各过一遍,让你看完之后至少能做到:知道wal是什么,知道它在哪些场景起作用,遇到问题知道从哪些方向排查。
文章把内容拆成四块:核心概念、参数配置、日常操作、故障排查。统的菜鸟讲解和常年踩坑的老手心得都会放在里面,正文里的SQL和命令我都在实际环境验证过,可以直接抄。
1. wal日志到底是什么:从一次崩溃恢复说起
1.1 数据库为什么不敢直接改数据文件
要理解wal,先理解一个基础问题:数据库为什么不直接把数据写进数据文件,非要先在日志里记一笔?
原因是性能。如果每次插入、更新都立刻修改磁盘上的数据文件,那一次写操作就要先找到目标行所在的数据页,然后做随机写入。磁盘随机写的速度比顺序写慢了至少一个数量级,这在低并发下还能忍,一旦堆高写入,整个数据库就会被磁盘拖死。
所以PostgreSQL采用了“先记日志、延迟落盘”的思路。事务提交时,只把变更描述顺序写入wal日志文件,这是纯顺序写;数据文件本身可以继续留在内存里的shared buffer中,等到合适的时机再统一刷盘。换句话说,wal日志像是给数据库加了一道保险:数据文件哪怕还没来得及刷新,只要wal日志还在,重启之后就可以通过重放日志把缺失的变更补回去。
这个设计就是“预写式日志”的核心思想,英文叫Write-Ahead Logging,有时候也翻译成预写日志。我见过不少新人把它理解为“数据库把数据先写到日志里”,其实不准确。wal里记录的并不是最终的数据快照,而是描述这次变更的物理信息,比如“在文件16384的偏移量为某处的一个页面被修改成了什么样子”,这样重放时才能精确重构变更后的页面。
1.2 六个必须搞懂的核心术语
网上讲wal的文章很多,但概念零散,这里我把日常工作中打交道最多的六个术语集中梳理一遍。
LSN(Log Sequence Number,日志序列号):LSN是一个单调递增的64位整数,用来定位wal日志中的字节位置。你可以把它理解成书页码,每次写日志都会向前推进。PostgreSQL的事务提交、检查点、复制都依赖LSN来做对齐。比如主库和备库之间同步,本质就是备库追着主库的LSN跑。
检查点(Checkpoint):检查点是一个时间节点,表示在某个LSN之前,所有变更都已经从shared buffer刷到了数据文件。它的作用是收缩恢复范围。恢复时不需要重放全部wal日志,只需要从最近一个检查点开始重放就行了,所以检查点做得越频繁,crash后恢复的时间就越短,但这也会增加刷盘频率,消耗IO。
wal段文件(WAL Segment File):wal日志在磁盘上以段文件的形式存在,默认一个文件16MB,文件放在数据目录的pg_wal子目录下面,这个目录在PG10之前叫pg_xlog,升级之后改名为pg_wal。段文件内部继续划分成页,每页8KB。
wal归档(WAL Archiving):把已经写满、即将被循环复用的wal段文件拷贝到其它存储位置,就叫归档。归档是为了做备份和恢复场景,你可以把归档理解为wal日志的长期保存仓库,而pg_wal目录里的wal段文件只是短期生产的临时区域。
归档模式(Archive Mode):归档开启的前提是将wal_level设置为replica(或logical),并设置archive_mode为on,同时提供archive_command命令,PostgreSQL才会在切换段文件时调用该命令进行归档。这里隐含一层关系:wal_level决定日志里记录多少信息,archive_mode决定要不要做归档,archive_command决定归档动作怎么做。
full_page_writes(全页写):full_page_writes开启时,PostgreSQL在检查点之后第一次修改某个数据页时,会把整页完整写入wal日志,而不仅仅是记录差异。这样做的原因是,在崩溃恢复时,如果数据文件里的页是一个“半旧半新”的状态,只有记录差异是无法还原的,必须有一个完整页面作为基准。这个参数几乎应该永远保持为on,除非你非常清楚关闭的后果。
为了方便记忆,我把这些概念的相互关系整理成一句话:事务修改数据时产生LSN推进,wal日志按LSN顺序写入段文件;检查点把数据页刷盘并推进恢复起点;wal_level控制日志记录粒度,归档模式按期拷贝段文件形成长期备份;full_page_writes保证恢复时页面完整可重建。
关于wal日志的更多概念,比如时间线、备库的replay进程、日志页校验,后面在操作和排查部分也会穿插补充,这里先记牢上面六个,它们已经覆盖了九成的日常讨论场景。
2. wal相关参数:一份带解释的配置清单
2.1 关键参数速查
PostgreSQL关于wal的参数都集中在postgresql.conf里,按英文分组是WRITE-AHEAD LOG这一大段。下面这张表是我整理出来的核心参数速查,后面会逐个展开说明。
| 参数名 | 默认值 | 作用说明 |
|---|---|---|
| wal_level | replica | 日志记录的详细信息级别,可选minimal、replica、logical |
| wal_buffers | -1(自动) | 共享内存中留给wal缓冲区的容量,相当于是日志的内存缓冲池 |
| wal_writer_delay | 200ms | wal写进程的刷盘间隔 |
| wal_writer_flush_after | 1MB | wal写进程累计写入量超过该值时触发刷盘请求 |
| max_wal_size | 1GB | 触发检查点的wal日志量软上限,影响检查点间隔 |
| min_wal_size | 80MB | 检查点之后pg_wal目录的目标保留体积,低于该值时不会立即回收 |
| checkpoint_timeout | 300s | 距离上一次检查点超过该时间就触发一次检查点 |
| checkpoint_completion_target | 0.9 | 检查点刷盘动作分散到该比例的下一次检查点周期内完成 |
| archive_mode | off | 是否开启wal归档 |
| archive_command | 空 | 归档时执行的shell命令,如cp %p /backup/%f |
| archive_timeout | 0(关闭) | 超过该秒数强制切换并归档当前wal段,适合低写入场景 |
| wal_keep_size | 0 | pg_wal目录中额外保留的wal段文件的量,用于备库连接延迟的情况 |
| max_slot_wal_keep_size | -1(不限) | 复制槽允许保留的wal日志最大体积,防止pg_wal无限膨胀 |
| full_page_writes | on | 检查点后首个页面修改时是否写全页 |
| synchronous_commit | on | 事务提交时是否需要等待wal刷盘成功 |
2.2 参数怎么选:按场景给出建议
这些参数里最值得你先去检查的,是wal_level、synchronous_commit、full_page_writes、max_wal_size和checkpoint_timeout。
wal_level这个参数很关键。如果你只是单机跑一个小应用,不需要做主从复制,也不需要做逻辑解析,把wal_level设成minimal可以减少一部分日志写入量,对性能有一点点帮助。但如果你以后可能要做流复制、做备份恢复,那必须保持replica。注意,wal_level的修改需要重启数据库才能生效,不能通过reload完成,所以在初始化实例之前就规划好比较省事。
synchronous_commit控制事务提交时是否等日志真正落盘。它有三个常用值:on表示提交时等wal刷进磁盘才返回成功,这是最安全的配置,也是默认值;off表示不等待刷盘就返回成功,性能更高,但数据库进程crash时可能丢失最近一小段提交记录;remote_apply用于同步复制的备库场景,要求备库已经应用完日志才返回。业务允许的话,性能敏感型系统可以把synchronous_commit设成off,但这同时意味着你不会拥有“零丢数据”的保障,很多金融和订单系统是接受不了这个配置的。
full_page_writes前面说过,是崩溃恢复的安全底线。我见过有人为了省IO把它关掉,理由是“磁盘很稳”。这个赌注太冒险,只要操作系统crash过一次,页就可能处于半更新状态,此时没有全页记录,恢复时轻则数据文件损坏,重则整库不可用,代价远超省下来的那点IO。这个参数不要动,保持默认on就是最好的实践。
max_wal_size和checkpoint_timeout共同决定检查点频率。max_wal_size是触发检查点的日志量软上限,默认1GB,意思是wal日志累计产生到接近这个量级时,系统会安排一次检查点,把数据刷盘并推进检查点位置。min_wal_size则是检查点完成后pg_wal目录期望保留的体积,如果目录里日志太少,系统会考虑这部分空间没必要释放太快。如果业务需要每秒产生大量日志,默认值会导致检查点过于频繁,建议把max_wal_size调大,比如4GB、8GB,同时把checkpoint_completion_target保持在0.9,让刷盘动作分散开,避免IO尖峰。
wal_buffers默认值是-1,表示按shared_buffers的1/32自动计算,通常取值范围64KB到2GB。如果业务有大事务,一次写入的wal量很大,可以手动设置为16MB、32MB,减少缓冲区不够时的用户进程阻塞等待。
2.3 参数调整后要做什么
postgresql.conf里的参数,改完之后有的可以即时生效,有的必须重启。判断方法很简单:在psql里执行
SELECT name, context FROM pg_settings WHERE name IN ('wal_level', 'synchronous_commit', 'full_page_writes', 'max_wal_size');context字段会告诉你生效方式。postmaster表示需要重启,sighup表示只需要pg_reload_conf()重载,user表示会话内部可以修改。wal_level和max_wal_size这些重要参数都需要重启,synchronous_commit和archive_command则可以通过重载生效。
实际工作中我经常用这个SQL批量确认改动是否正确加载:
SELECT name, setting, unit, context FROM pg_settings WHERE name LIKE '%wal%' OR name LIKE '%checkpoint%' OR name LIKE '%archive%' OR name LIKE '%slot%';这个查询能一次看到所有wal相关参数的当前值、单位以及生效状态,在排查问题时效率很高。注意参数改完并不代表配置合理,还要结合实际负载观察一段时间,重点看pg_wal目录体积、归档是否跟上、检查点间隔是否均匀。
3. wal日志日常操作与归档恢复
3.1 查看wal状态的几个命令
和wal直接打交道的操作,核心场景就三个:查看状态、手动切换、归档恢复。
查看当前wal位置,用下面几个函数:
-- 当前wal写入位置 SELECT pg_current_wal_lsn(); -- 当前wal文件对应的名称 SELECT pg_walfile_name(pg_current_wal_lsn()); -- 距上次检查点经过多少字节的wal SELECT pg_current_wal_lsn() - checkpoint_lsn FROM pg_control_checkpoint();保留LSN是一个文本类型的结构,比如0/16E72A8,它可以直接做加减运算,差值的单位就是字节。pg_control_checkpoint()返回的信息在排查问题时非常有用,它会显示最新的检查点LSN、redo位置、检查点时间等。
要查看pg_wal目录里的段文件,直接在操作系统层面列目录:
ls -lh $PGDATA/pg_wal你会看到一堆以十六进制命名的文件,每个默认16MB。文件名共24位,分三段:前8位是时间线ID,中间8位是日志文件ID,后8位是段文件ID。每个wal段文件内部是连续的8KB页,页面里保存的是一条条wal记录。
进一步查看wal段文件里的具体内容,可以用pg_waldump工具:
pg_waldump -p $PGDATA/pg_wal 000000010000000000000001这个命令会输出每条wal记录的类型、LSN范围、事务ID、资源管理器等信息。对于验证归档内容、排查复制断点、理解崩溃恢复路径,pg_waldump是离不开的工具。有些运维同事不常用它,但其实它比任何第三方工具都靠谱,因为这个工具和PostgreSQL的wal格式是同源维护的,不会出现格式解析偏差。
3.2 归档配置:把日志安全地备份出来
归档配置是wal实操里最常用、最需要小心的环节。先看一个完整配置示例:
wal_level = replica archive_mode = on archive_command = 'test ! -f /backup/wal/%f && cp %p /backup/wal/%f' archive_timeout = 60解释一下这个archive_command。PostgreSQL在切换一个wal段文件后,会执行该命令,此时%p代表源wal文件的完整路径,%f代表文件名。我的写法里加了一个test判断,意思是如果目标文件已存在就不重复拷贝,这个是防止重复归档时覆盖已有文件。实际线上我建议所有归档命令都不要去掉这个判断,因为重复执行archive_command是允许的,但覆盖已有归档文件可能正中恢复时的大忌。
配置完成之后,通过reload生效,然后查一下归档状态:
SELECT * FROM pg_stat_archiver;重点关注last_archived_wal和failed_count两列。前者表示最后一次成功归档的文件名,后者表示出错的归档次数。如果failed_count持续增长,说明archive_command失败,需要立刻检查归档目录权限、磁盘空间、命令本身。
对于低写入的库,auto段文件可能很久才切换一次,导致归档长时间没有动静。此时可以把archive_timeout设成60秒,意思是日志超过60秒没有写入到新段文件时,PostgreSQL会强制切换到新段文件,并触发归档。这会稍微增加一点文件切换频率,但好处是归档后的数据时延可控。pg_rman、pgBackRest这类备份工具做增量备份时也依赖及时的归档产生。
3.3 手动切换与手动检查点:什么时候用
手动切换wal段文件使用pg_switch_wal()函数:
SELECT pg_switch_wal();调用之后,当前wal段文件会立即关闭并进入归档流程,然后新建一个段文件继续写。手动切换在两种场景下很有用:一是你准备做备份或恢复实验,想把截止某个时间点的日志完整归档出来;二是排查归档问题后,想立即验证归档命令是否正常,此时手动切换一次,再观察pg_stat_archiver的变化即可。
手动触发检查点则使用:
SELECT pg_checkpoint();这个命令会立即安排一次检查点,把shared buffer里的脏页刷盘,并更新pg_control中的检查点位置。注意,在生产环境不要没事频繁去执行pg_checkpoint(),因为每次检查点都会带来一次数据文件刷盘,如果数据库load比较高,强行checkpoint会把IO打满,造成业务抖动。只有在你明确需要缩短恢复时间、或者准备安全关闭实例时,手动检查点才有必要。
归档恢复的核心机制是:基础备份(数据文件快照)+ wal归档日志(增量重放)。实际操作时,你先把数据目录恢复到基础备份时的状态,然后把归档日志按照LSN顺序连续重放,PostgreSQL就能把数据库推进到归档日志末尾对应的那个时间点。这套机制也是PITR(Point-In-Time Recovery,时间点恢复)的基础。PITR上线流程我在后面的故障排查部分也展示了索引目录和命令链条。
4. wal日志常见故障排查与实战心得
4.1 pg_wal目录爆满,第一现场怎么处理
pg_wal目录无限膨胀是最常见的wal故障,表现是磁盘使用率接近100%,数据库日志里开始报“could not write to file pg_wal/... No space left on device”。
排查思路按下述顺序逐步推进,而不是一上来就删文件。第一步先搞清楚,哪个因素在阻止wal段文件被回收。用这个SQL查看当前的wal保留机制:
SELECT slot_name, active, restart_lsn, pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS lag FROM pg_replication_slots;如果有active复制槽,而且restart_lsn落后主库当前LSN很远,那说明备库长时间没来消费wal日志,主库为了保证备库将来还追得上,只能一直保留从中断点开始的wal段文件。这时候处理方案是:排查备库状态,先把断开的复制恢复;如果那个备库已经废弃,那就把复制槽删掉,命令是:
SELECT pg_drop_replication_slot('slot_name');如果只有一个数据库节点,没有复制槽,那继续检查wal_keep_size设置,它也会额外保留一部分wal。另外,备份工具如pgBackRest、barman如果开启了归档保留功能,也可能滞后于主库消费,同样会导致wal堆积。一句话,先找“谁在按住LSN不放”,而不是盲目去删pg_wal里的文件。
4.2 归档失败,主库为何越跑越慢
归档失败不会直接影响主库的写入性能,但它会让pg_wal目录持续增长,因为PostgreSQL不会回收尚未成功归档的段文件。症状上有时候表现为:磁盘使用率缓慢上涨,pg_wal目录里文件数量每天都在增加。查定位用前面提过的pg_stat_archiver视图,看failed_count和last_archived_wal,如果last_archived_wal长时间不变,基本可以认为归档链路断了。
归档命令失败的常见原因有三类:第一,archive_command写的有问题,比如路径写错、命令不存在、权限不够;第二,归档目标存储满了;第三,归档目标是一个网络盘,网络抖动导致拷贝超时。
所以我建议,写archive_command的时候,先在操作系统中手工执行一遍,把%p和%f替换成实际文件验证逻辑。例如:
test ! -f /backup/wal/000000010000000000000001 && cp /var/lib/postgresql/16/main/pg_wal/000000010000000000000001 /backup/wal/000000010000000000000001如果手工能成功,再把它填到配置里,这样能排除一半以上的低级错误。归档目标如果是网络盘,我在脚本里会额外加一个重试逻辑,用简单的循环来保证网络抖动时归档不失败:
archive_command = 'for i in 1 2 3 4 5; do cp %p /backup/wal/%f && break; sleep 2; done'真实生产经验是,本地归档路径一般不会失败,异地备份或网络盘归档需要把超时重试考虑进去。
4.3 高可用与数据恢复场景下的wal使用技巧
在高可用方案里,Patroni是目前主流选择,大家在PostgreSQL集群搭建时经常遇到。Patroni底层依赖的仍然是wal日志、流复制和复制槽。Patroni配置中常见一个参数wal_keep_size,在管控模式下它用来自动管理pg_wal目录里的wal保留量,尽量让集群内所有节点共用一份稳定的wal,以便备库快速追上。不过需要提醒的是,在Patroni环境里修改wal相关参数不要直接在postgresql.conf里改,要通过Patroni的配置接口改,否则集群会认为你修改了数据库配置并触发重启。
另一个很实用也容易踩坑的是恢复验证。用wal归档做恢复时,如果不小心在recovery.signal文件(PG15以后默认改为recovery.signal)的加持下恢复了主库,它变成备库,然后又被提升为主库,时间线就会分叉。分叉会造成旧时间线的归档文件不再被重放,这个机制是合理的设计,但如果你去手动删旧时间线的归档,就要小心不要在恢复时误删了新时间线需要的基础wal。
验证一个归档集是否完整可用,我通常用这个方式:把归档文件复制到一个临时目录,然后用pg_waldump查看最后一组时间线和日志ID是否连续。比如:
pg_waldump -p /tmp/myarch 0000000100000000000000FF如果看到记录可以连续解析到文件末尾,至少说明归档文件没有明显的物理损坏和空洞。当然,最可靠的验证还是在一个临时实例里跑完整恢复流程。
4.4 故障速查表与经验总结
最后把我在实际运维过程中遇到过的常见场景汇总成一张速查表,方便你对照排查。
| 症状 | 可能原因 | 优先排查方向 |
|---|---|---|
| pg_wal目录持续变大,磁盘告警 | 复制槽断开、wal_keep_size过大、归档失败 | 先查pg_replication_slots,再看pg_stat_archiver,最后看wal_keep_size |
| 数据库重启恢复非常慢 | 检查点间隔过长,或恢复需要重放的wal量很大 | 查看checkpoint_timeout和max_wal_size,合理调小 |
| 归档一直失败,failed_count上涨 | archive_command路径错误、归档目录不可写、网络盘故障 | 手动执行archive_command,检查归档目录权限 |
| 主从复制延迟持续放大 | 备库I/O能力不足、wal段文件过大、网络带宽不够 | 观察备库pg_stat_replication视图,确认received_lsn与replay_lsn之间的间隔 |
| 事务提交延迟时高时低 | fsync等待、synchronous_commit设置过强、wal_buffers过小 | 观察IO等待,必要时调整synchronous_commit或wal_buffers |
| 启用逻辑复制后wal体积暴涨 | wal_level=logical,日志记录的信息变多 | 确认业务是否真的需要逻辑复制,不需要就改回replica |
| 删除复制槽后pg_wal仍不回收 | 有其它进程占用,或者归档过程仍在进行 | 再查一次pg_replication_slots,并确认归档链是否正常 |
这里面的每个问题,到最后几乎都会指向同一个核心原则:wal日志的回收不是随意的,它受制于复制槽、归档状态、wal_keep_size、max_wal_size、min_wal_size等多重因素的共同约束。排查时永远先问三个问题——谁还在消费这段日志?谁还没完成归档?谁在强制它保留?把这三个问题回答了,pg_wal膨胀的问题基本就解决了。
我在实际项目里还有一个习惯:每个星期用脚本跑一次wal健康检查,自动记录归档延迟、复制延迟、pg_wal目录体积这三项,连续观察几周后,能非常敏感地发现异常趋势。这个方法强烈建议长期维护数据库的团队也做起来。把wal相关的状态监测纳入例行巡检,比等告警响了再救火要省心得多。