CoreRanch
返回文章列表

PostgreSQL 18 异步 I/O 调优手册:io_method、io_workers 与 effective_io_concurrency 的真实默认值

2026-11-09 #数据库

一句话结论:PG 18 的异步 I/O 不是一个开关,而是四个参数 + 一个编译选项的组合。90% 的"我改了没效果"来自:io_combine_limit 被 io_max_combine_limit 静默压回 128kB、io_method 需要重启、以及实例根本没编译 liburing。

0. 先把默认值摆出来

下表全部来自 PostgreSQL 18.6 官方文档 19.4 节(Resource Consumption),检索时间 2026-11-09[1]。"生效方式"一列尤其重要——标"重启"的参数用 ALTER SYSTEM + pg_ctl reload 是不会生效的。

参数默认值取值范围 / 说明生效方式
io_methodworkerworker / io_uring(需 --with-liburing 或 meson -Dliburing 编译)/ sync仅重启
io_workers3仅当 io_method = worker 时有效改配置文件 / 命令行
io_max_concurrency-1-1 表示按 shared_buffers 与最大进程数推导,上限 64仅重启
io_combine_limit128kB合并 I/O 的最大尺寸;设得比 io_max_combine_limit 高会被静默取低值可随时改
io_max_combine_limit128kB硬上限;Unix 通常可到 1MB,Windows 128kB仅重启
effective_io_concurrency161–1000,0 表示禁用异步 I/O 请求可随时改
maintenance_io_concurrency16语义同上,用于代表多个会话的维护操作可随时改
backend_flush_after00–2MB,单后端写满该量后强制下刷可随时改
bgwriter_flush_afterLinux 512kB,其他平台 00–2MB可随时改

两个数字值得单独记住:

  • io_combine_limit 默认 128kB = 16 个 8kB 页。这意味着默认配置下,一次合并 I/O 最多覆盖 16 个数据块。
  • io_max_concurrency 的推导上限是 64。文档明确它由 shared_buffers、max_connections、autovacuum_worker_slots、max_worker_processes、max_wal_senders 共同决定,但不超过 64[1]。

1. 三种 io_method 的取舍

io_method 只有三个值,但选错会浪费整台机器的 IO 能力。

worker(默认)

用后台 worker 进程执行异步 I/O。优点是不需要特殊编译,在任何发行版包、任何容器镜像里都能用;代价是多一层进程间调度。io_workers 默认 3,意味着默认只有 3 个进程在替你提交异步 I/O。

适用:无法重新编译 PG 的生产环境、IO 队列深度有限的云盘、容器内受限的 seccomp 环境。

io_uring

直接走 Linux io_uring,省掉 worker 进程的调度开销。前提是编译时启用 liburing,否则该值不可用。

判断你的实例是否支持:

# 1) 看编译选项里有没有 liburing
pg_config --configure | tr ' ' '\n' | grep -i liburing

# 2) 运行时确认当前值(io_method 是重启参数,SHOW 看到的是已加载值)
psql -Atc "SHOW io_method;"

如果 pg_config 没有输出,说明当前二进制不带 io_uring 支持——此时把它改成 io_uring 会导致启动失败或不可用,不要在生产上试。

适用:自建、可控内核版本(io_uring 对内核版本敏感)、本地 NVMe。

sync

把所有"本可异步"的 I/O 同步执行。这不等于关闭 AIO 框架,而是显式退化为同步,用于对照实验:先跑 sync 建立基线,再切 worker/io_uring,你才能量化改动收益。这是本文最推荐的用法。

2. 参数之间的真实耦合关系

很多团队把 effective_io_concurrency 从 16 拉到 200 就以为完事,实际上有四个耦合点:

2.1 io_combine_limit 被 io_max_combine_limit 压住

文档原文的意思是:io_combine_limit 若高于 io_max_combine_limit,较低的那个会被静默使用,两者可能都需要抬高[1]。而 io_max_combine_limit 只能启动时设置。

所以想真正把合并 I/O 从 128kB 提到 1MB,必须:

# postgresql.conf —— 注意顺序:先把硬上限打开,再设软上限
io_max_combine_limit = 1MB     # 仅启动时生效
io_combine_limit     = 1MB     # 可随时改

然后再重启。

