
凌晨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_size8KB散落在磁盘不同位置。修改一行数据可能要随机读写一个页面如果每秒几千个事务磁盘就成了随机IO的战场延时会高到你怀疑人生。PostgreSQL把脏页积攒在shared_buffers里由bgwriter和checkpointer按批次顺序地刷盘就是为了把随机IO摊平成相对可控的负载。而WAL是顺序追加写一条记录连着一条记录往文件尾部写磁盘顺序IO比随机IO快一个数量级。所以“先写顺序日志、后刷随机数据页”这个设计本质是用顺序IO换随机IO用一条可靠的日志链路换整套高性能的缓冲机制。评价一个数据库的持久化设计好不好绕不开这个逻辑。1.3 LSN把数据页和WAL串在一起的那根线WAL记录不是无序的流水账每条记录都有编号叫LSNLog Sequence Number。它是一个8字节无符号整数单调递增好比草稿纸上的页码。你写了一条记录它就有自己的LSN指向WAL文件里的具体位置。同时每个数据页的头部也有一个字段叫pd_lsn记录这个页面最近一次被修改时对应的WAL位置。这个名字太容易让人记混了页面有自己的LSNWAL记录也有自己的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里看到一种特殊的记录某个页面被完整地写进日志里而不是只写了变化的部分。这种记录叫FPIFull Page Image整页镜像它来自full_page_writes机制。为什么要写整页考虑一个场景checkpoint之后某个数据页第一次被修改。如果此时系统崩溃磁盘上的这个页面可能处于一个“半新半旧”的混乱状态——数据库只写了页面的前半部分后半部分还是旧数据。这种情况下只凭一条“从偏移100开始改了20字节”的增量记录是无法恢复出正确页面的因为基准页本身就是残缺的。解决方案很朴素checkpoint之后第一次修改某个页面时不写增量直接把整个8KB页面原样写进WAL。这样恢复时无论页面基准状态如何只要WAL里的整页是完整的就能重建页面再继续apply后续的增量修改。代价也很明显整页数据非常占空间一次大面积更新可能能把WAL撑到好几个GB。full_page_writes默认是on绝大多数情况下别关。只有你的存储能保证8KB页面写入的原子性比如某些带电池保护的阵列、ZFS的COW机制才有资本关掉它换更小的WAL量。如果你的磁盘遇到部分写问题关了它可能导致无法恢复的损坏这个风险不值得赌。另一个思路是开wal_compressiononFPI会被压缩后再写入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_target0.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_levelreplicaWAL记录级别minimal无法归档/复制logical用于逻辑复制wal_buffers-1WAL缓冲区大小自动按shared_buffers比例计算一般几MB够用wal_sync_method平台相关提交时刷盘方式Linux通常fdatasync机械盘/网络盘差异大synchronous_commiton提交是否等待WAL落盘/备库确认off性能高但有丢数据窗口full_page_writesoncheckpoint后首改页面是否写整页无原子写保障前保持onwal_compressionoff是否压缩FPI空间紧张时开启wal_keep_size0保留最近N字节WAL越大越占盘给断连备库兜底max_slot_wal_keep_size-1限制复制槽锁定的WAL量-1不限制谨慎设置checkpoint_timeout5min强制checkpoint间隔间隔越长恢复要重放的WAL越多max_wal_size1GB触发checkpoint的软目标别设太小会频繁checkpointmin_wal_size80MBWAL预留文件池减少文件反复创建开销checkpoint_completion_target0.9刷脏页时间占比高IO环境可适当调低wal_recycleon复用旧WAL段文件一般保持默认wal_init_zeroon新建段文件预填零特殊存储才考虑关闭4.1 写入与持久化相关参数wal_sync_method直接决定事务提交时系统用哪种方式把WAL刷到磁盘。在Linux上默认通常是fdatasyncWindows上默认可能是open_datasync。这个参数对提交延迟的影响非常直观如果数据库跑在机械盘、网络存储或者虚拟化磁盘上建议实际压测下不同fsync方式的差异别只看默认值。synchronous_commitoff是很多人为了提性能会动的一个参数。事务提交时不再等待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写一点就触发checkpointcheckpoint本身要刷大量脏页会产生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_modeonarchive_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:00PG12及以上版本还需要在数据目录创建recovery.signal文件表示进入恢复模式。启动数据库后它会从备份点开始应用WAL一直重放到recovery_target_time指定的时间点然后停止。恢复完成后注意日志里recovery finishing的相关信息确认没有报错。另外要理解recovery_target_time后面有个inclusive选项默认true表示恢复操作会包含目标时间点那一刻的记录如果误操作恰好发生在10:30:00可能需要设置recovery_target_inclusiveoff恢复到该时间点之前。这类操作建议先拿测试环境演练别在生产上临时研究。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文件会陆续变成.donePostgreSQL才允许回收对应的WAL文件。这个过程中可以执行一次CHECKPOINT让redo point前移加快回收。复制槽卡住的如果对应的备库或订阅端还能救先恢复消费如果确认这个槽已经不使用了直接dropSELECT 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_commitremote_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数据安全的命脉。