编程 PostgreSQL 19 异步 I/O 调优:删掉手工 io_workers,让 worker 池自己伸缩

2026-10-23 21:03:32

PostgreSQL 19 异步 I/O 调优:删掉手工 io_workers,让 worker 池自己伸缩

PostgreSQL 18 把异步 I/O 引入主线。Linux 上主要使用 io_uring,其他环境回落到由 io_method=worker 控制的 worker 池,也可以设成 sync 保持旧行为。pganalyze、Aiven、Better Stack 当时的基准都显示,读密集、大顺序扫描的场景有实际收益,同时也暴露了 worker 回落路径的问题:默认 worker 数量在扫描打满时不够用,调大之后小查询又变慢。

PostgreSQL 19 从两个方向处理这件事:worker 池能自己调自己,I/O 层的行为也能在执行计划里直接看到。

参考:

18 上手工调的 io_workers,在 19 应该删掉

在 18 上,io_method=worker 的 worker 数量是个静态值。它只能对应某一种负载形态:为宽表全扫调到够用,小查询的延迟就会被拉高;为点查压低,大扫描又等不到足够的 I/O 并发。所谓“调好了”,其实只是调成了某个特定场景下的折中。

19 里这套静态旋钮不再是多数负载的正确设置。如果之前在 18 上手工设过 worker 数,做法是把它取消,直接在 19 的默认值上重新跑基准。原本选出的数字只对当时的负载形状成立,而自伸缩覆盖的范围更宽。

四个新 GUC 与上下限的设

io_method=worker 在 19 新增四个参数:

  • io_min_workers:池的下限
  • io_max_workers:池的上限
  • io_worker_idle_timeout:空闲 worker 在被回收前保留多久
  • io_worker_launch_interval:有压力时新增 worker 的速度

四个参数合起来就是一个 autoscaler:查询真的在等 I/O 时扩容,不等时缩回去。

需要人为决定的只剩边界。上限仍然是硬约束,io_max_workers 设到 32 不会让八核机器变快,只会让上下文切换和内存占用上升;下限决定的是空闲期过后还能立刻接住多少个并发 I/O 请求,设得过高等于长期养着一批闲着的工作进程。稳妥的起点是保留默认值,先只观察,再给上限加一个与核数同量级的封顶,下限保持在低位。

大请求的 read-ahead 按扫描规模伸缩

read-ahead 调度器针对大 I/O 请求做了重写。改动点很具体:不再无论请求大小都发出固定数量的预读请求,而是让预取窗口随实际扫描规模伸缩。受益的是宽表扫描,以及每个索引项要抓取大量 heap 页的 index range scan。

预期是:18 上异步 I/O 本来就有收益的负载继续拿到收益,18 上刚好没能打平的负载开始出现新增收益。

用 EXPLAIN (ANALYZE, IO) 验证

EXPLAIN ANALYZE 增加了 IO 选项,按计划节点报告异步 I/O 活动:发起的读(reads issued)、完成的读(reads completed)、传输字节数(bytes)。这是 19 这轮异步 I/O 改动里最有运维价值的一项:在 19 之前,怀疑某个计划比预期慢,只能把 pg_stat_io 的输出和计划手工对照;现在这些信息就在计划里。

用法:

EXPLAIN (ANALYZE, IO)
SELECT ... FROM big_table WHERE ...;

对 18 上异步 I/O 表现不稳定的那类负载尤其应该打开它。发起读和完成读之间的差距、以及 bytes 的分布,能直接说明预取的 I/O 是否真的与执行重叠,还是执行器在某个节点上被迫串行等待。

EXPLAIN ANALYZE 的开销取舍

EXPLAIN ANALYZE 会真正执行语句,并逐节点收集计时与计数,IO 选项只是在这些节点统计里再增加一组异步 I/O 计数器,因此开销主体来自 ANALYZE 本身的 instrumentation,并不来自 IO 选项。查询越复杂、节点越多,逐节点统计带来的额外开销越明显。

取舍在于:对已经确认慢的读密集查询,值得完整跑一次 EXPLAIN (ANALYZE, IO) 拿到 per-node 的图景;对高频、低延迟的语句,不要在生产路径上常态化开启,采样或换到影子实例上执行更合适。只想看计划形状而不想执行时,用不带 ANALYZE 的 EXPLAIN,但那样就没有 I/O 计数。

pg_stat_io 补什么

EXPLAIN (ANALYZE, IO) 给的是单次执行、单个节点的明细;pg_stat_io 仍然负责另一层:按 backend type、object、context 维度累计的实例级读数。两者不是替代关系——计划里的计数回答“这条查询在这个节点上拿到了多少异步 I/O”,pg_stat_io 回答“整个实例的读是否真的走了异步路径、哪类后台进程在产生 I/O”。排查时先用 pg_stat_io 确认异步确实在工作,再用 EXPLAIN (ANALYZE, IO) 定位到具体节点。

19 中与 I/O 相关的发布项

  • 改进大请求的异步 I/O read-ahead 调度(Andres Freund)
  • 允许 io_method=worker 自动控制所需的 background worker(Thomas Munro),新增 io_min_workers、io_max_workers、io_worker_idle_timeout、io_worker_launch_interval
  • 新增 EXPLAIN ANALYZE 的 IO 选项,报告异步 I/O 活动(Tomas Vondra)
  • EXPLAIN (ANALYZE, WAL) 输出中增加 WAL full-page write 字节数
  • 允许查询中的表扫描把页标记为 all-visible 写入 visibility map
  • 多项性能改进,包括 I/O worker 进程数的自动伸缩、更快的外键检查
  • 新增 REPACK 命令,REPACK (CONCURRENTLY) 可在不阻塞读写的情况下重整
  • wal_level=replica 时逻辑复制可不重启启用
  • standby 上通过新的 WAIT 命令支持 read-your-writes 一致性
  • 表索引并行 autovacuum,以及基于新评分系统的 autovacuum 优先级

18 的 AIO 基线

回头对照 18 的实现:io_method 在 worker、io_uring 与 sync 之间切换;io_combine_limit 和 io_max_combine_limit 控制请求合并;pg_aios 展示用于异步 I/O 的文件句柄;effective_io_concurrency 与 maintenance_io_concurrency 的默认值提高到 16。

19 的调整建立在这套基线上。落地动作只有三步:取消 18 上手工设的 worker 数,只把 io_min_workers / io_max_workers 当作下限和上限,下次遇到慢的读密集计划时打开 EXPLAIN (ANALYZE, IO)。

复制全文 生成海报 PostgreSQL 数据库 性能调优

推荐文章

程序员茄子在线接单