实测修正(2026-11-09,PostgreSQL 18.6):只改第二行时,pg_settings 里 io_combine_limit 会直接显示 128 个 8kB 块(即 1MB),source = configuration file,看起来完全生效;与此同时 io_max_combine_limit 仍是默认的 16 块(128kB)。也就是说,文档所说的"被静默取低值"发生在运行时的 I/O 合并逻辑里,配置视图不会给你任何提示。这比报错更危险:你没有可观测信号,只能靠"两个参数都设"来保证。

2.2 effective_io_concurrency 不是越大越好

官方明确:过高的值可能增加所有查询的 I/O 延迟[1]。它是"我预期存储能同时处理多少个并发 I/O"的声明,不是"我想要多快"的旋钮。判断依据应该是存储的实际队列深度,而不是 CPU 核数。

一个可操作的起点:

存储类型建议起点理由
单块 SATA SSD / 网络盘(gp2 类)16(默认)队列深度有限,抬高只会排队
本地 NVMe64–128高 IOPS、深队列
多盘 RAID / 分布式块存储128–256后端聚合队列更深
冷数据、对象存储网关0–4延迟主导,并发无意义

2.3 io_workers 默认 3,可能不够

io_workers 只在 worker 模式下有效。3 个进程对单机小库够用;当 effective_io_concurrency 提到 64 以上、且并发查询很多时,提交侧会成为瓶颈。经验做法是 min(CPU 核数, effective_io_concurrency / 16),从 4–8 之间试。

2.4 异步 I/O 变快之后,瓶颈会立刻转移到 work_mem

这是最容易被误判的连锁反应:I/O 延迟下降后,原本"慢在磁盘"的排序/哈希开始"慢在落盘临时文件"。相关默认值[1]:

参数默认值说明
work_mem4MB每个排序/哈希操作的上限,不是每会话总量
hash_mem_multiplier2.0哈希类操作可用的上限 = work_mem × 该值
maintenance_work_mem64MBVACUUM/CREATE INDEX 等
autovacuum_work_mem-1-1 表示复用 maintenance_work_mem
vacuum_buffer_usage_limit2MBVACUUM/ANALYZE 的缓冲策略上限,范围 128kB–16GB,且会被静默压到 shared_buffers 的 1/8

work_mem 的风险在于乘法:一个复杂查询可能同时有多个排序/哈希节点,每个节点各自可用满 work_mem;再加上并发会话数,实际内存是 work_mem × 节点数 × 并发数。文档对 hash_mem_multiplier 的建议很具体:当 work_mem 已提到 40MB 以上时,2.0–8.0 甚至更高可能有效[1]。

3. 一套可直接套用的配置

假设:16 vCPU / 64GB 内存 / 本地 NVMe / 专用于数据库。

# ---- 内存基线 ----
shared_buffers = 16GB            # 25% 起点;超过 40% 通常不会更好[1]
work_mem = 8MB                   # 从 4MB 提到 8MB,观察 temp_blks_written
hash_mem_multiplier = 2.0        # work_mem 还没到 40MB,先不动
maintenance_work_mem = 1GB
autovacuum_work_mem = 512MB
vacuum_buffer_usage_limit = 256MB

# ---- 异步 I/O ----
io_method = worker               # 无 liburing 编译时的正确选择
io_workers = 8
io_max_combine_limit = 1MB       # 仅启动时生效
io_combine_limit = 1MB
effective_io_concurrency = 64
maintenance_io_concurrency = 64

# ---- 下刷节奏(降低 checkpoint 尾部 fsync 抖动)----
bgwriter_flush_after = 512kB     # Linux 默认值,保持不变
backend_flush_after = 0          # 默认,不开

内存预算校验(务必自己算一遍):

shared_buffers            16 GB
maintenance_work_mem       1 GB   (并发维护操作时各自独立)
autovacuum 最多          3 × 512MB = 1.5 GB(受 autovacuum_max_workers 限制)
work_mem 峰值   8MB × 4 节点 × 100 并发 = 3.2 GB
------------------------------------------------
峰值估算                  ≈ 22 GB,留出 OS 页缓存余量

autovacuum_max_workers 的默认值文档未在本节列出(在 19.10 节),计算时请以实例实际值为准——这里用 3 只是示例。

4. 生效与验证:三个必须检查的点

4.1 哪些改动还没生效

-- 列出所有"已写入配置但需重启"的参数
SELECT name, setting, pending_restart
FROM pg_settings
WHERE pending_restart
ORDER BY name;

pending_restart = true 的行就是"你改了但还没生效"的参数。io_method、io_max_concurrency、io_max_combine_limit 都会出现在这里。

