凌晨2点17分,磁盘告警。我打开监控面板,看到pg_wal目录在半小时内从2GB涨到了8GB。这是相当经典的PostgreSQL运维场景——WAL日志(Write-Ahead Logging,预写日志)在PG里既是数据安全的基石,也是日常排错中最容易让人头疼的东西。很多人对WAL的理解停留在“事务日志”四个字,可真要回答它为什么会产生、为什么会让目录膨胀、checkpoint和它有什么关系、LSN到底是什么,又往往说不清楚。
这篇文章我想一次性把WAL讲透:先讲它存在的理由,再拆开它从产生到落盘的完整路径,然后是WAL文件回收与膨胀的机制,接着给出一份常用参数的速查表,最后走一遍真实排错链路,看看“pg_wal快满了怎么办”。无论你是刚接触PG的开发,还是被日志问题困扰过的DBA,按这条线走下来,再看WAL相关内容应该会顺很多。
1. WAL的本质:为什么PostgreSQL非要“先写日志再改数据”
1.1 用账房先生的方式理解预写日志
想象一个老式账房:白天所有进出账都先记在草稿本上,晚上再誊写到正式账本里。如果打烊时突然停电,只要草稿本还在,第二天就能根据草稿把账补全,不会错漏。WAL干的就是这个活。
PostgreSQL里,正式账本叫数据文件,草稿本叫WAL。用户执行UPDATE、DELETE、INSERT时,数据页先被加载到shared_buffers这块共享内存里修改,不会立刻写进磁盘上的数据文件。这个“内存改完、磁盘没改”的页面就是脏页。脏页什么时候落盘,由后台进程统一调度。
问题来了:如果脏页还没来得及刷盘,数据库进程崩溃或者主机断电了,内存里的修改就没了。怎么保证不丢数据?答案就是WAL——数据页可以先不写,但“我刚才改了哪个页面、改了什么内容”必须已经落到磁盘上。等数据库重启,从WAL里把修改重放一遍,数据就回来了。
这就是“预写”二字的含义:WAL必须先于数据页落盘,才能作为崩溃恢复的依据。事务提交成功的那一刻,系统保证的是“WAL写成功了”,而不是“数据页写成功了”。
1.2 为什么不能每次都直接把数据页刷盘
你可能想问:既然这么麻烦,干脆每次修改都直接写数据文件不行吗?不是不行,是代价太大。
数据文件是8KB一个页面(默认block_size=8KB),散落在磁盘不同位置。修改一行数据,可能要随机读写一个页面;如果每秒几千个事务,磁盘就成了随机IO的战场,延时会高到你怀疑人生。PostgreSQL把脏页积攒在shared_buffers里,由bgwriter和checkpointer按批次顺序地刷盘,就是为了把随机IO摊平成相对可控的负载。
而WAL是顺序追加写,一条记录连着一条记录往文件尾部写,磁盘顺序IO比随机IO快一个数量级。所以“先写顺序日志、后刷随机数据页”这个设计,本质是用顺序IO换随机IO,用一条可靠的日志链路换整套高性能的缓冲机制。评价一个数据库的持久化设计好不好,绕不开这个逻辑。
1.3 LSN:把数据页和WAL串在一起的那根线
WAL记录不是无序的流水账,每条记录都有编号,叫LSN(Log Sequence Number)。它是一个8字节无符号整数,单调递增,好比草稿纸上的页码。你写了一条记录,它就有自己的LSN,指向WAL文件里的具体位置。
同时,每个数据页的头部也有一个字段叫pd_lsn,记录这个页面最近一次被修改时对应的WAL位置。这个名字太容易让人记混了:页面有自己的LSN,WAL记录也有自己的LSN,它们之间怎么配合?
崩溃恢复时,PostgreSQL从最近一次checkpoint的位置往后读WAL。读到一条WAL记录,发现它说要修改某个页面,就去看看那个页面的pd_lsn。如果页面的pd_lsn比这条WAL记录的LSN还大,说明这个修改已经包含在页面里了,跳过;如果页面的pd_lsn比记录的LSN小,说明这个修改还没刷进页面,重放。这个比较逻辑贯穿整个恢复过程,也是理解WAL最核心的一个点。
2. 一条WAL记录的完整生命周期:从LSN分配、WAL buffer到日志段文件
2.1 从UPDATE到WAL落盘的完整路径
一条普通的UPDATE语句,在数据库内部走过的路径比你想象的长。
先看目标行所在的数据页在不在shared_buffers里,不在就从磁盘读进来。然后在内存里修改该页面,同时生成一条WAL记录,记录的内容是“哪个数据块、从哪个偏移、发生了什么类型的修改、修改后的数据长什么样”。数据库用资源管理器(rmgr)来区分不同类型的WAL记录,比如Heap表示堆表操作,Btree表示索引操作,Transaction表示事务提交或回滚。
生成的WAL记录会被追加到WAL buffer里,这是shared memory里的一块缓冲区。每条记录分配一个LSN,记录之间的先后顺序通过指针串联起来。WAL buffer里的数据不是立刻刷盘的,它等几个时机:
- 第一个时机是事务提交。synchronous_commit参数为on时,提交必须等到WAL记录真正fsync到磁盘才返回成功。
- 第二个时机是walwriter后台进程周期性刷盘,默认每200ms左右刷一次(wal_writer_delay参数控制)。
- 第三个时机是WAL buffer满了。缓冲区就那么几MB,写满了自然要往磁盘倒。
这里有个很容易被忽略的设计:多个事务几乎同时提交时,第一个事务触发一次fsync,后面的事务可以“搭车”,把自己的WAL记录和前面的一起刷出去,减少fsync次数。这就是组提交(Group Commit)。PostgreSQL现代版本已经内置了这个逻辑,所以大部分场景下不需要像早期版本那样手动调commit_delay和commit_siblings。
2.2 WAL段文件的命名规则与pg_wal目录
WAL最终落在文件里,每个文件默认16MB,这个大小由wal_segment_size参数决定,只能在initdb初始化集群时指定,范围是1MB到1GB,且必须是2的幂。很多人启动数据库的时候没注意这个参数,等跑了一两年,想改WAL段大小,发现改不了,只能重建实例。这是初始化时就要想清楚的事情。
WAL文件命名是24位十六进制字符串,比如000000010000000000000001。拆开看,前8位是时间线ID,中间8位是逻辑日志ID,后8位是段ID。文件名和LSN之间有确定性的换算关系,但日常维护不需要手算,PostgreSQL提供了pg_walfile_name()函数帮你转换。
WAL文件存放在数据目录下的pg_wal子目录里。PG10之前这个目录叫pg_xlog,后来改名叫pg_wal,因为“xlog”这个叫法很容易和WAL记录本身混淆。目录旁边还有个archive_status子目录,存放归档状态标记文件。生产环境里,有人会把pg_wal目录单独软链接到独立磁盘上,这样WAL顺序写入不会和数据文件的随机读写争抢IO,是个很实用的部署技巧。
另外两个参数wal_init_zero和wal_recycle会决定WAL段文件的创建方式。wal_recycle默认on,意思是过期的WAL文件不直接删除,而是重命名成未来要用的段号,减少反复创建文件的开销。wal_init_zero默认on,表示新建的WAL段文件先填零,避免真正写入时磁盘临时分配块产生延迟。这两个参数绝大多数环境保持默认就行,只有在存储写零行为异常的特殊设备上才需要考虑关闭。
2.3 full_page_writes:日志里为何经常出现整页数据
你可能会在WAL里看到一种特殊的记录:某个页面被完整地写进日志里,而不是只写了变化的部分。这种记录叫FPI(Full Page Image,整页镜像),它来自full_page_writes机制。
为什么要写整页?考虑一个场景:checkpoint之后,某个数据页第一次被修改。如果此时系统崩溃,磁盘上的这个页面可能处于一个“半新半旧”的混乱状态——数据库只写了页面的前半部分,后半部分还是旧数据。这种情况下,只凭一条“从偏移100开始改了20字节”的增量记录,是无法恢复出正确页面的,因为基准页本身就是残缺的。
解决方案很朴素:checkpoint之后,第一次修改某个页面时,不写增量,直接把整个8KB页面原样写进WAL。这样恢复时无论页面基准状态如何,只要WAL里的整页是完整的,就能重建页面,再继续apply后续的增量修改。代价也很明显:整页数据非常占空间,一次大面积更新可能能把WAL撑到好几个GB。
full_page_writes默认是on,绝大多数情况下别关。只有你的存储能保证8KB页面写入的原子性,比如某些带电池保护的阵列、ZFS的COW机制,才有资本关掉它换更小的WAL量。如果你的磁盘遇到部分写问题,关了它可能导致无法恢复的损坏,这个风险不值得赌。
另一个思路是开wal_compression=on,FPI会被压缩后再写入WAL。PG15之前默认使用zlib压缩,PG15之后如果编译时启用了lz4或zstd,可以指定更快或压缩率更高的算法。压缩会消耗一点CPU,但能明显减少WAL落盘量,在IO瓶颈的场景里通常很划算。
3. checkpoint与WAL回收:日志目录为什么会膨胀,又如何瘦身
3.1 checkpoint到底做了什么
checkpoint不是一个可以忽略的日常概念,它是WAL回收的起点。执行checkpoint时,PostgreSQL会把shared_buffers里的所有脏页刷到磁盘,然后在控制文件里记录一条redo point。崩溃恢复时,只需要从redo point开始重放WAL,因为redo point之前的修改都已经落盘了。
触发checkpoint有几个途径:checkpoint_timeout到了(默认5分钟),或者当前WAL总大小接近max_wal_size(默认1GB),或者数据库正常关闭,或者手动执行CHECKPOINT命令。后台有一个checkpointer专用进程负责这件事。
checkpoint不是瞬间完成的,默认它会用两个checkpoint间隔内大约90%的时间把脏页刷完,这就是checkpoint_completion_target=0.9的含义。这样做的目的很简单:把刷脏页的IO压力摊平,避免checkpoint开始时一瞬间把所有IO抢光,影响正常业务请求。
3.2 WAL文件的回收条件:不是checkpoint完就能删
很多人以为checkpoint一执行,旧的WAL文件就能删了。实际没那么简单。一个WAL文件要被回收或复用,必须同时满足几个条件:
第一,它对应的LSN位置已经早于redo point,也就是说它包含的所有修改都已经反映在数据文件里。第二,如果开启了归档模式,这个文件必须已经被成功归档,目录里有对应的.done标记。第三,它不能被任何一个复制槽锁定,不能落在任何活跃复制槽的restart_lsn之后。第四,它不能在wal_keep_size参数保留的范围内。
只有这些条件全部满足,PostgreSQL才会把文件删除,或者重命名成未来要用的段号。所以你会看到,即使checkpoint正常执行,pg_wal目录还是可能堆积,问题多半出在归档失败或者复制槽没释放这两个环节上。
max_wal_size和min_wal_size这两个参数也容易被误读。max_wal_size不是一个硬上限,它更像一个软目标:WAL总量达到这个值就触发一次checkpoint,但checkpoint期间WAL还在继续写,超过1GB是正常的。min_wal_size则用来维护一个WAL文件池,让文件不至于被频繁地创建和删除,默认80MB,一般不用动。
3.3 复制槽是如何“扣住”WAL的
复制槽(Replication Slot)是WAL回收路径上最容易被忽略的“手刹”。每个复制槽都会记录一个restart_lsn,代表消费端至少需要保留到哪个位置。只要某个WAL文件里的内容在这个位置之后,主库就必须无条件保留它,不管它是不是已经checkpoint过、是不是已经归档过。
典型场景:主库挂了几个物理备库,其中一个备库停机维护了一周。主库的pg_wal就攒了一周的日志,因为你不能删,删了备库回来就接不上了。再比如逻辑复制订阅端因为网络问题停止消费,发布端的复制槽也会一直往后锁住WAL。
排查时看pg_replication_slots视图,如果restart_lsn和pg_current_wal_lsn()之间的差距以GB计,基本就是复制槽卡住了。应对方法要么是让消费端尽快跟上,要么确认槽确实不需要了,直接drop掉。wal_keep_size是另一种保围机制,它不看消费位点,而是简单粗暴地保留最近N字节的WAL,默认0。如果同时配了复制槽和wal_keep_size,实际保留量会取两者中更保守的那个,也就是保留更多WAL。
4. 常用WAL参数速查:默认值、含义与调优边界
WAL相关参数很多,我把最容易碰到、也最值得理解的参数整理成了表格,先看全貌,再逐个讲边界。
| 参数 | 默认值 | 作用 | 调优备注 |
|---|---|---|---|
| wal_level | replica | WAL记录级别 | minimal无法归档/复制;logical用于逻辑复制 |
| wal_buffers | -1 | WAL缓冲区大小 | 自动按shared_buffers比例计算,一般几MB够用 |
| wal_sync_method | 平台相关 | 提交时刷盘方式 | Linux通常fdatasync,机械盘/网络盘差异大 |
| synchronous_commit | on | 提交是否等待WAL落盘/备库确认 | off性能高但有丢数据窗口 |
| full_page_writes | on | checkpoint后首改页面是否写整页 | 无原子写保障前保持on |
| wal_compression | off | 是否压缩FPI | 空间紧张时开启 |
| wal_keep_size | 0 | 保留最近N字节WAL | 越大越占盘,给断连备库兜底 |
| max_slot_wal_keep_size | -1 | 限制复制槽锁定的WAL量 | -1不限制,谨慎设置 |
| checkpoint_timeout | 5min | 强制checkpoint间隔 | 间隔越长,恢复要重放的WAL越多 |
| max_wal_size | 1GB | 触发checkpoint的软目标 | 别设太小,会频繁checkpoint |
| min_wal_size | 80MB | WAL预留文件池 | 减少文件反复创建开销 |
| checkpoint_completion_target | 0.9 | 刷脏页时间占比 | 高IO环境可适当调低 |
| wal_recycle | on | 复用旧WAL段文件 | 一般保持默认 |
| wal_init_zero | on | 新建段文件预填零 | 特殊存储才考虑关闭 |
4.1 写入与持久化相关参数
wal_sync_method直接决定事务提交时,系统用哪种方式把WAL刷到磁盘。在Linux上默认通常是fdatasync,Windows上默认可能是open_datasync。这个参数对提交延迟的影响非常直观,如果数据库跑在机械盘、网络存储或者虚拟化磁盘上,建议实际压测下不同fsync方式的差异,别只看默认值。
synchronous_commit=off是很多人为了提性能会动的一个参数。事务提交时不再等待WAL刷盘,而是由walwriter后台周期性地把WAL刷下去。代价是:如果数据库崩溃,最近一小段时间内已经向客户端返回“提交成功”的事务可能丢失,丢失窗口取决于wal_writer_delay和系统负载。这个参数适合对数据丢失容忍度高的场景,比如某些分析型导入、允许丢少量数据的采集系统。核心业务系统,尤其涉及资金、订单的,保持on。
wal_buffers是另一个容易误升的参数。它默认是-1,表示按shared_buffers的比例自动计算。很多人觉得WAL buffer越大越好,实际并非如此。WAL buffer只是暂存区,真正决定性能瓶颈的是提交时fsync的磁盘速度。buffer调得再大,fsync还是那一次。一般默认的几MB到十几MB完全够用,bloat这个参数不会有收益。
4.2 复制与保留相关参数
max_wal_senders控制允许同时存在多少个WAL发送进程,主从复制和pg_basebackup都会占用。只跑单机,这个参数可以很小;有多个备库和逻辑复制环境时,数一下需要几个发送进程,留点余量就行。
wal_keep_size和复制槽的关系要澄清一下。它俩都用于确保断连的备库回来后还能补上缺失的WAL,但机制不同:wal_keep_size按字节数保围,不看谁在消费;复制槽按消费位置保围,精确但会一直锁住。如果你用wal_keep_size保护备库,它不产生复制槽那类“永远扣住”的问题,但它无法精确感知备库实际消费到哪,只能靠“最近N字节”粗暴地兜底。
max_slot_wal_keep_size是给复制槽加锁的上限。某个slot消费停滞太久,如果它锁定的WAL量超过这个值,该复制槽会被标记为无效,对应的备库或订阅端后续会断流。这个参数默认-1表示不限制,一旦设置,需要想清楚断流风险可不可接受。
4.3 checkpoint与段文件参数
max_wal_size和checkpoint_timeout是联动的关系。max_wal_size设太小,WAL写一点就触发checkpoint,checkpoint本身要刷大量脏页,会产生IO高峰,还可能加剧full_page_writes——因为每次checkpoint结束后,后续第一次修改的页面又要写整页。设太大,checkpoint间隔拉长,崩溃恢复时重放WAL的耗时也会变长。生产环境建议观察两个指标:pg_stat_bgwriter里的checkpoints_timed和checkpoints_req,如果req远大于timed,说明max_wal_size太紧,可以适当调大。
checkpoint_completion_target默认0.9,意思是把刷脏页的动作尽量铺开到下一个checkpoint之前90%的时间里。如果磁盘在checkpoint期间还是顶不住IO压力,可以试着调低到0.7或者0.8,代价是刷脏页的窗口变短,单次IO强度可能更集中。这个参数没有绝对最优值,结合监控看效果。
5. 实操:查看WAL状态、解析日志内容、完成时间点恢复
5.1 用SQL直接查看当前WAL位置
先来几个最常用的SQL,日常判断WAL状态会经常用到。
-- 当前WAL已flush的位置,和当前插入但可能未flush的位置 SELECT pg_current_wal_lsn(), pg_current_wal_insert_lsn(); -- 把LSN映射成具体的WAL文件名 SELECT pg_walfile_name(pg_current_wal_lsn());pg_current_wal_lsn()返回的是已经刷到磁盘的WAL位置,pg_current_wal_insert_lsn()返回的是写入WAL buffer的最新位置。两者通常很接近,如果差距持续拉大,说明walwriter刷盘跟不上写入速度,磁盘IO可能存在瓶颈。
想要直观看到WAL写入量,还可以查询pg_stat_wal视图:
SELECT * FROM pg_stat_wal;这里能看到wal_write、wal_sync、wal_bytes等累计统计,适合做一段时间的增量对比。注意pg_stat_wal是PG14才引入的视图,老版本需要通过pg_stat_bgwriter看部分统计。
5.2 手动切换日志段与观察归档状态
有时候你想测试归档配置、或者想确认某个WAL文件已经被归档,可以手动触发日志段切换:
SELECT pg_switch_wal();这条语句会结束当前WAL段的写入,让PostgreSQL开始使用新的段文件。之后,旧段文件会进入归档状态,你可以在pg_wal/archive_status目录里看到它的状态标记。文件名后缀.ready表示等待归档,.done表示归档已完成。
archive_command的写法有个经典注意事项。推荐写成这样:
archive_command = 'test ! -f /backup/archive/%f && cp %p /backup/archive/%f'先用test判断目标文件是否已存在,不存在才拷贝。这样做的好处是,即使PostgreSQL因为超时等原因重试归档命令,也不会覆盖掉已经归档成功的文件。归档命令有个硬性要求:成功后必须返回0,任何非0返回码都会被当成归档失败,PostgreSQL会每秒重试一次,同时不断往日志里写错误信息。所以,归档命令本身一定要留好日志输出,否则排错时抓瞎。
5.3 pg_waldump:读懂WAL里到底写了什么
WAL文件是二进制格式,直接看会乱码。PostgreSQL提供了pg_waldump工具,可以按记录逐行解析WAL内容。
pg_waldump -p $PGDATA/pg_wal 000000010000000000000001输出大概长这样:
rmgr: Heap len (rec/tot): 61/ 61, tx: 503, lsn: 0/170000D8, prev 0/170000A0, desc: insert off 13 flags 0x00, blkref #0: rel 1663/16384/1259 blk 0 FPW解释一下各字段:rmgr表示这条记录属于哪个资源管理器,比如Heap是堆表操作、Btree是索引操作、Transaction是事务控制。lsn是这条记录的位置。tx是事务ID。desc描述操作类型和细节,比如insert表示插入,off 13表示插入到页面的第13个槽位。blkref后面的FPW表示这条记录带了整页镜像。
当你怀疑某个时间窗口内WAL异常膨胀,用pg_waldump看那个时段的文件,很快就能定位是大量FPI、还是某个大事务持续在写某个表。这个工具基本是只读解析,不会对数据库产生额外负载,排错时可以放心用。唯一要注意的是权限,操作系统用户需要能读取WAL文件。
5.4 基于WAL的时间点恢复操作实例
时间点恢复(PITR)是WAL最典型的应用场景。我在测试环境完整走一遍,步骤并不复杂,但每一步都有坑。
第一步,确保归档已经开启且正常工作,archive_mode=on,archive_command配置好。第二步,用pg_basebackup做一份全量备份:
pg_basebackup -h 127.0.0.1 -p 5432 -U replicator -D /backup/base -Ft -z -P第三步,模拟一次误操作,比如删除了一张重要表。然后恢复:把全量备份解压到新实例的数据目录,编辑postgresql.conf,写上恢复目标:
restore_command = 'cp /backup/archive/%f %p' recovery_target_time = '2024-06-01 10:30:00'PG12及以上版本,还需要在数据目录创建recovery.signal文件,表示进入恢复模式。启动数据库后,它会从备份点开始应用WAL,一直重放到recovery_target_time指定的时间点,然后停止。
恢复完成后注意日志里recovery finishing的相关信息,确认没有报错。另外要理解recovery_target_time后面有个inclusive选项,默认true,表示恢复操作会包含目标时间点那一刻的记录;如果误操作恰好发生在10:30:00,可能需要设置recovery_target_inclusive=off,恢复到该时间点之前。这类操作建议先拿测试环境演练,别在生产上临时研究。
5.5 一个高危提醒:永远不要手动删除pg_wal文件
这是我最想强调的一条。看到pg_wal目录很大,千万不要直接rm。WAL文件是崩溃恢复的命根子,你删掉某个文件时,数据库可能正好需要从这个文件开始恢复,删了之后实例直接起不来,那时候只能跑pg_resetwal,而pg_resetwal会丢弃恢复点之前的日志,造成的后果可能比磁盘满还要严重得多。
正确处理思路永远是先找根因,让PostgreSQL自己回收文件。目录膨胀的原因逃不出第3章说的那几类:没有及时checkpoint、归档失败、复制槽锁住、wal_keep_size设置过大。把原因解决掉,执行一次CHECKPOINT,等下一轮回收机制运转起来,目录会自然瘦下来。
6. 实战排错:pg_wal目录暴涨到磁盘写满的完整排查链路
6.1 症状确认:先搞清楚是“涨得快”还是“堆得久”
接到磁盘告警,先做三件事,把现状摸清楚。
df -h du -sh $PGDATA/pg_wal ls $PGDATA/pg_wal | wc -l第一件事是确认剩余空间还有多少。第二件事确认pg_wal目录当前占多大。第三件事看文件数量,如果文件数量在几百个以上,说明已经积压了至少几GB。
然后看趋势。如果pg_wal目录半小时前还只有2GB,现在8GB,这是急性增长,多半有大批量写入或大事务正在执行。如果这个目录好几天都稳定在某个高位,那是慢性堆积,多半是归档或复制槽的问题。两种情况的排查方向不同,先区分清楚,不要一上来就删文件。
6.2 按影响面逐项排查:checkpoint、复制槽、归档
确认症状之后,我习惯按这个顺序逐项排查。
先看checkpoint是否正常:
SELECT checkpoints_timed, checkpoints_req, buffers_checkpoint FROM pg_stat_bgwriter;checkpoints_timed表示由定时触发的次数,checkpoints_req表示因为达到max_wal_size等条件而触发的次数。如果req远大于timed,说明checkpoint被频繁请求,这不是WAL堆积的原因,但能反映写入压力大。
再看复制槽卡没卡住:
SELECT slot_name, slot_type, active, restart_lsn, pg_walfile_name(restart_lsn) AS restart_wal FROM pg_replication_slots;把restart_lsn和pg_current_wal_lsn()对比,差得越远,锁住的WAL越多。曾经遇到一个故障,逻辑复制订阅端挂了两周,发布端复制槽的restart_lsn纹丝不动,pg_wal里从两周前到现在的文件全被锁着,目录直接堆到80GB。这种问题用SQL一眼就能看出来。
接着看归档是否失败:
ls $PGDATA/pg_wal/archive_status | grep ready | wc -l如果.ready文件数量很大,说明堆积的WAL还没有归档成功。然后去数据库日志里看archive_command报了什么错——权限不够、目标目录不存在、磁盘满了,这些都是常见原因。
最后看有没有大事务正在跑。pg_stat_activity里查一下长事务,尤其是长时间未提交的写事务。这里有个容易混淆的点:长事务主要影响vacuum和表膨胀,不直接影响WAL回收;但如果它持续产生大量写入,WAL增长是必然的。
6.3 处置动作与回收验证
定位到根因之后,处置动作要分场景。
归档失败的,先修复归档命令或目标存储,确认archive_command能手动跑通。归档恢复后,积压的.ready文件会陆续变成.done,PostgreSQL才允许回收对应的WAL文件。这个过程中可以执行一次CHECKPOINT,让redo point前移,加快回收。
复制槽卡住的,如果对应的备库或订阅端还能救,先恢复消费;如果确认这个槽已经不使用了,直接drop:
SELECT pg_drop_replication_slot('slot_name');也可以在不drop的情况下把slot推进到当前位置,PG10.1及以上支持:
SELECT pg_replication_slot_advance('slot_name', pg_current_wal_lsn());注意,推进slot意味着你告诉系统“这个消费端不再需要旧位置的WAL了”,如果消费端确实已经重建或不再使用,这个操作没问题;否则就是人为制造断流。
处置完之后,过一段时间再看:
du -sh $PGDATA/pg_wal正常情况下目录会慢慢收缩到正常水位。如果只是checkpoint完还没轮到后台回收,几个文件不降是正常的,给点时间。
6.4 复盘:监控与演练比应急更重要
每次处理完这类故障,我都会复盘监控体系哪里漏了。现在维护的每个PostgreSQL实例,至少会盯四个WAL相关指标:pg_wal目录总大小、archive_status里.ready文件数量、复制槽restart_lsn与当前LSN的差距、checkpoint频率变化。告警阈值设在磁盘使用率到80%就要响,别等满了再处理。
归档目标存储和主库数据盘要分开。好多故障的本质是归档目录和主库共享一块盘,归档写不进去,反过来把主库的WAL也堵住了。物理隔离能砍掉一大半连锁故障。
另外,每季度至少做一次恢复演练。用一个测试实例,真的把备份恢复到某个时间点,这一步能提前暴露归档命令错误、权限问题、恢复参数不对等一堆平时不会显现的坑。真到生产故障那天,演练过和没演练过,心态和效率完全是两回事。
7. WAL在主从复制与备份恢复中的角色:从理解到用好
7.1 主从流复制里的WAL传递路径
主从复制的底层载体就是WAL。主库的walsender进程把产生的WAL记录实时发送给备库,备库的walreceiver进程接收后写入本地pg_wal,再由startup进程重放。备库的replay_lsn表示已经应用到的WAL位置,flush_lsn表示已经刷盘的WAL位置。
你配置同步复制时,本质上是在约定“主库事务提交成功需要备库的WAL确认到哪一步”。synchronous_commit=remote_write表示备库把WAL写到操作系统缓存就算确认,remote_apply表示备库已经应用完才算确认。确认越靠后,数据安全级别越高,但提交延迟也越高。理解了WAL在备库上的流转顺序,再去看pg_stat_replication视图里的sent_lsn、write_lsn、flush_lsn、replay_lsn,就不会一头雾水。
所以在搭建主从环境时,复制槽要不要建、wal_keep_size设多少,都要提前想清楚。不建槽,备库长时间断连后主库可能把备库没收到的WAL回收掉,备库回来就断流。建了槽,备库挂了太久,主库pg_wal又会一路涨。没有银弹,靠的是监控和运维规范。
7.2 WAL归档与PITR:没有归档就没有“后悔药”
归档是把WAL从主库持续拷贝到安全位置的机制。只有全量备份没有归档,数据只能恢复到备份完成的那一刻,备份之后的事务全部丢失。有了归档,你拥有一份“从备份时间点开始、连续不断的增量日志”,理论上可以恢复到归档范围内的任意时间点。
PITR的三个要素缺一不可:一份全量备份、从备份点到目标时间的连续归档、时间线管理。其中时间线是一条很容易踩的线:每次PITR恢复会生成新的时间线,避免旧归档被错误重放。比如你恢复到了一个上午10点的时间点,之后想再恢复到中午12点,需要基于原始时间线的归档重新开始,而不是基于刚恢复出来的这个分支。
归档命令看起来简单,实际运营中问题最多。路径写错、权限不对、目标盘满、命令返回码错误,都会导致归档失败。强烈建议把archive_command写成一个脚本,脚本里带上日志记录和返回值处理,别把复杂的逻辑直接写在postgresql.conf里。
7.3 逻辑复制与WAL解码:WAL不只是物理日志
WAL里存的本质是物理层面的页面变更,但PostgreSQL也允许从WAL中解码出行级的逻辑操作,这就是逻辑复制的数据来源。wal_level设置为logical时,WAL会额外携带逻辑解码所需的信息,发布端的walsender负责把WAL解码成insert/update/delete操作,发送给订阅端。
很多人在逻辑复制出问题时的第一反应是去看订阅端状态,但根因往往在发布端的WAL和复制槽上。订阅端消费停滞,发布端复制槽一样会扣住WAL,和物理备库断线没有本质区别。所以排查逻辑复制慢或停的时候,先看发布端pg_replication_slots里的restart_lsn,再去看订阅端apply worker的状态,顺序不要反。
回到最开始那个问题:WAL为什么值得花这么多篇幅去理解?因为它贯穿了PostgreSQL的几乎所有关键机制。备份恢复靠它,主从复制靠它,逻辑复制靠它,崩溃恢复靠它。你花一个下午把这套链路理顺,再看任何PostgreSQL高可用文档,会发现所有模块都指向同一个核心概念。检查过我负责的实例之后,有一个经验越来越明确:十个WAL相关的故障,九个都能回溯到“谁还指着这段日志没有放”这个问题上。在你准备动pg_wal目录里的任何文件之前,先问自己三个问题:它已经过了checkpoint吗?归档成功了吗?有复制槽还在消费它吗?三个问题都能答“是”,这个文件才谈得上释放;答不上来的时候,先诊断再动手。
最后给你一个日常习惯:每个季度至少做一次完整的恢复演练,在测试环境里真的把备份恢复到某个时间点。平时不做,真出故障时哪怕文档在手边也容易慌。WAL这根链条平时看着不起眼,它承载的却是PostgreSQL数据安全的命脉。