ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

PostgreSQL WAL格式演进:从9.5到18的wal_compression与性能优化

PostgreSQL WAL格式演进:从9.5到18的wal_compression与性能优化 1. 项目概述PostgreSQL 9.5 的 WAL 格式改了什么1.1 核心需求解析PostgreSQL 9.5 在 2016 年 1 月发布回过头看这版本对 WALWrite-Ahead Logging预写日志体系的改动是后来这几年所有性能优化和复制功能的地基。这个项目标题之所以值得拆是因为它把两个东西绑在了一起一是 WAL 记录格式本身的结构调整二是wal_compression这个参数的引入。这两个变化看起来不那么显眼但实际影响范围非常大——从 9.5 开始所有做物理复制、PITR时间点恢复、以及在高写入负载下做性能调优的人都会直接或间接被这两个改动影响。标题里还提到9.5→18 演进这其实是更值得深挖的部分。wal_compression从 9.5 引入到现在默认值变过9.5 到 13 都是 off14 开始默认 on可选算法从单一 pglz 扩展到了 lz4、zstdWAL 的头部结构和块引用格式也一直在调整。到 18 这个版本也就是 2025 年发布的 PostgreSQL 18如果你去翻源码XLogRecordBlockHeader结构已经和 9.5 时代有了明显的差异。把这条线串起来看才能理解 PostgreSQL 社区对 WAL 设计的长期思路。这篇内容适合三类人一是正在做 PostgreSQL 内核源码阅读或者二次开发的人二是负责生产环境 PostgreSQL 运维、需要评估升级和参数调整的 DBA三是对数据库存储引擎和日志系统设计感兴趣的后端工程师。我不会把每个结构体字段都贴出来当字典念而是把关键的设计逻辑和实际影响讲清楚配合源码位置和实操验证方法让你看完能自己动手去确认。1.2 为什么 9.5 是个关键版本先说结论9.5 的 WAL 格式调整核心目的有三个——减少 WAL 体积、提高备份恢复效率、为后续的复制功能腾出空间。在 9.4 及更早的版本里一条 WAL 记录的结构是这样的先是通用的XLogRecord头部24 字节固定部分包含 xl_prev、xl_xid、xl_tot_len、xl_info、xl_rmid 等字段紧接着是XLogRecordBlockHeader数组最后才是真正的主数据main data和每个 block 的数据。问题出在XLogRecordBlockHeader上在 9.4 及之前每个被引用的 block 都要带一个 12 字节的 header而且这个 header 里有一个next_len字段用来记录下一个 block header 的偏移量。这意味着什么如果你的一条 WAL 记录里带了 4 个 block 的变更就要写 4 个 12 字节的 header还要额外存 4 个next_len偏移量来串成链表。解析这条记录的时候你得沿着这个链表一个个跳逻辑上绕空间上也有浪费。9.5 把XLogRecordBlockHeader重构成了更紧凑的格式并且把 block 的数量直接写进了长页面long page的头部也就是XLogRecord后面的那个区域。具体来说9.5 引入了XLogRecordBlockCompressHeader、调整了XLogRecordBlockHeader的字段布局还把原来分散的next_len链表彻底干掉了改成了基于num_block_ids的紧凑数组结构。这个改动的直接收益是对于含多个 block 引用的 WAL 记录头部开销显著下降WAL 写入量变小同时解析逻辑更简单CPU 开销更低。而wal_compression参数就是在这个背景下被一起塞进 9.5 的。它的作用很直接当full_page_writes开启默认就是开启并且某个 page 是第一次被修改checkpoint 后第一次时PostgreSQL 会把整个 page 写入 WAL这就是所谓 full page imageFPI。一个 page 默认是 8KB如果这个页面是稀疏的比如大量空页、或者页面上只有少量 tuple直接写入 WAL 就是浪费。wal_compressionon之后这个 FPI 会先被 pglz 算法压缩再写入压缩后的块可以小到几百字节效果非常明显。2. 源码级解析WAL 新格式与 wal_compression 的实现细节2.1 XLOG 长页面头部与块引用数组先说源码位置。wal_compression和 WAL 记录格式相关的核心代码主要集中在src/backend/access/transam/xloginsert.c和src/backend/access/transam/xlogrecord.c这两个文件里。如果你用的是 9.5 的源码打开xloginsert.c会看到一个XLogRecordBlockHeader的宏定义和XLogRecordAssemble函数这个函数就是负责把事务修改的 buffer 信息组装成最终的 WAL 记录。9.5 里XLogRecordBlockHeader结构体定义在src/include/access/xlogrecord.h长这样typedef struct XLogRecordBlockHeader { uint8 id; /* block reference ID */ uint8 fork_flags; /* fork number and flags */ uint16 data_length; /* number of payload bytes (not including header) */ /* If BKPBLOCK_HAS_IMAGE, an XLogRecordBlockImageHeader follows */ /* If BKPBLOCK_SAME_REL is not set, a RelFileNode follows */ /* Block number follows */ } XLogRecordBlockHeader;这里有个关键变化9.4 及之前每个 block header 里有一个next_len字段uint16用来指向下一个 block header 相对当前 header 的偏移。9.5 直接把这个字段删了改成在长页面的 XLogRecord 头部区域存一个num_block_ids字段一个字节然后所有 block header 和对应数据按顺序紧凑排列。解析器拿到num_block_ids之后按顺序遍历就行不需要跳转。再看XLogRecordBlockImageHeader这是 9.5 新增的结构体专门用来描述 FPIfull page image信息typedef struct XLogRecordBlockImageHeader { uint16 length; /* length of the image ( uncompressed ) */ uint16 hoi_mask; /* mask of the hole, if any */ uint8 extra_hoi_mask; uint8 extra_length; } XLogRecordBlockImageHeader;wal_compression的实现就和这个结构体有关。当压缩开启时FPI 数据不是原始 8KB而是压缩后的字节流length字段记录的是原始未压缩长度压缩后的实际长度则通过extra_length或者 block header 的data_length来传递。解压的时候PG 根据这些字段还原出完整的原始 page 镜像。这里有一个容易踩的坑压缩后的 FPI 在写入 WAL 时data_length存的是压缩后的数据长度而XLogRecordBlockImageHeader.length存的是原始长度。如果你在写工具解析 WAL比如自己写 walminer 这类工具这个区别一定要搞对否则解压逻辑会错乱。2.2 wal_compression 的源码实现路径wal_compression的 GUCGrand Unified Configuration参数定义在xlog.c里9.5 版本只有on/off两个取值对应的是 pglz 压缩算法。到了 15 版本这个参数扩展成了on/off/0/1/2/3的形式其中 2 对应 lz43 对应 zstd这是后话后面详细讲。核心实现路径在xloginsert.c的XLogRecordAssemble函数里。大致流程是遍历所有被修改的 bufferregistered_buffers逐个判断是否需要写 FPI。判断逻辑是如果当前 LSN 小于该 page 的pd_lsn也就是 checkpoint 之后第一次修改并且full_page_writeson就生成 FPI。生成 FPI 时先调用PageGetLSN拿到当前页面的 LSN然后调用page_compress相关函数。在 9.5 里这个函数叫pglz_compress封装在src/common/pg_lzcompress.c在 15 里改成了lz4_compress/zstd_compress等多路分支。压缩后的结果如果比原始页小一般都会小但某些极端情况可能不压反大就采用压缩存储在XLogRecordBlockImageHeader里设置BKPBLOCK_HAS_COMPRESSED_IMAGE标志位如果压缩后反而更大就回退到原始存储不设压缩标志。把组装好的 WAL 记录写入 WAL buffer然后调用XLogFlush刷盘。这个过程中还有个细节值得注意hole空洞处理。PostgreSQL 的 page 结构里在页头和页尾之间可能有一大块空闲区域这个区域的内容是未定义的没必要写入 WAL。所以 PG 在写入 FPI 时会先计算 hole 的位置和大小通过XLogRecordBlockImageHeader里的hoi_mask字段记录把 hole 部分从 FPI 中剔除只写页头和有效数据部分。9.5 引入wal_compression之后压缩操作是在 hole 剔除之后进行的也就是先去掉空洞再对剩余部分做 pglz 压缩。这个顺序很重要因为它会显著影响压缩效果——8KB 的页面里如果有 4KB 的 hole先去洞再压缩压缩率能提升不少。2.3 wal_compressionon 的收益与代价很多人一看到wal_compression默认 off9.5~13就以为它是个可有可无的参数。实际上在高写入量的环境里这个参数的开和关对 WAL 生成量、磁盘 IO 和复制延迟的影响经常是数量级的差别。先算一笔账。假设你的业务是订单系统checkpoint 间隔 5 分钟每 5 分钟内有 20000 个页面被第一次修改也就是要产生 20000 个 FPI。每个 FPI 原始 8KB那么每 5 分钟 WAL 里光 FPI 就要写 20000 * 8KB 160MB。如果是 32KB 的页面编译时--with-blocksize32这个数字直接翻 4 倍640MB。如果开了wal_compressionon假设压缩率 60%这是很保守的估计WAL 体积能降到 64MB 左右。这不仅仅是磁盘空间的问题——WAL 写入是顺序 IO但大量的 FPI 会占用 WAL buffer 的 flush 带宽进而拖慢 commit 速度这是很多数据库突然变慢的隐藏原因。但wal_compressionon也是有代价的。最明显的是 CPUpglz 压缩在 9.5 时代效率并不高压缩 CPU 开销大约在 100~200MB/s 的量级。如果是超高频写入、单机几十万 TPS 的场景压缩 CPU 可能成为瓶颈。另外压缩后的 WAL 在恢复replay阶段需要解压同样消耗 CPU。所以 9.5 时代很多 DBA 对wal_compression的态度是知道它有用但不敢随便开。这也是为什么 14 版本才把它改成默认 on——14 版本里除了 pglz没有新增算法但社区对 pglz 的优化和整体 CPU 预算有了更充分的评估加上大家已经积累了足够多的生产验证。我个人的建议是如果你的 CPU 有富余比如 16 核以上的机器CPU 使用率常年低于 50%并且 WAL 生成速率是你磁盘 IO 的瓶颈之一那不要犹豫把wal_compression打开。14 及以后默认开这个默认值本身就已经说明问题。13 及以下可以先通过pg_current_wal_insert_lsn()9.5 里是pg_current_xlog_insert_location()和pg_wal_lsn_diff()9.5 里是pg_xlog_location_diff()观察 WAL 增长速率再决定开不开。2.4 新格式带来的解析变化与兼容策略WAL 格式变了最直接的受影响方其实不是数据库本身而是围绕 WAL 做文章的周边生态物理流复制、pg_rewind、第三方备份工具、CDCChange Data Capture工具如 Debezium、以及各种 WAL 解析工具。比如pg_waldump这个工具9.5 版本对 WAL 记录的打印输出做了重构你现在在 14 版本上跑pg_waldump看到的block 0: rel ... blk 123输出格式雏形就是在 9.5 定下来的。如果你是从老版本比如 9.4直接跳到 14看pg_waldump的输出会觉得变了很大核心原因就是 block header 数组的解析逻辑变了。更重要的兼容性问题在复制协议上。PostgreSQL 的物理复制协议是通过 WAL 记录流式同步的主库发什么 WAL 记录备库就应用什么。9.5 的 WAL 格式调整意味着9.5 的主库不能向 9.4 的备库发送新格式的 WAL其实通过版本检查也直接挡掉了同一大版本内的物理复制没问题跨大版本的流复制本身就不支持除了通过 pg_upgrade 升级。但对于归档archive和 PITR 来说如果你有老版本的备份归档在恢复到新版本时得走 pg_upgrade 或者转储恢复不能直接拿 9.4 的 WAL 在 9.5 上 replay。这一点在做大版本升级规划时一定要提前评估。从 9.5 到 18WAL 格式的兼容性方面有一个长期策略值得说PostgreSQL 在设计 WAL 记录格式时一直遵循向前兼容旧版本读取新版本 WAL是不可能的但同一版本内向后兼容是有保障的。所以在升级 PostgreSQL 时物理复制和 PITR 跨大版本失败是正常现象不要尝试用老版本工具去读新版本 WAL报错是正常的不是 bug。3. 实操验证从源码编译到观察 WAL 格式变化3.1 编译支持 wal_compression 的 PostgreSQL这一节用实际操作带你把前面的原理验证一遍。我建议直接在本地编一个 9.5 的 PostgreSQL 来观察老格式同时编一个 15 或 16 的版本做对比。如果你的机器性能不错两个实例可以并行跑用initdb初始化两个数据目录端口分开就行。编 9.5 之前要确认依赖。9.5 的configure脚本比较老在较新的 Linux 发行版上可能会遇到-Werrorimplicit-function-declaration这种编译报错解决办法是给configure加上CFLAGS-Wno-errorimplicit-function-declaration。在 CentOS 7 或者 Ubuntu 20.04 上9.5 的编译流程基本没问题到了 Ubuntu 22.04 就得先解决这个编译告警问题。# 以 9.5 为例 wget https://ftp.postgresql.org/pub/source/v9.5.25/postgresql-9.5.25.tar.bz2 tar xf postgresql-9.5.25.tar.bz2 cd postgresql-9.5.25 ./configure --prefix/usr/local/pg95 --with-pgport5495 CFLAGS-O2 -Wno-errorimplicit-function-declaration make -j$(nproc) make install初始化数据目录时可以直接指定-k开启数据校验和checksum这样后面验证 FPI 是否恢复正确时更方便。/usr/local/pg95/bin/initdb -D /data/pg95 -k /usr/local/pg95/bin/pg_ctl -D /data/pg95 -l /data/pg95/log -o -c wal_compressionon start启动之后用SHOW wal_compression;确认参数生效。9.5 版本wal_compression可以在postgresql.conf里设置也可以ALTER SYSTEM SET但注意它是SIGHUP级参数SET之后不用重启pg_ctl reload或者SELECT pg_reload_conf();即可。3.2 用 pg_waldump 观察新旧格式差异接下来我们制造一点 WAL 流量来观察格式。建一张表不断插入数据同时做一次CHECKPOINT然后在插入一些数据再执行SELECT pg_switch_wal();9.5 里这个函数叫pg_switch_xlog()强制切换 WAL 段。CREATE TABLE t_demo (id serial primary key, data text); INSERT INTO t_demo (data) SELECT repeat(a, 1000) FROM generate_series(1, 10000); CHECKPOINT; INSERT INTO t_demo (data) SELECT repeat(b, 1000) FROM generate_series(1, 10000); SELECT pg_switch_wal();然后找到最新的 WAL 文件用pg_waldump看ls -l /data/pg95/pg_wal/ # 9.5 路径是 pg_xlog /usr/local/pg95/bin/pg_waldump /data/pg95/pg_wal/000000010000000000000002你会看到类似这样的输出rmgr: Heap len (rec/tot): 81/ 8591, tx: 789, lsn: 0/17000030, prev 0/16FFFFF8, desc: INSERTINIT off 1 blkref 0x0如果在pg_waldump的输出里看到FPI字样说明这条记录里带了 full page image。对比关闭wal_compression前后的len (rec/tot)值你会发现总的记录长度tot字段有显著差异——压缩后的 FPI 记录总长度明显小于未压缩的记录。这是最直观的验证方式。如果你想看更底层的结构可以给pg_waldump加-b显示 block 引用和-p显示每个 block 的详细信息输出会更详细。在高版本15里pg_waldump的详细模式甚至会显示压缩算法和原始长度对分析非常有帮助。3.3 用 pageinspect 验证 FPI 行为很多人不知道pageinspect这个 contrib 扩展在 WAL 分析中的用处。它主要是查看数据页内容的但它的page_header()函数能帮我们确认一个页面的pd_lsn值从而验证 FPI 生成的时机。CREATE EXTENSION IF NOT EXISTS pageinspect; SELECT lsn, pd_lsn, pd_lower, pd_upper FROM page_header(get_raw_page(t_demo, 0));在CHECKPOINT之前和之后分别跑一次这个查询你会看到pd_lsn在 checkpoint 后会变成0/0或者一个很老的位置。然后继续插入数据checkpoint 之后第一次修改的页面就会触发 FPI如果wal_compressionon这些 FPI 在 WAL 里就是压缩存储的。这个验证方法可以让你确认哪些页面产生了 FPI、压缩是否真的生效了比单纯看 WAL 段大小更有说服力。配合 WAL 段大小的统计可以算压缩率SELECT pg_wal_lsn_diff(pg_current_wal_insert_lsn(), 0/0) / 1024 / 1024 AS wal_mb;先开wal_compressionoff跑固定负载再开on跑同样负载对比 WAL 增长量压缩率一目了然。这种对比实测方式比任何文档都更有说服力。4. 常见问题与排坑实录4.1 为什么开了 wal_compression 后 WAL 体积反而变大这个问题不是 bug而是 hole 处理和压缩失效的边界场景。第一种情况你的表全部是每个页面只写少量数据但页面的其他部分都是随机数据的访问模式比如高频 UPDATE 大字段、或者频繁写入大量无法压缩的二进制数据图片、加密数据等。这种情况下 pglz 压缩率接近 1甚至可能因为压缩算法自身的开销pglz 在极端情况下会退化为不压缩但 header 开销增加导致 WAL 比不压缩还大一点。解决办法先评估你的数据可压缩性如果全是随机字节wal_compression收益很低可以考虑关掉省下 CPU。第二种情况hole 处理与wal_compression的交互。PostgreSQL 在写 FPI 时会计算页面中数据实际存在的区间把空洞部分剔除。如果页面非常满比如 fillfactor 高页面利用率在 95% 以上hole 少甚至没有 hole那么 8KB 页面 8KB 全量写压缩收益就取决于数据本身的可压缩性。所以wal_compression对页面稀疏、有大量空洞的场景收益最大对页面紧凑、数据随机的场景收益很小。4.2 主备物理复制下wal_compression 配置不一致有什么问题先说结论wal_compression是主库 WAL 写入侧的参数备库只需要能解析主库发过来的 WAL 记录即可不需要在备库上也开启wal_compression。即使主备的wal_compression配置不一致物理复制也能正常工作因为 WAL 记录在生成时已经把压缩与否的状态通过标志位BKPBLOCK_HAS_COMPRESSED_IMAGE打包进记录了备库根据标志位决定是否解压不依赖自己的 GUC 参数。但有一个隐性影响备库的wal_compression配置会影响其 WAL 归档和级联转发。如果备库开启了wal_compressionon并且备库也接收写请求比如备库上有热备查询但不会产生新的 WAL那么在备库上产生的 WAL 主要是它自己执行恢复时产生的比如 truncate、vacuum 等普通操作不产生 WAL FPI。这个配置不影响主库发来的 WAL 记录所以整体影响不大。但是如果你用备库做归档archive_modeon且archive_command配置在备库上备库接收到的 WAL 段本身已经是主库生成的压缩标志位是主库决定的备库的wal_compression配置不影响归档段的大小。4.3 9.5 的 wal_compression 与 15 的差异升级需要注意什么从 9.5 一路升到 15/16/18wal_compression这个参数的行为变化很大升级时如果不注意可能出现为什么我配了lz4不生效或者为什么 WAL 突然变大了的问题。最典型的差异15 版本开始wal_compression从布尔值变成了多值枚举off/on/pglz/lz4/zstd默认值是on即 pglz。如果你在 9.5 的postgresql.conf里写了wal_compression truepg_upgrade生成的 15 配置文件里会显示成wal_compression on语义上没问题。但如果你在 15 里把wal_compression改成lz4而你的 PostgreSQL 编译时没有--with-lz4启动时会直接报错。这个启动报错是很多升级踩坑的第一站。另外15 版本引入了 WAL 汇总器WAL summarizer的概念用于支持更高效的pg_rewind。这个功能的实现依赖 WAL 记录的 block 引用信息9.5 的 WAL 记录格式在汇总器里也能解析但 9.5 本身不支持 WAL 汇总。如果你是从 9.5 直接跳到 15要记得pg_rewind在 15 里默认要求开启 WAL 汇总wal_summary自动开启这在老版本里是没有的。4.4 备份与 PITR 场景下 wal_compression 的影响用pg_basebackup做物理备份时wal_compression的配置会影响备份期间的 WAL 归档大小。如果你的 WAL archive 是走archive_command传到对象存储或者备份服务器开着wal_compression能显著减少传输带宽和存储成本。但要注意pg_basebackup在备份过程中产生的 WAL包括 FPI也会被压缩而压缩的 WAL 段在 PITR 恢复时需要解压所以恢复机也要有足够的 CPU。这一条在云端恢复场景尤其重要——如果你用的是小规格的恢复实例CPU 弱解压 WAL 可能成为恢复速度的瓶颈。这里顺便提一个 9.5 时代特有的坑9.5 的pg_basebackup如果开了压缩 WAL备份生成的backup_label文件里记录的起始 LSN对应的是压缩 WAL 段的位置。恢复时如果从归档拉取 WAL 段归档工具要有能力处理压缩段解压后再交给 PostgreSQL 重放。很多自研的归档脚本只做了cp没有做解压恢复时就会出现requested WAL segment ... has already been removed这类错误。排查方法和解决办法在recovery.conf9.5 及之前里配置restore_command时加一层管道解压比如restore_command gunzip -c %f.gz %p。4.5 常见问题速查表问题现象可能原因排查方法解决办法wal_compressionon后 WAL 反而变大数据不可压缩页面无空洞用pg_waldump对比压缩前后 FPI 记录长度用pageinspect查看页面空洞比例评估业务数据可压缩性决定是否关闭调整 fillfactor 增加空洞比例15 设置wal_compressionlz4启动失败编译未启用 lz4 支持pg_config --configure查看编译选项SELECT * FROM pg_settings WHERE namewal_compression查看可用值重新编译 PostgreSQL 并加--with-lz4或改用 pglz/zstd升级后pg_rewind失败WAL 汇总器未开启或未生成汇总文件SELECT * FROM pg_stat_wal_summaries;查看汇总状态调大wal_summarize_interval或直接ALTER SYSTEM SET wal_summaryon然后重启PITR 恢复时requested WAL segment already removed归档段是压缩的但restore_command没做解压检查归档目录文件扩展名查看 PostgreSQL 日志修改restore_command加解压步骤统一归档脚本处理压缩段主备复制延迟与wal_compressionon相关备库 CPU 弱解压开销大观察备库 CPU 使用率在pg_stat_replication里看replay_lsn落后量备库扩容 CPU或者主库临时关闭压缩低峰期评估pg_waldump看不到 FPI 输出版本不同导致输出格式变化或者记录确实没有 FPI用高版本pg_waldump加-b参数检查full_page_writes是否被关闭确认full_page_writesoncheckpoint 后第一次插入时段内观察5. 从 9.5 到 18 的完整演进路线图5.1 wal_compression 默认值与算法的演变wal_compression参数的演进可以用下面这张时间线看得很清楚PostgreSQL 版本参数取值默认值可用的压缩算法9.5on/offoffpglz9.6on/offoffpglz10on/offoffpglz11on/offoffpglz12on/offoffpglz13on/offoffpglz14on/offonpglz15off/on/pglz/lz4/zstdonpglz、lz4、zstd16off/on/pglz/lz4/zstdonpglz、lz4、zstd17off/on/pglz/lz4/zstdonpglz、lz4、zstd18off/on/pglz/lz4/zstdonpglz、lz4、zstd这个演进里有两个关键转折点。第一个是 14 版本把默认值改成on这说明 pglz 的 CPU 开销在主流硬件上已经不是一个显著瓶颈或者说 WAL 体积的收益已经明显超过 CPU 代价。第二个是 15 版本引入 lz4 和 zstdlz4 的解压速度比 pglz 快一个数量级zstd 的压缩比又远高于 pglz这两个算法的加入让wal_compression的适用面变得非常广。这里有个有意思的细节PostgreSQL 15 在引入新算法时把wal_compression的 GUC 类型从bool改成了enum但为了兼容旧配置保留了on和off两种语法。on等价于pglz。如果你看到有人在 15 里配wal_compression on他用的实际上就是 pglz不是 lz4。想要最优压缩还是得明确指定算法。5.2 WAL 记录格式的后续调整从 9.5 到 18WAL 记录格式并不是一成不变的。9.5 是块引用数组化的起点之后的每个大版本都在这个基础上做增量调整。10 版本引入了逻辑复制但逻辑复制的 WAL 记录类型XLOG_LOGICAL_MESSAGE和物理 WAL 格式并行不悖物理格式本身没有大的改动。11 版本引入了pg_promote、pg_ctl promote的行为调整对 WAL replay 状态机有影响。12 版本开始PostgreSQL 支持了--with-icuWAL 本身没变但很多索引相关的 WAL 记录类型有变化。13 版本在 WAL 里增加了对REPLICA IDENTITY FULL行为的支持。14 版本是 WAL 相关功能的大版本引入了wal_compression默认开、WAL 归档并行、pg_waldump的--save-fullpage选项等。15 版本引入 WAL 汇总器和pg_rewind增强。16 版本调整了 WAL 的 replay 并发控制max_parallel_apply_workers_per_subscription用于逻辑复制。17 版本主要是逻辑复制和 vacuum 相关的 WAL 记录优化。18 版本2025 年发布在 WAL 方面着重做了超大事务的性能提升和 WAL 格式的紧凑化。在这条演进线里XLogRecordBlockHeader结构体一直保持大体稳定但字段含义和标志位有细微变化。比如 15 版本在XLogRecordBlockImageHeader里增加了BKPBLOCK_HAS_COMPRESSED_IMAGE标志位的多算法扩展还引入了BKPBLOCK_XLOG_LSN标志位用于在 WAL 里记录页面 LSN。15 版本还增加了XLogRecordBlockHeader中的fork_flags字段对MAIN_FORKNUM之外 fork 的支持。这些都是内部细节但如果你在写 WAL 解析工具必须跟着版本同步更新。5.3 升级路径与心理预期如果你现在还在运行 9.5想升到 18路径上要注意几条硬规则一是不支持直接从 9.5 升级到 18必须分步走中间至少要经过一个 10 以上的版本官方文档里写的是从 10 之前版本升级必须先升到 10。推荐的路径是 9.5 → 10 → 13 → 16 → 18或者 9.5 → 12 → 15 → 18。每一步都要跑pg_upgrade的检查模式--check确认没有插件兼容性问题。二是wal_compression的行为差异。在 9.5 里如果开启了wal_compressionon升级到 14 之后默认值还是 on行为一致到 15 之后如果服务器编译时带了 lz4/zstd你可以在新版本里对比一下三种压缩算法的压缩率和 CPU 开销通常 zstd 在压缩比上有明显优势而 lz4 在速度上有明显优势生产环境推荐 zstd 或 lz4 二选一不建议再停留在 pglz。三是 WAL 汇总器。15 开始如果启用了 WAL 汇总默认自动开启会额外产生pg_wal_summary目录下的汇总文件。这些文件会占用磁盘空间但可以通过pg_wal_summary_cleanup()清理。如果你对 WAL 段大小有严格的审计需求升级后要记得把这个新目录纳入监控。四是备份和归档脚本。9.5 时代的archive_command通常是cp %p /archive/%f升级到 15 之后如果wal_compressionzstd归档段依然是原始 WAL 格式不会额外压缩因为 PostgreSQL 不会对已经写入的 WAL 段做二次压缩所以archive_command不需要为压缩算法做额外适配。但如果你想在归档端做二次压缩比如 gzip 一下再传对象存储要确保restore_command能正确解压回原始 WAL 段否则 PITR 会失败。5.4 从 WAL 角度看未来的 PostgreSQL按目前社区的开发节奏未来几个版本的 WAL 还可能继续优化。有一个持续讨论的点是 WAL 记录里 block header 的进一步压缩——通过减少RelFileNode的重复存储比如一个事务里多次操作同一个文件时只在第一条记录里写一次RelFileNode来减少 WAL 体积。这个优化在 14 版本里已经实现了一部分BKPBLOCK_SAME_REL标志位后面可能还会继续扩展。另一个方向是 WAL 的并行写入和异步刷盘。目前 PostgreSQL 的 WAL 写入是单线程的虽然 WAL buffer 是共享的但XLogFlush是全局互斥的。在高并发场景下WAL 写入锁一直是隐藏的瓶颈点。社区已经有一些雏形讨论但短时间内没有明确的落地计划。不过这和wal_compression关系不大反而是和synchronous_commit、commit_delay这些参数交互更密切。6. 实操心得几个值得收藏的小技巧最后分享几个我在实际操作中积累的经验不算常规文档内容但对排查问题和日常调优很管用。技巧一快速确认 FPI 压缩是否生效。不要只看 WAL 段大小那个太粗糙。用pg_waldump -b找到一条包含 FPI 的记录对比len (rec/tot)里的rec和tot。如果rec明显小于 8KB比如几百字节说明压缩生效了如果rec是 8192 左右说明没有压缩。这种方法比猜参数是否生效要精确得多。/usr/local/pg15/bin/pg_waldump -b /data/pg15/pg_wal/000000010000000000000003 | grep FPI | head -5输出里你会看到类似FPW或FPI字样的标记后面跟着 block 信息。如果压缩开启这行记录总长度会明显小于 8192 头部开销。技巧二用pg_stat_archiver监控 WAL 归档速率。归档跟不上 WAL 生成速率是 PITR 失效的最大风险。pg_stat_archiver视图里的failed_count和last_failed_time字段能帮你快速判断归档是否健康。配合wal_compression的压缩效果归档压力会小很多。技巧三云环境下的 WAL 压缩选择。在云上跑 PostgreSQL尤其是使用网络存储EBS 等时WAL 的 IO 带宽往往比本地盘贵得多。这种情况下wal_compressionzstd通常是最优选择——压缩率高、解压速度也不差。而如果是对延迟极敏感的 OLTP 场景还要考虑 zstd 压缩本身带来的 CPU 延迟可能需要压测后在lz4和zstd之间选一个平衡点。技巧四从 9.5 升级到 18先做一次 WAL 格式调研。很多人在升级前只关注插件兼容性和 SQL 语法忽略了 WAL 格式的变化。我建议在升级前先在新版本上跑一段时间的老版本备份归档比如用pg_basebackup从老库拉一个备份恢复到新版本实例上然后用pg_waldump对比解析老库产生的 WAL 文件和新库产生的 WAL 文件。这个动作能提前发现解析工具、恢复脚本里的格式兼容问题避免真正升级时手忙脚乱。按照我的经验PostgreSQL 的 WAL 体系是内核里最值得花时间研究的模块之一。它看似只是顺序写日志但在高可用、备份恢复、性能调优这些核心场景里WAL 的细节往往决定系统的上限。9.5 到 18 的这条演进线正好把 WAL 从勉强够用带到了紧凑高效中间涉及的压缩、格式、汇总器这些点每一个都值得在实战中验证一遍。希望这篇梳理能帮你省下一些翻源码和踩坑的时间。
返回列表