4.2 确认合并 I/O 真的抬起来了

SELECT name, setting, unit, source
FROM pg_settings
WHERE name IN ('io_method','io_workers','io_combine_limit',
               'io_max_combine_limit','io_max_concurrency',
               'effective_io_concurrency');

看到 io_combine_limit = 1MB 不等于生效。实测中把它设为 1MB 后,pg_settings 就显示 1MB(setting=128、unit=8kB),但 io_max_combine_limit 仍是 128kB,运行时按较小的值执行——配置视图不会告诉你这件事。所以这一节的检查必须两行一起看,回到 2.1 节。

4.3 用 pg_stat_io 观察,而不是凭感觉

-- 按后端类型看读写与 fsync 分布
SELECT backend_type,
       sum(reads)     AS reads,
       sum(writes)    AS writes,
       sum(fsyncs)    AS fsyncs
FROM pg_stat_io
GROUP BY backend_type
ORDER BY reads DESC;

调整前后的对比方法是:先记录一次快照,跑同一组查询,再记录一次;关注 checkpointer 与 client backend 的 fsyncs 比例变化,以及 client backend 的 reads 是否下降(说明预读/合并生效)。

5. 五个常见反模式

反模式结果正确做法
只设 io_combine_limit = 1MBpg_settings 显示 1MB,实际合并上限仍是 128kB,配置视图无提示同时抬 io_max_combine_limit 并重启
容器里直接设 io_method = io_uring启动失败:FATAL: could not setup io_uring queue: Operation not permitted宿主机允许 io_uring 的前提下加 --security-opt seccomp=unconfined
effective_io_concurrency = 1000全库 I/O 延迟上升[1]按存储队列深度设,NVMe 64–128 起步
用 ALTER SYSTEM 改 io_method 后只 reload没生效,显示 pending_restart需要重启,排进维护窗口
shared_buffers = 70%挤压 OS 页缓存,通常更慢[1]25% 起,最多 40%

6. 本机实测记录(2026-11-09)

以上所有默认值都不是抄文档——我在本机把 PostgreSQL 18.6 跑起来逐条读了 pg_settings,并做了三组对照。

6.1 环境

项值
宿主机Windows 11 + Docker Desktop 29.4.3(WSL2 后端)
镜像 / 版本postgres:18 → PostgreSQL 18.6 (Debian 18.6-1.pgdg13+2), x86_64
编译选项含 --with-liburing(Debian 官方包默认启用)
容器资源32 vCPU / 31.9 GiB(nproc、free -m 实测)、shared_buffers=256MB、max_connections=200、fsync=on
数据集实验一:pgbench -i -s 20 → 307MB / 200 万行;实验二:-s 400 → 5989MB / 4000 万行(shared_buffers 的 47 倍)
测量时间2026-11-09 22:40–23:00(Asia/Shanghai)

局限先说明:WSL2 的虚拟磁盘、单机容器、8 个 pgbench 客户端,都使绝对吞吐不可外推到生产。下面只用来验证行为(参数是否生效、失败信息长什么样)与方法(怎么量),不作为性能承诺。

6.2 出厂值实测(与文档逐条一致)

io_method = worker | io_workers = 3 | io_combine_limit = 16 (8kB) = 128kB
io_max_combine_limit = 16 (8kB) = 128kB | io_max_concurrency = 64
effective_io_concurrency = 16 | shared_buffers = 16384 (8kB) = 128MB
work_mem = 4096kB | maintenance_work_mem = 65536kB | max_connections = 100

注意 io_max_concurrency 实测就是 64——文档说这个推导值"不超过 64",在 32 vCPU 的容器里直接顶到了上限。

6.3 三个被实测确认的行为

① pending_restart 是唯一可靠的"还没生效"信号

ALTER SYSTEM SET io_method = 'io_uring';
SELECT pg_reload_conf();

实测结果:

 name      | setting | pending_restart
-----------+---------+-----------------
 io_method | worker  | t

setting 仍是 worker(旧值继续服务),pending_restart = t 才告诉你"要重启"。只看 SHOW io_method 会误以为配置没写进去。

② 容器里 io_uring 的真实拦路虎是 seccomp,不是编译选项

把 io_method 设为 io_uring 并重启后,容器直接起不来:

FATAL:  could not setup io_uring queue: Operation not permitted
HINT:  Check if io_uring is disabled via /proc/sys/kernel/io_uring_disabled.

逐步排查的结果:

