刚刚整理完今天的 PostgreSQL 相关动态,这期日报里最有意思的其实是两条看似不搭的线索:一边是低调到快被遗忘的 pg_waldump,另一边是能让查询在 IO 等待上少耗几个来回的索引预取。放在一起看,恰好构成了一条完整链路——先知道 WAL 里写了什么,再想怎么把 WAL 引用的页面提前取到内存。这篇日报我把这两块都展开聊聊,顺带整理几个适合日常巡检和故障排查的实操技巧,DBA、内核开发者和对 PostgreSQL 性能调优感兴趣的同学都可以直接参考。
1. 本期技术日报速览
先说今天的"版面"。技术日报最忌讳的就是罗列一堆链接然后说"大家自己看",所以我习惯先给一张速览表,把自己认为值得投入时间的方向标出来,后面再逐个讲透。
| 关注点 | 热度 | 适合谁 | 一句话结论 |
|---|---|---|---|
| pg_waldump 实战用法 | 中 | DBA、内核开发者 | 排查复制和归档问题时,比翻日志快得多 |
| WAL 结构认知 | 中高 | 所有 PostgreSQL 使用者 | 理解 LSN 和资源管理器,很多疑难杂症能自己定位 |
| 索引预取新进展 | 高 | 性能优化、内核开发者 | 方向很明确,但离进主干还需要时间沉淀 |
| 预取参数调优 | 中高 | DBA、一线运维 | 别盲目调高 effective_io_concurrency,SSD 上反而容易退化 |
1.1 为什么把 waldump 和索引预取放一起聊
因为没人说过"看 WAL 的人"和"做预取的人"之间有多大关系。但实际上,预取要解决的第一个问题就是:你得知道接下来会访问哪个块。WAL 里恰恰记录了事务修改过的所有页面引用,页面的 block reference 写得明明白白。pg_waldump 的-b参数会把每个记录引用的 block 编号打出来,你一眼就能看到某条记录动了哪张表的哪个页。
理解了这一点,再去看社区里关于 WAL 预取和索引预取的讨论,就不会觉得那是空中楼阁了。不管是崩溃恢复过程中要把 WAL 指向的页面预取到 buffer pool,还是索引扫描时提前把索引项对应的堆页面拉进缓存,本质上都在做同一件事:用 IO 预读掩盖磁盘随机访问延迟。后面我会分别展开。
2. pg_waldump:平时没人提,出事却靠它
2.1 WAL 与崩溃恢复:先补基础
PostgreSQL 的 WAL(Write-Ahead Logging)机制,简单说就是"先写日志,再改数据"。任何事务在把改动落盘到数据文件之前,必须先确保对应的 WAL 记录已经落盘。这样一来,系统崩溃后,PostgreSQL 可以从最后一个检查点开始,把 WAL 里记录的所有修改重放一遍,恢复出崩溃前的状态。
大多数人对 WAL 的认知止步于此,"知道它是崩溃恢复的保障"。但 WAL 还承担着另外两个重要职责:复制和归档。备库依赖 WAL 流复制来同步数据,归档工具依赖 WAL 文件来保存历史变更。所以当复制延迟、归档失败、日志堆积这些问题出现时,你第一件想做的事情应该是:看看 WAL 里到底有什么。
pg_waldump 就是官方提供的 WAL 解析器。它在 PostgreSQL 10 以后作为核心工具随安装包一起发布,不需要额外编译。它能把 WAL 文件里一条条记录解码成可读文本,告诉你每条记录的资源管理器类型、LSN、事务号、修改的页面信息。
2.2 常用参数:一张表讲清楚
很多人看到 pg_waldump 的第一反应是"参数好多"。其实高频使用的就几个:
| 参数 | 作用 | 我的使用场景 |
|---|---|---|
-p | 指定 WAL 文件目录 | 默认是pg_wal目录,离线分析时必带 |
-r | 按资源管理器过滤 | 只看 heap、btree 或 checkpoint 记录,输出干净 |
-b | 打印块引用细节 | 排查"到底动了哪个页面"时开 |
-s/-e | 指定起始 / 结束 LSN | 配合 LSN 定位一段区间 |
-n | 限制输出记录条数 | 先看前几条确认格式,避免刷屏 |
-t | 指定时间线 | 处理时间线切换问题时用 |
-x | 按事务 ID 过滤 | 查某个长事务都写了什么 |
-q | 安静模式 | 脚本里减少噪音 |
基础命令就是这样:
pg_waldump -p /var/lib/postgresql/16/main/pg_wal 0000000100000002000000A1 0000000100000002000000A5这里的两个参数是起始段和结束段的文件名。段文件名长度固定 24 位十六进制,前 8 位是时间线,中间 8 位是逻辑日志序号,后 8 位是段内偏移。如果只传一个段,它就只解析那一个文件。
2.3 实战:看一条 INSERT 记录到底写了什么
光说参数没意思,我放一段典型的输出,咱们逐字段拆一下。我加上了-b,所以能看到块引用信息:
pg_waldump -p /data/pg_wal -b -n 3 0000000100000002000000A5输出大概是这个风格:
rmgr: Heap len (rec/tot): 34/ 34, tx: 547, lsn: 2/A5000030, prev 2/A5000018, desc: INSERT off 49 flags 0x00, blkref #0: rel 1663/16384/16385 blk 0 rmgr: Transaction len (rec/tot): 42/ 42, tx: 547, lsn: 2/A5000058, prev 2/A5000030, desc: COMMIT 2025-03-22 10:24:11.281302+08逐个解释:
rmgr: Heap:这条记录由 Heap 资源管理器产生。PostgreSQL 把 WAL 记录按模块分类,heap 代表数据页修改,btree 代表索引页修改,transaction 代表事务提交/中止,checkpoint 代表检查点,standby 代表备库相关记录。len (rec/tot): 34/34:前者是记录本身长度,后者是加上页头之后的总长度。如果开了整页写,tot 会远大于 rec。tx: 547:产生这条记录的事务 ID。lsn: 2/A5000030:这条记录写入的 LSN 位置。prev 2/A5000018:上一条记录的 LSN,用来保证日志链的连续性。desc: INSERT off 49:这条记录描述的是在 49 号槽位插入了一个 tuple。blkref #0: rel 1663/16384/16385 blk 0:引用的块是数据库 16384、表 16385(relfilenode)的第 0 页。1663 是表空间的默认 OID,所有普通表都长这样。
看到这,你就明白复制和归档的数据来源多清晰了。备库拿到这条记录后,会去更新对应页面的第 49 号槽位;崩溃恢复时,也会重放同样的操作。blkref里的 relfilenode 和 block 编号,正是预取机制判断"接下来该加载哪个页面"的直接依据。
用-r过滤也非常实用。比如我只想看看某个时间窗口内有没有 checkpoint 记录:
pg_waldump -p /data/pg_wal -r checkpoint -n 10 0000000100000002000000A5输出会告诉你 checkpoint 的 redo 位置、停止位置、检查点开始时的事务快照。配合监控里的 checkpoint 频率,能判断是不是max_wal_size设置太小导致频繁触发。
3. 索引预取:从"每页等一次"到"提前把书拿到桌边"
3.1 随机读的痛点和预取的收益模型
先做一个思想实验。假设你在一个图书馆里查资料,索引相当于目录柜,数据堆相当于书架上的书。PostgreSQL 的普通索引扫描流程是这样的:先在索引里找到一批条目的位置(目录告诉你在第几排第几本),然后一本书一本书地去拿。糟糕的是,每拿一本书之前你都不知道下一本是不是就在隔壁,所以来回走动的时间完全停不下来。
传统索引扫描的行为就是这样的"来回走动"。每一条索引项指向一个堆页面,而相邻索引项指向的页面在物理上往往隔得很远。机械硬盘时代,这种随机读几乎是灾难;换成 SSD,单个随机读延迟降到了几十微秒级别,但高并发下依然可能把 IO 队列打满。
预取的思路很简单:既然我大概知道接下来要读哪些页面(B-tree 扫描时叶子节点就是按序走的),为什么不在 CPU 处理当前页面的同时,把后面那几个页面的读取请求提前发出去?这样在内存处理阶段,磁盘和内存的工作就重叠了,延迟被隐藏掉,整体吞吐自然上去。
这套理念在 PostgreSQL 里其实早就有落地。做bitmap heap scan时,系统会先扫一遍索引,把命中的堆页面位图收集起来,再按页面顺序物理读。这个阶段就可以用effective_io_concurrency控制并发预读。而最近社区讨论的"索引预取"更进一步,是在普通index scan的过程中也插入预取逻辑,不再依赖位图扫描的那套缓冲机制。
3.2 社区里的"新进展":recovery_prefetch 与异步 IO 基建
顺着 WAL 这条线讲,我最想提的是恢复阶段的 WAL 预取。上一次引入这类能力时,核心思路就是:崩溃恢复过程中,WAL 记录已经明确指出了接下来会用到哪些页面(就是前面blkref里的信息),那么系统可以在重放当前记录的同时,把后续记录将要引用的页面预先读入 buffer pool。这个功能以recovery_prefetch的形式出现,配合maintenance_io_concurrency可以控制预取的并发度。对从备份恢复的大实例来说,开启后的速度提升往往非常可观。
之所以说这是"新进展",是因为索引预取的方向虽然已经讨论了很久,但一直缺乏一个统一的基础设施。PostgreSQL 传统上对异步 IO 的支持比较原始,很多预取逻辑靠posix_fadvise这类内核接口实现,能做的只是"提示",无法做到真正的高并发异步读写。现在社区把异步 IO 基础设施作为重要方向在推进,一旦有了干净的 AIO 接口,索引预取、WAL 预取、批量预读这些能力就不需要各自实现一套 hack 了。
如果你去翻邮件列表,会发现相关提案里反复出现几个关键词:pginvalidate、startprefetch、iocontext。这些补丁真实地反映了设计难点——预取不是简单的"提前读几张页",而是要管理预取的并发度、深度的上限,还要处理预取失败时的退避策略。所以我个人判断,短期内在稳定分支看到索引预取全面进核心的可能性不大,但这方向本身是正确的,值得持续跟踪。
3.3 预取参数的调优边界
说完了方向,落到日常可以动手的部分。和预取相关的 GUC 有三个最常见的:effective_io_concurrency、maintenance_io_concurrency、recovery_prefetch。前两个控制普通查询和维护命令的预取并发度,第三个控制恢复时的预取开关。
我见过不少配置直接抄网上"推荐值"翻车的案例。effective_io_concurrency在 SSD 上确实可以调到几十甚至几百,但前提是数据库服务器的 IO 栈扛得住。如果你用的是云盘且底层有带宽限制,调太高反而让预读请求排队,导致正常读请求的延迟上升。一个相对稳妥的办法是分步测试:从 2 开始,跑有代表性的查询,观察pg_stat_io里的evict_buffer和extend时序,逐步上调,直到 TPS 不再增长为止。
机械硬盘时代,预取的价值显而易见;SSD 时代,预取的作用从"救火"变成了"锦上添花"。但真正的大胜利在恢复场景:几千 GB 的数据在崩溃后重新加载,WAL 预取能让恢复时间从"小时"降到"分钟"量级,这个收益不是简单拿 IOPS 能衡量的。
4. 日报视角:几个值得留意的技术方向
4.1 监控与观测:让 WAL 和 IO 不再黑盒
这期日报我还想单独留个位置,聊聊监控侧的变化。pg_waldump 是一个离线工具,你无法用它实时观察线上 WAL 的写入情况。所以在日常巡检里,我更依赖这些实时视图:
pg_stat_wal:看 WAL 写入量、刷盘次数。pg_stat_io:看读写的来源和去向,区分 recovery、maintenance、extension 等背景。- 归档目录里文件的时间戳:如果归档持续堆积,第一时间用 pg_waldump 打开那个卡住的时间点附近的段。
三者结合,基本能还原出"WAL 写得多快、读了哪些块、预取有没有生效"的完整画面。尤其是在排查复制延迟时,pg_stat_wal显示wal_records增长,但归档目录的文件数不动,那问题几乎都出在归档命令或者归档槽上,不需要直接看 WAL 内容就能定位。
4.2 版本策略:新特性别急着上,先看副作用
追踪 PostgreSQL 演进的人应该有个体会:新特性的性能收益是"宣传期"最亮眼,但真正坑人的往往在边界条件里。无论是 recovery_prefetch 还是未来的索引预取,潜在副作用都包括预取时机的误判导致缓存污染,以及 IO 并发过高拖垮共享存储。
所以我给自己定的版本策略很简单:重大性能特性等发布后的第一个次要版本再评估,评估时只看两类数据——一是业务侧真实负载下的性能回归测试,二是pg_stat_io里缓存命中率的变化。日报里看到新的预取补丁讨论,可以拿来当技术预研去看,但别把它当成本赛季的"冲分英雄"。
5. 常见问题速查:WAL 与预取篇
5.1 WAL 相关高频故障
| 现象 | 可能原因 | 定位方法 | 处理思路 |
|---|---|---|---|
| WAL 文件堆积 | 归档命令失败或复制槽不消费 | 查 archive_status 目录,用 pg_waldump 看最新写入位置 | 修复归档命令或清除无效复制槽 |
| 报错缺 WAL 段 | 段被手动删除或流复制中断过久 | 按报错段号在备份中查找 | 从备份恢复缺失段,或走全量重建 |
| 备库持续追不上主库 | 日志量过大或wal_keep_size不足 | 看备库 pg_stat_replication 的 replay_lsn 与 sent_lsn | 调大wal_keep_size,或检查网络带宽瓶颈 |
| 整页写导致 WAL 膨胀 | 检查点间隔过长或频繁触发 | 搜索rmgr: Heap ... flags 0x01记录 | 调整 checkpoint 参数,平衡恢复时间与 WAL 体积 |
这里有一条排查经验:当归档目录里出现.ready文件堆积,而.done文件不增加时,不必急着翻日志。先用pg_waldump -p直接读最新的 WAL 段,确认记录还在正常推进,十有八九是归档命令本身的问题。WAL 生成没有问题,归档壳有问题,这两个方向区分开来,排查效率会高很多。
5.2 预取与 IO 性能异常排查
| 现象 | 可能原因 | 定位方法 | 处理思路 |
|---|---|---|---|
| 查询变慢,但 CPU 空闲 | 随机读延迟高 | 看 pg_stat_io 的 read 统计,关注 evict/extend | 考虑开启或调高 effective_io_concurrency |
| 恢复时间比预期长 | recovery_prefetch 关闭或并发过低 | 查看恢复日志里的 "recovery worker" 相关提示 | 确认 GUC 生效,调大 maintenance_io_concurrency |
| IO 带宽被打满 | 预取并发过高 | 用系统工具查看 IO 队列深度 | 降低预取并发,观察 TPS 曲线找平衡点 |
| 预取未生效 | 不支持的操作系统或内核版本 | 用 strace 观察 pread64 的调用序列 | 确认文件系统支持 hint,或升级内核 |
关于"预取到底有没有在工作",我有个土办法:对单条 SQL 开auto_explain,对比开启和关闭 effective_io_concurrency 时的Buffers: shared hit/read比例。如果 read 大比例转化成 hit,说明预取机制正在起作用;如果 read 居高不下且每次都很慢,那大概率是并发度不足或者你的存储根本不支持异步 hint。
6. 最后一个小习惯:把 waldump 变成日常巡检工具
说个我自己的实操习惯。我不会等到出现复制故障才去用 pg_waldump,而是把它做成一个每周巡检项:从归档目录里随机挑一个段,跑一次分资源管理器统计,看看这一周里 heap、btree、checkpoint、standby 各类记录占比有没有异常。做这件事只需要一条命令:
pg_waldump -p /archive/wal 0000000100000002000000A1 -q | awk '{print $1}' | sort | uniq -c | sort -rn如果某一天发现Standby类的记录异常增多,我就会去检查是不是有新的备库上线;如果Checkpoint记录明显变多,我就会去核对max_wal_size和检查点频率。这其实不需要通读 WAL 内容,只需要看分布变化。
从 pg_waldump 到索引预取,这一整条链路的技术点并不复杂,但每个环节都需要对"WAL 里记录了哪些块、预取如何利用这些块引用"有足够的敏感度。实操中多花十分钟做做这类小工具式的巡检,收益往往比盲目抄参数大得多。下一期日报,我打算专门聊聊 WAL 归档目录的运维细节和常见坑,如果你正好在那上面栽过跟头,欢迎带着案例来交流。