
1. 这期日报为什么值得细看从工具到优化方向的一次串联做 PostgreSQL 的人平时看技术日报最怕两件事要么全是版本发布公告通篇“修了几个 bug、加了几项功能”看完就忘要么全是深度源码分析字都认识就是不知道跟自己有什么关系。这期日报标题把两个词放在一起pg_waldump和“索引预取”。一个是很硬核的日志解析工具一个是查询性能优化方向表面看一个管“排障”一个管“加速”实际放到同一天出现恰好反映了我一直强调的一个观点——PostgreSQL 的运维和调优底层逻辑是贯通的。你不理解 WAL 怎么记录变化就很难解释为什么有些索引优化方案会在崩溃恢复场景下“原形毕露”你不理解预取机制也可能把 IO 瓶颈误判成 SQL 问题然后疯狂加索引效果却微乎其微。先说清楚这篇日报内容对谁有用。如果你是数据库管理员天天处理慢查询、数据不一致、归档失败这类问题pg_waldump是你排查链条上非常关键但常常被忽略的一环如果你是应用开发者关心线上 SQL 在数据量上来之后的稳定性那“索引预取”这个方向会直接影响你对索引扫描、位图扫描的选择和理解哪怕你只是刚开始接触 PostgreSQL 的爱好者这两个词也能帮你把“WAL 是什么”“索引扫描为什么慢”这两个基础问题真正弄明白。这一期内容说白了就是在讲一件事PostgreSQL 内部每个机制都不是孤立的搞清楚一个工具的用法往往能顺带弄懂三类问题的成因。我翻了翻近期的社区动态这个时间点上讨论这两块内容其实很有代表性。一方面WAL 相关的话题持续升温从日志格式、归档策略到崩溃恢复的可靠性讨论密度一直很高另一方面存储硬件迭代太快NVMe 盘的随机读能力已经远超传统磁盘这逼着执行器层重新思考“要不要预取、怎么预取、预取多少”这些老问题。所以这期日报既是一份“今日技术快讯”更是一个不错的切入点帮我们把 PostgreSQL 的底层机制一层层剥开看。2. pg_waldump 拆解真正看懂 WAL 才能做对排障2.1 WAL 到底是什么为什么值得专门用一个工具去解析很多刚接触 PostgreSQL 的朋友会把 WAL 理解成“日志”这个没错但不够准确。WAL 全称 Write-Ahead Logging核心规则就一句话数据页在落盘之前对应的变更记录必须先安全写入日志文件。这么做最大的价值是崩溃恢复——数据库掉电了内存里改了但没来得及写进数据文件的页面全部丢失但只要 WAL 还在重启后就能把这些变更重放出来数据和一致性不丢。这个机制听起来简单但实现细节非常复杂。一条 WAL 记录里不只是“我在某张表的某个页面改了某一行”它还要记录是哪个事务、影响哪个页面、属于哪种操作类型甚至在某些情况下会直接记录整个数据页的镜像。这些信息被组织成一种紧凑的二进制格式分门别类由“资源管理器”Resource Manager简称 rmgr处理。你直接用文本编辑器打开 WAL 文件看到的只会是乱码所以 PostgreSQL 官方提供了一个解读工具那就是pg_waldump。它的作用就是把这堆二进制日志翻译成人类可以读懂的文本告诉你“哪个时间点、哪个事务、对哪个数据块做了什么操作”。说句实在话日常运维中用到pg_waldump的频率不一定很高但它一旦派上用场基本都是疑难杂症。比如数据无端丢失、恢复后页面内容对不上、WAL 归档文件为什么异常膨胀、某个表的数据变更到底发生在哪个 LSN 范围这些问题靠 SQL 查不到靠猜更不可能唯一靠谱的办法就是把 WAL 翻开逐条看记录。这也是为什么这期日报会把它作为一个重点来提——工具冷门但价值绝对不冷。2.2 pg_waldump 的常用姿势与输出解读我先把最常用的命令形式写出来大家可以直接复制参考。假设 WAL 文件存放在数据目录下的pg_wal子目录旧版本叫pg_xlog先看看怎么解析单个文件pg_waldump -p /var/lib/postgresql/15/main/pg_wal/ 000000010000000000000001这个命令的意思是解析pg_wal目录下名为000000010000000000000001的 WAL 段文件并把其中所有记录逐条打印出来。如果只想看某一段 LSN 范围内的记录可以这样pg_waldump --start0/16B3740 --end0/16B4A28 -p /var/lib/postgresql/15/main/pg_wal/这里的0/16B3740就是 LSN 的表示方式pg_waldump会从指定的起始 LSN 解析到结束 LSN非常灵活。日常用得最多的是加上过滤条件避免输出太杂pg_waldump -p /var/lib/postgresql/15/main/pg_wal/ 000000010000000000000001 -r HEAP2-r参数指定 rmgr 类型HEAP2表示只看堆表操作的记录。你也可以用-t指定表名或者用-n限制输出条数方便快速定位问题。如果还想看更详细的信息比如记录里涉及哪些数据块、块内偏移是多少加一个--block参数即可。输出结果大概长这样rmgr: Heap , rec (lsn: 0/16B3740, prev: 0/16B36F0, xid: 701, len: 54) rmgr: Heap2 , rec (lsn: 0/16B3800, prev: 0/16B3740, xid: 0, len: 58) rmgr: Transaction , rec (lsn: 0/16B3840, prev: 0/16B3800, xid: 0, len: 34)每一行代表一条 WAL 记录lsn是这条记录自己的位置prev是上一条记录的位置xid是事务号len是记录长度。通过这些信息你可以非常直观地看到事务活动的先后顺序也能观察到 checkpoint、vacuum 等后台操作产生的记录。这些字段在实际问题排查中比你想的更常用——比如通过prev字段可以反向串联事务活动链确认哪些操作是连续的通过xid为 0 的记录可以轻松识别后台进程操作而不是用户事务。2.3 实战场景三个我能想到的最典型用法场景一怀疑某个表的数据被“莫名其妙”修改或删除。你把 WAL 文件备出来用-t指定那张表把所有相关记录拉出来然后根据 LSN 和时间点对账基本就能定位到是什么操作、在什么时间范围内发生的。注意这里有个前提WAL 文件得还在如果已经被回收那就只能靠归档文件或者提前开启归档日志。场景二检查 full_page_writes 是否生效。从 PostgreSQL 9.6 之后的版本理解起来更容易在 checkpoint 之后第一次修改某个页面时WAL 记录会带上这个页面的完整镜像这是为了防止崩溃恢复时出现页面部分写的问题。你可以用pg_waldump观察记录类型看看哪些记录带着完整的页面镜像判断 full_page_writes 的开启状态是否符合预期。这个检查在备份恢复演练时很有用可以提前发现配置隐患。场景三分析 WAL 膨胀的根源。有时候 WAL 目录突然暴增排除了长事务、大事务之后还是找不到原因。这时候用pg_waldump统计一下各类 rmgr 记录的数量和字节数就能快速定位到底是谁在刷日志——是热表的频繁更新还是某些循环删改操作还是参数配置不当导致的页面镜像过多。统计方式可以用简单的 shell 管道配合 awk把pg_waldump的输出按 rmgr 类型分组求和效率很高。2.4 用 pg_waldump 的几个“坑”很多人第一次用会觉得“输出好乱看不太懂”这其实是正常的。WAL 记录描述的是物理变更不是逻辑 SQL你很难直接看到“UPDATE users SET namexxx”这种语句看到的是Heap/UPDATE加上偏移量、长度这些物理信息。所以要真正用好pg_waldump你得对 PostgreSQL 的存储结构有一定了解至少知道 heap、btree、gin 这些页面是什么否则就是看个热闹。第二个坑是 WAL 文件的获取。线上数据库的 WAL 文件可能已经被回收或归档了pg_waldump默认只能读本地文件。如果你需要排查的时间点比较早记得先从归档存储中把对应文件拉回来放到临时目录再解析不要直接在线上环境翻找。另外用-p指定目录时目录里如果有多个 WAL 段文件解析时要注意顺序最好显式指定文件名或者用时间线参数避免解读错乱。还有一个很容易被忽略的细节WAL 格式是分版本的旧版本的工具不一定能正确解析新版本生成的 WAL 文件反之亦然。官方工具一般会提示版本不匹配但某些场景下也可能输出看起来很合理、实则错位的记录。所以尽量使用与数据库版本匹配的pg_waldump不要拿主版本不同的工具去硬试。提示生产环境不建议直接在数据目录里跑来跑去的pg_waldump建议把相关 WAL 段复制到分析机或临时目录再解析避免意外触碰数据库文件也避免正在归档的文件被读到一半而出问题。3. 索引预取慢查询优化里那块“看不见的短板”3.1 索引扫描为什么会慢一个容易被忽视的环节数据库里的索引本质上是一种为了“少读数据”而设计的结构。但最讽刺的是通过索引查数据反而可能比全表扫描更慢尤其当数据量很大、命中行数又比较多的时候。这背后的根源就是 PostgreSQL 的索引扫描机制分了两步第一步顺着索引结构找到匹配的行指针TID第二步拿这个指针去访问对应的堆表页面。第一步走的是索引页面数据紧凑、顺序性好第二步访问堆表页面时行指针指向的页面是随机分布的。随机 IO 是传统磁盘时代的噩梦因为磁头每跳到一个新位置都需要寻道时间。你今天在 SSD 上跑随机读已经没那么可怕但依然存在延迟差异而且更关键的是——你每读一个页面很可能只是为了取其中一行数据剩下的空间全被浪费了。如果一行行地“查找索引-回表-查找索引-回表”整个查询就会陷入大量细碎的 IO 请求响应时间自然下不来。索引预取要解决的就是这个问题。它的核心思想很朴素既然已经知道接下来要访问哪些 TID 了与其一个个傻等不如先提前把后续要用到的页面读进内存缓存让等待时间被计算过程“重叠”掉。打个比方你去图书馆找十本书如果只知道书名一本本找每本都要花时间在书架间走动但如果提前拿到全部书的位置清单就可以规划路线一次性把路径串起来减少来回跑的次数。预取就是那个“提前规划路线”的动作。3.2 PostgreSQL 里预取机制是怎么落地的PostgreSQL 中与预取关系最密切的是位图堆扫描Bitmap Heap Scan。执行器先扫一遍索引把满足条件的 TID 收集起来放到一个位图结构里然后按物理存储顺序去读堆页面。因为收集了全部 TID它就拥有了“全局视野”可以一次性看到接下来应该读哪些页面——这时候预取就派上用场了执行器会在读取当前页面的同时把后续若干页面的读取请求提前发出。控制这一行为的关键参数是effective_io_concurrency。这个参数的值代表系统“认为”磁盘能并行处理多少 IO 请求默认在桌面版和服务器版上有所不同多数发行版默认是 1 或 2取值越大执行器越倾向于一次性多预取一些页面。注意关键词是“认为”——它并不真实检测硬件能力而是你要根据自己的存储类型去调。传统机械盘这个值调到 2 左右就差不多了调太高反而因为寻道竞争性能更差如果是 SSD尤其是 NVMe可以放心调到 8、16 甚至更高具体值需要实测通过对比不同取值下相同查询的耗时来确定。除了位图扫描之外普通索引扫描Index Scan在 PostgreSQL 17 目前的主线开发中也有预取方面的讨论和进展。你可以在社区邮件列表和开发提交记录里看到关于“索引扫描过程中预读后续堆表页面”的实验性实现。原理上那段链路完全可行但实践中要小心预取的页面如果最终没有被用到就会污染 shared_buffers把热数据挤出去反而拖慢其他查询。所以这种优化通常需要非常谨慎的判断逻辑不是简单地“扫到就预取”就能完事的。3.3 对照实验不同参数下你能看到什么为了让大家对预取的实际效果有一个直观认识我描述一个典型实验框架你可以照着在自己环境里验证。测试表 1000 万行以上有一个二级索引查询条件选择率在 5% 左右也就是大约返回 50 万行。对于传统机械盘环境effective_io_concurrency 0关闭预取全表回表时间可能接近 20 秒以上每次读取页面都排队等待effective_io_concurrency 2时间明显下降可能到 810 秒原因是预取把部分等待时间重叠了effective_io_concurrency 8如果还是机械盘时间反而回升因为并行读请求太多磁头来回跳动更频繁。在 SSD 环境同样的表数据 1时可能耗时 5 秒 16时降到 2 秒 64之后提升就不明显了再高甚至会因为上下文切换和共享内存竞争出现轻微劣化。所以“参数越大越好”完全不成立正确姿势是画一条“耗时 vs 并发度”的曲线找到拐点所在。另一个需要留意的参数是maintenance_io_concurrency它影响的是 CREATE INDEX、VACUUM 这类维护操作中的预取并发度。在 SSD 上把它设高一点能有效缩短建索引和 vacuum 的时间对于大表维护窗口紧张的团队很有价值。不过这个参数只在支持同步扫描和预取的操作中生效机械盘上依然要谨慎。3.4 为什么这期日报会关注“索引预取的新进展”这个主题和 WAL 放在一起不是编辑随手排的。你看 PostgreSQL 最近的几个大版本和开发主线很多努力都集中在“如何让执行引擎更好地利用现代存储”上。索引预取就是其中一个方向。过去数据库设计面对的是慢速磁盘优化重点是减少 IO 次数现在 NVMe SSD 普及了单次 IO 延迟已经降到微秒级但 CPU 处理速度的提升反而没那么大于是“如何让 CPU 在等待 IO 时干更多有用的事”变成了新的优化重点。预取本质就是拿 CPU 时间换 IO 等待时间让流水线不要断。社区里关于预取的讨论方向大致有这么几条第一扩大预取范围把位图扫描的成功经验推广到普通索引扫描和嵌套循环连接里第二引入更细粒度的异步 IO 能力不再依赖posix_fadvise这类“建议性”的预读接口而是用真正的异步提交-完成机制把 IO 请求的调度权还给数据库本身第三让预取决策自适应通过统计信息判断“这个查询返回的行是不是集中在少数页面”从而动态决定要不要预取、预取多少页。这些方向都还在演进中但方向非常明确跟 DBA 日常调优直接相关——你今天理解的预取逻辑明年可能就写进新版本的核心路径里了。提醒改effective_io_concurrency不需要重启可以按会话或事务临时设置。建议先在备库或者测试环境用真实业务 SQL 跑一轮对比别直接拿生产环境试参数尤其是你不确定存储并发能力的时候。4. 日报之外WAL 与预取相关的几个延伸必知点4.1 WAL 归档和备份的关系以及对参数调优的启发看日报时很多人会被pg_waldump这个工具吸引但忽略了 WAL 在备份恢复体系里更大的作用。WAL 归档archive_mode / archive_command是持续备份的基础它把每次生成的 WAL 文件同步到远端存储这样你做完整基础备份之后只要持续归档 WAL就能在任何时间点恢复到最近状态。现场排查问题时归档文件也是救命稻草——pg_waldump直接读线上目录文件可能已经轮转走了但归档里一定还能找到。实践中我遇到过一种情况某团队把archive_command配在了本地磁盘没留意磁盘空间结果 WAL 文件归档速度跟不上生成速度archive 进程一直报错。后来用pg_waldump对比归档目录和在线目录的文件列表才发现问题根源是归档 IO 瓶颈而不是数据库本身有异常。这说明理解 WAL 生成与归档的节奏平时就要有意识地用工具去量化别等到报错再去猜。4.2 预取效果与内存配置的联动索引预取要想真正发挥威力光调effective_io_concurrency不够还得看shared_buffers的设置。如果缓存太小预取进去的页面马上就被挤出去等于白读如果缓存足够大预取的页面能存得住后续访问命中率大幅提升查询速度自然快。所以调优时预取并发度和shared_buffers应该组合起来考虑而不是孤立地改一个参数。具体做法上我会先用pg_buffercache扩展观察预取页面的命中率或者直接用一个返回大量行的查询反复执行几次对比执行计划里Buffers: shared hit/read的比例。如果 read 很高而 hit 很低说明缓存太小或预取策略没吃到缓存红利如果 hit 占比高且耗时有改善说明预取和缓存的搭配是健康的。用数据说话比凭经验改参数可靠得多。5. 常见问题与排查技巧实录5.1 我用 pg_waldump 踩过的几个具体报错和坑报错一“error: could not open file ... No such file or directory”。这种最常见的原因就是路径不对或者文件已被回收。解决办法先确认pg_wal目录下存在该文件如果不在去归档存储找如果归档里也没有那就只能接受现实看看是否可以从备库同步对应 WAL否则这一段日志无法追溯了。报错二解析出来的记录和实际业务对不上时间线对不上。这通常是因为同一个 LSN 在不同的时间线上有不同的含义尤其是做过恢复、standby promote 之后。处理方式用--timeline或者直接指定正确的 WAL 段文件确保解析的是目标时间线上的日志而不是旧时间线的残留。报错三工具版本和数据库版本不一致。有时候你机器上同时装了多个 PostgreSQL 版本顺手调用了默认 PATH 里的pg_waldump结果解析新版数据库的 WAL 时格式错乱。我现在的习惯是用绝对路径调用比如/usr/lib/postgresql/15/bin/pg_waldump或者先pg_config --bindir查清楚当前工具的版本再执行解析。5.2 预取调优后查询没变快可能原因清单我把这类问题整理成一个速查表方便大家对号入座现象可能原因解决思路参数调大后耗时没变化查询不是 IO 密集型而是 CPU 或网络瓶颈看执行计划确认耗时集中在哪个节点位图扫描读大量页面但命中率低shared_buffers 太小预取页面被挤出增大共享缓存并观察命中率机械盘上调高并发反而变慢寻道竞争导致 IO 效率下降调回低并发值比如 12查询返回行数很少但预取触发优化器估算误差实际选择率比预想低更新统计信息或用enable_bitmapscanoff对比建索引变慢而不是查询变慢maintenance_io_concurrency 未合理设置维护操作单独调参并验证效果排查这类问题我的经验是先看执行计划里有没有 Bitmap Heap Scan再看Buffers:那一行的命中率最后才是调整并发参数。顺序反了很可能折腾半天发现方向就不对。5.3 几条收藏级的实操建议善用EXPLAIN (ANALYZE, BUFFERS)任何涉及预取和 IO 的调优都应该以这个输出为基准而不是只看耗时。耗时受环境影响波动大但 buffer 命中率的变化能直接反映预取有没有生效。先测后改一次只改一个参数预取相关参数有effective_io_concurrency、maintenance_io_concurrency、shared_buffers如果同时改两三个出了问题根本分不清是谁导致的。用 swappy 环境或测试库做 WAL 解析练习别等生产事故才第一次用pg_waldump那样压力太大。可以先在本地环境造点数据、产生 WAL然后用工具把自己当成排障人员试着从日志中还原执行过的操作。最后再分享一个小技巧。每次看完类似的技术日报我都习惯随手把关键词记下来然后翻一下对应版本的官方文档和 release notes。原因很简单日报标题是线索真正的知识体系还是在文档和源码里。pg_waldump的完整选项、预取参数的精确行为文档写得明明白白远比二手转述可靠。把日报当作索引去读原版文档和实验验证收获会比只看日报本身大得多。这也是我这个做了多年 PostgreSQL 运维的从业者最想给看日报的朋友们的一句真心话。