检查结果
镜像编译选项有 --with-liburing ✅
宿主机 cat /proc/sys/kernel/io_uring_disabled0(内核未禁用)✅
默认 Docker seccomp 配置拦截 io_uring_setup ❌
加 --security-opt seccomp=unconfined 后重启启动成功,SHOW io_method = io_uring ✅

这条对"在 Kubernetes 里用 io_uring"同样成立:runtime 的 seccomp profile 必须在允许列表里放开 io_uring 系统调用,否则改 GUC 只会换来一个起不来的实例。

③ 三种 io_method 在缓存命中率高时量不出差别

同一份数据、同一容器配置,只改 io_method,每次重启后先热身 8 秒再测 20 秒:

io_method读写混合 tps平均延迟只读 tps只读平均延迟
worker(默认)1364.05.87 ms94212.70.085 ms
sync(同步退化)1372.55.83 ms93875.80.085 ms
io_uring1370.55.84 ms88467.60.090 ms

差异落在 ±1% 以内(只读那一列 io_uring 反而低 6%),在噪声范围内。

这不是"参数没用",而是这个场景根本没让异步 I/O 成为瓶颈:

  • 32 vCPU 的容器里只用了 8 个 pgbench 客户端,CPU 与块设备队列都没被压满——测量的是"空闲机器上的参数差异",不是"瓶颈处的参数差异";
  • 307MB 数据集 + 256MB shared_buffers,热点几乎全在内存里,只读命中率接近 100%;
  • WSL2 虚拟磁盘的延迟特征与真实 NVMe 不同。

这一点必须说透:在 32 核机器上用 8 个客户端测得"三种 io_method 没差别",正确解读是"没有可测的瓶颈",而不是"io_method 无效"。 那到底什么样的负载才有差别?我把数据集放大到 47 倍 shared_buffers 又测了一轮——见 6.4。而且真正起作用的不是"数据有多大",而是"访问模式是不是顺序的"。

6.4 换成顺序扫描:差异出现了,而且"调大"反而变慢

第二组实验把数据集做到 shared_buffers 的 47 倍:

项值
数据pgbench -i -s 400 → 5989MB 库,pgbench_accounts 5124MB / 4000 万行
shared_buffers128MB(数据量 ≈ 47 倍)
负载SELECT sum(abalance) FROM pgbench_accounts(走并行顺序扫描)
冷启动控制每次扫描前 sync; echo 3 > /proc/sys/vm/drop_caches

第一轮(顺序:sync → worker@16 → worker@256 → io_uring@256,各 3 次扫描):

配置3 次扫描(ms)
sync / eioc=161889(冷启动异常)· 728 · 742
worker / eioc=16478 · 483 · 483
worker / eioc=256527 · 498 · 494
io_uring / eioc=256749 · 724 · 763

第二轮(反序执行以抵消缓存漂移,各 4 次扫描):

配置4 次扫描(ms)中位数
io_uring / eioc=256744 · 736 · 708 · 725725
worker / eioc=16464 · 471 · 475 · 468470
worker / eioc=256 + io_combine_limit=1MB847 · 829 · 825 · 817827
sync / eioc=16706 · 719 · 709 · 705707

两个独立轮次、相反执行顺序,得到同一组结论:

  1. sync 明显慢于 worker:707–742ms vs 470–483ms,约 1.5 倍。这是 io_method 唯一稳定可测的差异。
  2. io_uring 并不比 worker 快(725 vs 470)。别假设"更新的接口更快"——在这个环境里它更慢。
  3. 把 io_combine_limit 抬到 1MB 反而慢了 66%(827 vs 498,同为 eioc=256)。"合并更大→更快"的直觉在这里是错的。
  4. eioc 16 vs 256(combine 保持 128kB)没有显著差异:483 vs 498,在噪声内。

6.5 一个必须写下来的方法论警告

先做个算术:5124MB 的表,470ms 扫完 ≈ 10.9 GB/s。

这个数字不可能来自磁盘。原因是:drop_caches 在 WSL2 内部只能清虚拟机的页缓存,清不掉 Windows 宿主对 VHDX 文件本身的缓存。所以 6.4 那张表衡量的是"暖缓存下的 I/O 提交模型",不是磁盘延迟。

这条经验比参数本身更有用:当你的压测显示吞吐高得离谱(GB/s 级全表扫描),先怀疑缓存没清干净,而不是硬件超神。 能真正清掉宿主持久缓存的,只有"数据量远大于宿主内存"或"换一台干净机器"。

