PostgreSQL 18 异步 I/O 调优手册:io_method、io_workers 与 effective_io_concurrency 的真实默认值
一句话结论: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_method | worker | worker / io_uring(需 --with-liburing 或 meson -Dliburing 编译)/ sync | 仅重启 |
io_workers | 3 | 仅当 io_method = worker 时有效 | 改配置文件 / 命令行 |
io_max_concurrency | -1 | -1 表示按 shared_buffers 与最大进程数推导,上限 64 | 仅重启 |
io_combine_limit | 128kB | 合并 I/O 的最大尺寸;设得比 io_max_combine_limit 高会被静默取低值 | 可随时改 |
io_max_combine_limit | 128kB | 硬上限;Unix 通常可到 1MB,Windows 128kB | 仅重启 |
effective_io_concurrency | 16 | 1–1000,0 表示禁用异步 I/O 请求 | 可随时改 |
maintenance_io_concurrency | 16 | 语义同上,用于代表多个会话的维护操作 | 可随时改 |
backend_flush_after | 0 | 0–2MB,单后端写满该量后强制下刷 | 可随时改 |
bgwriter_flush_after | Linux 512kB,其他平台 0 | 0–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(默认) | 队列深度有限,抬高只会排队 |
| 本地 NVMe | 64–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_mem | 4MB | 每个排序/哈希操作的上限,不是每会话总量 |
hash_mem_multiplier | 2.0 | 哈希类操作可用的上限 = work_mem × 该值 |
maintenance_work_mem | 64MB | VACUUM/CREATE INDEX 等 |
autovacuum_work_mem | -1 | -1 表示复用 maintenance_work_mem |
vacuum_buffer_usage_limit | 2MB | VACUUM/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 = 1MB | pg_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_disabled | 0(内核未禁用)✅ |
| 默认 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.0 | 5.87 ms | 94212.7 | 0.085 ms |
sync(同步退化) | 1372.5 | 5.83 ms | 93875.8 | 0.085 ms |
io_uring | 1370.5 | 5.84 ms | 88467.6 | 0.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_buffers | 128MB(数据量 ≈ 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=16 | 1889(冷启动异常)· 728 · 742 |
worker / eioc=16 | 478 · 483 · 483 |
worker / eioc=256 | 527 · 498 · 494 |
io_uring / eioc=256 | 749 · 724 · 763 |
第二轮(反序执行以抵消缓存漂移,各 4 次扫描):
| 配置 | 4 次扫描(ms) | 中位数 |
|---|---|---|
io_uring / eioc=256 | 744 · 736 · 708 · 725 | 725 |
worker / eioc=16 | 464 · 471 · 475 · 468 | 470 |
worker / eioc=256 + io_combine_limit=1MB | 847 · 829 · 825 · 817 | 827 |
sync / eioc=16 | 706 · 719 · 709 · 705 | 707 |
两个独立轮次、相反执行顺序,得到同一组结论:
sync明显慢于worker:707–742ms vs 470–483ms,约 1.5 倍。这是io_method唯一稳定可测的差异。io_uring并不比worker快(725 vs 470)。别假设"更新的接口更快"——在这个环境里它更慢。- 把
io_combine_limit抬到 1MB 反而慢了 66%(827 vs 498,同为eioc=256)。"合并更大→更快"的直觉在这里是错的。 eioc16 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. 三行结论
- 先判断异步 I/O 是否在你的关键路径上:点查型负载调
io_method毫无收益(实测差异在噪声内);大表顺序扫描才有差别(worker比sync快约 1.5 倍)。 io_combine_limit想生效必须同时抬io_max_combine_limit,而且这两个值调大之后可能更慢(实测 combine=1MB 让扫描慢了 66%)——"更大更快"是错觉。- 每次改动都用
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.html | shared_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),实测与文档有两处差异,已在正文标出:
- 文档说
io_combine_limit高于io_max_combine_limit时"较低的会被静默使用"——实测pg_settings显示的是你设置的值(1MB),被压回 128kB 只发生在运行时,配置视图无任何提示; - 文档说
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 的判断表在目标环境确认。