6.6 那到底该按什么顺序调

把两组实验合起来,得到一张可操作的判断表:

你的主要负载异步 I/O 是否在关键路径该做什么
点查(单页索引查找)、高并发短事务否(单页查找几乎不产生异步 I/O)别调 io_method,去调 shared_buffers、索引、连接池
大表顺序扫描 / 报表聚合是io_method=worker(实测比 sync 快约 1.5 倍)
VACUUM / CREATE INDEX / 大批量导入是同上,并关注 maintenance_io_concurrency
数据全在内存里(工作集 < shared_buffers)否调 I/O 参数没有收益,属于常见误区

再加上一条纪律:任何"调大"(eioc、io_combine_limit、io_workers)都必须用你自己的负载实测。本次实测里,"调大"两项都变慢了。

7. 三行结论

  1. 先判断异步 I/O 是否在你的关键路径上:点查型负载调 io_method 毫无收益(实测差异在噪声内);大表顺序扫描才有差别(worker 比 sync 快约 1.5 倍)。
  2. io_combine_limit 想生效必须同时抬 io_max_combine_limit,而且这两个值调大之后可能更慢(实测 combine=1MB 让扫描慢了 66%)——"更大更快"是错觉。
  3. 每次改动都用 pg_settings.pending_restart 确认已生效,用 pg_stat_io 与同条件的 A/B 扫描量化;看到 GB/s 级全表扫描时,先怀疑缓存没清干净。

附录:数据来源与检索时间

检索时间统一为 2026-11-09(Asia/Shanghai),抓取对象为 PostgreSQL 18.6 在线文档。

编号来源URL本文引用的具体数据
[1]PostgreSQL 18 文档《19.4 Resource Consumption》https://www.postgresql.org/docs/18/runtime-config-resource.htmlshared_buffers 默认 128MB、25% 起始、>40% 通常无益;work_mem 4MB;hash_mem_multiplier 2.0 与 40MB/2.0–8.0 建议;maintenance_work_mem 64MB;autovacuum_work_mem -1;vacuum_buffer_usage_limit 2MB/128kB–16GB/1/8 上限;effective_io_concurrency 默认 16、范围 1–1000、0 禁用、"过高会增加所有查询 I/O 延迟";maintenance_io_concurrency 16;io_combine_limit 128kB;io_max_combine_limit 128kB 且 Unix 上限 1MB、Windows 128kB、仅启动生效;io_max_concurrency -1/上限 64/仅启动;io_method worker·io_uring(需 liburing 编译)·sync,默认 worker、仅启动生效;io_workers 3;backend_flush_after 0;bgwriter_flush_after Linux 512kB
[2]PostgreSQL 18 文档《pg_upgrade》https://www.postgresql.org/docs/18/pgupgrade.html用于交叉确认 18.6 为当前受支持 minor(升级目标版本)

核验说明:参数名、默认值、取值范围、"仅启动生效"的约束、以及"静默取低值"的行为均逐条比对官方文档(2026-11-09)。

第 6 节为本机实测(PostgreSQL 18.6,Docker/WSL2),实测与文档有两处差异,已在正文标出:

  1. 文档说 io_combine_limit 高于 io_max_combine_limit 时"较低的会被静默使用"——实测 pg_settings 显示的是你设置的值(1MB),被压回 128kB 只发生在运行时,配置视图无任何提示;
  2. 文档说 io_uring 需 --with-liburing 编译——实测真正的拦路虎通常是容器 runtime 的 seccomp,报错为 could not setup io_uring queue: Operation not permitted。

实测的绝对吞吐数字仅代表本机 WSL2 容器环境,不可外推。第 6 节包含两组实验,结论方向相反但都重要:

  • 6.3:小工作集 + 点查 → 三种 io_method 差异在噪声内(没有可测瓶颈,不等于参数无效);
  • 6.4:大工作集(47× shared_buffers)+ 顺序扫描 → worker 比 sync 快约 1.5 倍,但 io_combine_limit=1MB 反而慢 66%;
  • 6.5:同时记录了本机的一个测量陷阱——drop_caches 清不掉 Windows 宿主对 VHDX 的缓存,导致全表扫描出现 10.9 GB/s 的非磁盘速度。

第 3 节的配置为基于官方默认值与文档建议推导的起点配置,实际取值必须用第 4 节的验证方法与 6.6 的判断表在目标环境确认。