资讯动态

PostgreSQL 18 io_method 深度解析:用 strace 验证异步 I/O 真实效果

发布时间:2026/10/16 12:22:40 来源:尧图企业网站定制
1. 先说结论io_method 到底管什么PostgreSQL 18 开发周期里最让我这类做运维的人眼前一亮的不是某个 SQL 语法糖而是这个看起来有点黑话的 GUCio_method。简单说它决定 PostgreSQL 能不能以及用哪种方式把 I/O 交给内核异步执行。在 PG18 之前阻塞式 pread/pwrite 是绝对主流从 PG18 开始执行器里的大部分预读、顺序扫描、位图堆扫描相关 I/O 都可以换成异步接口。这套机制在社区里讨论了挺久落到参数上我手上这个版本里主要有三个值默认的 worker、Linux 上直通内核的 io_uring以及什么都不做的 none。我第一次看到这个参数时的反应跟大多数人一样又一个玄学调优项文档就一两句话算法细节藏在源码深处。但好在 Linux 上有一个永远诚实的观察手段——strace。把系统调用摊开看I/O 到底是同步阻塞、worker 模拟异步还是真正进了 io_uring一眼就能分清楚比任何监控工具都直观。这篇文章不打算讲调了它性能提升百分之多少这种结论而是带你把 io_method 在 strace 下的各类行为看明白。看完你至少能回答三个问题io_method 到底生效没有不同取值下后端进程在系统调用层面分别做了什么生产环境里要怎么验证、怎么决策。适合对 PostgreSQL 内部机制好奇的 DBA、刚接手 PG18 实例的运维以及想排查慢查询却搞不清楚 I/O 子系统的人。1.1 为什么第一眼就觉得很玄学原因很简单io_method 不像 shared_buffers 或 max_connections 那样调大调小有非常直观的效果。它是一个开关型参数打开之后性能未必立刻变好数据量小的时候甚至完全看不出差别。它改变的是内核态和用户态的协作方式选了 io_uring后端进程不再自己调用 pread 去傻等而是把一批读取请求塞进内核的提交队列等数据准备好了再从完成队列里取结果。整个过程对 DBA 来说几乎是黑盒很难根据 pg_stat 里的数字判断它到底干活没有。再加上它在旧内核、容器、受限安全策略下还可能静默失效或者只在某些 I/O 路径上生效。如果不顺着进程一级一级往下看光靠参数值和性能监控曲线确实只能靠猜。这也是我坚持用 strace 的原因——系统调用是用户态能看到的最后一层所有优化和回退最终都会诚实地反映在这一层上做不了假。1.2 同步与异步最朴素的区别先补一点背景免得后面看代码发懵。同步 I/O 的模型是进程发起一次 read内核把数据放到用户缓冲区之后这次调用才返回。中间进程要么阻塞在不可中断睡眠状态要么反复重试总之这个线程在这段时间里没法干别的事。异步 I/O 的模型则是进程把我要读这些块这个请求交给内核内核立即返回进程继续做自己的事等数据准备好了内核再通过某种机制通知进程来取结果。异步不是为了把单次 I/O 变快而是为了让 CPU 不再干等磁盘。PostgreSQL 过去对同步 I/O 做了大量工程优化比如通过位图扫描一次取一堆 tid、按物理顺序预读、把多个请求尽量合并。但底层仍然是一个后端进程发一堆阻塞读发一个等一个。io_method 就是想在这一层改变交互方式。用生活化的例子说同步 I/O 是你去柜台办业务一个一个窗口排队办完一个再排下一个异步 I/O 是你把一摞材料一次性交进去拿个号去旁边坐着玩手机叫到你再过去拿结果。代价是你要多管理一个号牌收益是过程中你的时间没有被柜台占死。1.3 几个取值分别意味着什么取值原理适用场景none完全不启用异步 I/O回到并发预读但同步等待的老路调试对照、最小化依赖、不确定环境因素时兜底worker后端进程把 I/O 请求打包交给辅助 worker 进程由 worker 去执行阻塞读再通过跨进程通信传回结果兼容性优先内核版本低或容器权限受限io_uring直接使用 Linux io_uring 这套内核异步接口请求进提交队列、完成进完成队列Linux 新内核、NVMe/SSD、高并发预读场景需要提醒的是不同发行版、不同编译参数下GUC 的取值名称和默认值可能略有差异。有的编译版本默认值是 worker有的是 none。你拿到实例后先执行SHOW io_method;确认再去查对应版本的参数说明。别把别人的默认值当成自己的默认值这是个很常见的认知坑。2. 实验环境怎么把一个带 io_uring 的 PG18 编译跑起来要做这种内核级实验最稳的不是依赖某个现成发行版而是自己编译一个带 liburing 的 PostgreSQL 18这样才能确认 io_method 的选项真的存在而不是被编译期开关剪掉了。下面是我实际准备流程每一条都可以直接照抄。2.1 先检查宿主机到底支持不支持io_uring 对内核版本有要求。Linux 5.1 开始有基本能力5.10 之后进入比较可用的状态越新越省心。我建议至少 5.10 以上6.x 更好。检查方法很简单uname -r grep io_uring /proc/filesystems如果/proc/filesystems里没有 io_uring 这一行说明当前内核编译时根本没带这个模块后面就不用白折腾了。如果你在容器里跑先别急着怪 PostgreSQL先看grep Seccomp /proc/self/statusSeccomp 为 2 的容器可能把某些新系统调用拦住。这个环境因素在后面的排查清单里还会碰到先把底摸清免得实验做到一半被误导。2.2 编译 PG18 时指定 --with-liburing编译时最关键的是--with-liburing。这个开关会去检查系统的 liburing 开发包把 io_uring 的封装库链接进 PostgreSQL。没有它即使内核支持PG 也不会暴露 io_uring 路径。我在源码目录里执行的 configure 大概是这样的./configure --prefix/opt/pg18 \ --with-liburing \ --with-icu \ --with-pgport5432 \ --enable-debugconfigure 结束前留意这一段输出出现checking for liburing.h... yes才算真正找到。如果显示 no装开发包Debian/Ubuntu 是liburing-devRHEL 系是liburing-devel。没装对的话后续 make 不会报致命错只是io_methodio_uring不可选等你想切的时候才发现就晚了。编译安装完后用pg_config或者直接起实例执行SHOW io_method; SHOW data_directory;再对应看ldd输出是否包含 liburing。到这一步实验材料就齐了。2.3 准备一张能产生大量 I/O 的表为了能明显看出异步与同步的差异测试表不能太小最好几百 MB 以上。我用一条 SQL 生成大约两千万行CREATE TABLE big_io AS SELECT generate_series(1,20000000) AS id, md5(random()::text) AS payload;然后给表加一个普通 btree 索引用于后面的位图堆扫描观察CREATE INDEX big_io_idx ON big_io(id);统计信息更新完确认执行计划能走到位图扫描路径。位图堆扫描是最容易触发批量预读的场景之一PostgreSQL 会一次性取一批 tid然后按块顺序读取非常适合观察异步 I/O 是否真的冒出来了。为了稳定执行计划我在每个实验里都会带上SET effective_io_concurrency 8; SET enable_indexscan off; SET enable_bitmapscan on;这个组合的意思是预读并发度固定为 8走位图路径。前面那个参数管的是预读量io_method 管的是预读通过什么系统调用发出去两者是配合关系后面会反复强调。3. 对照实验同步 I/O 在 strace 下到底是什么样实验之前必须先跑一次基线。基线就是 io_methodnone也就是完全关闭异步 I/O。这一步不是浪费时间后面判断 io_uring 是否生效时没有基线的对比很容易被零散的调用刷昏头。3.1 怎么把 strace 挂到 postmaster 和所有后端上PostgreSQL 是多进程架构跟踪单个 SQL 时至少要跟踪对应的 backend 进程。最简单的办法是直接跟踪 postmaster并且带上 -f 让它跟随 fork 出来的所有子进程mkdir -p /tmp/aio rm -f /tmp/aio/trace* strace -f -tt -T -yy \ -e tracepread64,preadv2,pwrite64,pwritev2,read,write,io_uring_setup,io_uring_enter,io_uring_register \ -o /tmp/aio/trace \ -p $(head -n1 $PGDATA/postmaster.pid)参数说明-f 表示跟踪子进程-tt 输出高精度时间戳-T 显示每个系统调用耗时-yy 在文件描述符后面标出对应文件路径-e trace 只保留和 I/O 相关的系统调用避免把 accept 和 epoll 刷爆日志。如果只想看 io_uring新版本 strace 还支持%io_uring这个分类符可以直接用。跑起来之后在另一个终端执行SET effective_io_concurrency 8; SET enable_indexscan off; SET enable_bitmapscan on; SELECT count(*) FROM big_io WHERE id BETWEEN 1 AND 10000000;统计结束后按 CtrlC 停掉 strace然后埋头翻日志。注意 strace 在高负载下会放大开销实验别跑太久能观察到几千次调用的模式就够了。3.2 同步模式下的特征一请求一等待打开 /tmp/aio/trace 文件看到的典型行是21034 13:15:23.123456 pread64(166/opt/pgdata/base/16386/16412, ..., 8192, 4096000) 8192 0.000083 21034 13:15:23.123600 pread64(166/opt/pgdata/base/16386/16412, ..., 8192, 4096001) 8192 0.000062 21034 13:15:23.123720 pread64(166/opt/pgdata/base/16386/16412, ..., 8192, 4096002) 8192 0.000090这个日志非常老实每一条记录都对应一次完整等待。第二条和第三条的时间差几乎等于磁盘返回的时间线程在这个过程中没有其他系统调用插进来。日志条数和预计要读的块数基本一一对应。也就是说这个模式下预读逻辑确实发出了多个请求但每个请求仍然是阻塞式等结果发得再快也逃不过发了就卡住的本质。从调度器角度看后端进程会出现明显的不可中断睡眠状态CPU 占用不高进程却好像被什么东西拖住了。遇到这种曲线经验不足的 DBA 会先怀疑锁等待其实锁在系统调用层根本藏不住一次性ps看进程状态加strace看调用栈几秒钟就能定位方向。3.3 用 -c 做摘要统计建立数字参照单独看流水刷屏看不出比例我再跑一次这次用 strace 的统计模式timeout 30 strace -f -c -p $(head -n1 $PGDATA/postmaster.pid) \ -e tracepread64,preadv2 2 /tmp/aio/sync_summary.txt结束后摘要文件里会有一张表列出 pread64 的总调用次数、单次耗时分布以及错误返回值。这张表就是基线的核心数字。我在同一台机器上测的典型结果是调用次数等于扫描涉及的数据块数平均每次耗时基本由磁盘响应延迟决定调用之间几乎没有机会去执行别的系统调用。这些数字先记下来下一步切 io_uring 后直接对比调用类型和调用次数效果差异会非常直观。如果没有这张基线表光看 io_uring 的日志会觉得它多了一堆奇奇怪怪的调用反而分不清好坏。4. 切到 io_methodio_uring内核接口终于露出了尾巴基线记录完成现在进入正题把 io_method 改成 io_uring然后重启让配置生效。4.1 修改配置并确认确实切换成功配置修改方式很常规ALTER SYSTEM SET io_method io_uring; pg_ctl -D $PGDATA restart -m fast重启后先看日志里有没有回退或报错。如果 PostgreSQL 检测到当前环境不支持 io_uring有的发行版会直接让实例起不来有的会打一条 WARNING 后继续用兼容模式。这一步是很多坑的源头所以我的习惯是重启后立刻执行SHOW io_method;如果显示的是 io_uring说明 GUC 层面已经生效。但这距离真的在内核里跑 io_uring还有一步必须靠 strace 验证。GUC 显示值只是配置值不代表运行时选择值很多人在这一步栽过。4.2 strace 里看到了 io_uring_setup 和 io_uring_enter用跟上一节一模一样的 strace 参数重新跑一轮查询打开日志后会发现画风突变。pread64 大量消失出现的是另一批系统调用。比较典型的三段输出如下[pid 10722] io_uring_setup(8, {flags0, sq_thread_cpu0, sq_thread_idle0, ...}) 3 [pid 10722] io_uring_register(3, IORING_REGISTER_FILES, [166, 167, ...], 16) 16 [pid 10722] io_uring_enter(3, 64, 0, 0, NULL, 0) 64第一行是创建 io_uring 实例它输出了队列深度 8。第二行是尝试把一批文件描述符注册进内核省得每次提交请求都传 fd这是 io_uring 的优化手段之一。第三行最关键io_uring_enter的第一个参数是 io_uring 的 fd第二个参数是这次提交的请求数第三个参数是最少等待完成的请求数。当第三个参数为 0 而第二个参数大于 0 时代表这次调用只是提交不等待结果。后面还会看到这种[pid 10722] io_uring_enter(3, 0, 16, IORING_ENTER_GETEVENTS, NULL, 0) 16这一次第二个参数是 0第三个参数为 16含义是我等至少 16 个请求完成。这两个动作合起来就是异步 I/O 的标准姿势先一把塞进去一堆请求然后统一收割。strace 里的时间戳会告诉你这两次 enter 之间往往隔着一段 CPU 计算时间说明后端进程确实没有趴在磁盘上干等。4.3 和同步模式的差异可以量化到调用次数上对比最直观的是 strace -c 的统计。同样一次位图扫描同步模式产生数万次 pread64io_uring 模式下产生的是几十次 io_uring_setup/register外加几百次 io_uring_enter每次 enter 的第二个参数往往大于 1。这说明单次系统调用已经从读一个块变成了批量提交一批块。观察项io_methodnoneio_methodio_uring主要读调用pread64io_uring_enter单次调用请求数通常 18、16、64 不等后端 D 状态频率高明显降低内核返回粒度单次完成批量完成日志中调用条数高低如果你在日志里依然看到满屏 pread64但 io_uring_enter 也出现了说明只是部分 I/O 走了异步另一部分仍然同步。这种情况在上节提过属于正常现象但比例需要重点观察。如果 pread64 占比还高那切换的意义就打折扣了。4.4 为什么批量提交反而更靠谱这里解释一下背后的逻辑。现代 NVMe 设备的队列深度很高一次只发一个请求等于让设备一直处于吃不饱的状态。io_methodio_uring 的价值在于把多个预读请求压进同一个提交批次内核和设备可以按最优顺序调度减少中断和上下文切换的次数。同步模式虽然一次只发一个但同样可以利用预读并发只是每发一个请求都会牵扯到进程调度和直返CPU 在等结果期间其实是被浪费的。更关键的是批量提交让 PostgreSQL 不必把读块这个动作做成顺序化。进程可以一次性规划出后续要读的几十个块把请求全部丢给内核然后趁内核搬运数据的时候去算位图、做过滤、准备下一批任务。对于 CPU 快、磁盘慢的典型分析型查询这一进一出节省的是大量等待时间。当然如果你的查询本身只读几个块比如走主键索引那 io_uring 几乎没有存在感因为它根本凑不齐一个像样的批次。5. io_methodworker没有 io_uring 的机器也不必放弃异步不是所有环境都允许 io_uring。容器里 Seccomp 过滤、云盘驱动不支持、内核版本太老都可能让你无法使用 io_uring 路径。PG18 为此留了 worker 这个选项用进程模拟异步。前面说过我手上这个版本默认值就是 worker所以它在兼容性上其实扮演了更重要的角色。5.1 worker 模式的工作原理worker 模式的基本思路是后端进程把 I/O 请求打包发送给一个专门的辅助进程。这个辅助进程承担阻塞式 read把数据读好后再通过共享内存或者管道还给后端进程。后端进程自身在整个过程中不用趴在磁盘上等待可以继续去做解析、计算、生成下一批请求等事情。这种做法等价于把同步阻塞从主执行路径挪到了辅助进程路径上。它比纯同步好在后端进程不会因为一次磁盘等待就完全停摆坏处也很明显多了一次跨进程通信而且辅助进程数量有限遇到海量小 I/O 时可能排队。但至少在没有 io_uring 的环境里它提供了一条相对平滑的异步路径不是非黑即白。5.2 strace 里 worker 模式长什么样再次切到 worker 模式相同查询strace 日志里的调用主体会发生迁移。你会发现大量的 pread64 不再出现在原来的 backend 进程 pid 下而转移到辅助 worker 的 pid 下。后端 pid 下频繁出现的反而是read/write管道、epoll_wait、poll这类用于处理完成通知的系统调用。典型的日志片段长这样[pid 10888] pread64(12/srv/pgdata/base/16386/16412, ..., 8192, 8192000) 8192 [pid 10888] pread64(12/srv/pgdata/base/16386/16412, ..., 8192, 8192001) 8192 [pid 10833] poll([{fd4, eventsPOLLIN}], 1, 30000) 1 [pid 10833] read(4, ..., 8) 8前面两条是 worker 在老老实实读盘后面两条是主后端收到完成通知。如果你用ps -ef同时看会发现多出一个名字里带 aio 或者 worker 的 PostgreSQL 子进程。用 strace 的 -f 把进程树展开能看到它和主后端之间通过管道或事件通知通信。5.3 worker 和 io_uring 到底怎么选我个人的经验是优先能用 io_uring 就用 io_uring尤其在高并发、小查询、延迟敏感的场景。但 worker 是兜底选项它能让你在不具备 io_uring 的环境里也享受到一部分异步收益代价是额外的进程间通信开销。如果实例本身查询并发不高、单查询 I/O 压力不大worker 和 io_uring 的差距基本感知不到。选型没有一刀切的答案关键要看 strace 展示的瓶颈。日志里如果看到主后端长期被 pread64 占据说明异步路径根本没生效如果看到辅助 worker 排队严重而 io_uring 又没法启用那不如退回到 none老老实实靠调整effective_io_concurrency优化预读策略。工具是用来做判断的不是用来制造信仰的。没有数据支撑的新特性一定更好十个有九个会翻车。6. 实操中经常翻车的排查清单做这种内核层实验坑一定比想象多。下面这些是我真实遇到或者看群友折腾后整理出来的现场事故复制出来给后来人少走弯路。6.1 io_method 显示 io_uring却看不到 io_uring_setup先别急着怀疑 strace 写错。最常见的两个原因一是跟踪目标不对二是编译时没带 liburing。确认编译期开关可以看pg_config --configure或者ldd里有没有 liburing确认跟踪目标用pgrep -P postmaster_pid看实际后端进程然后单独 attach。还有一个容易忽略的点如果 PostgreSQL 检测到 io_uring 不可用有的版本会直接回退并使用 worker 或 none但 GUC 的显示值可能仍然是配置值。所以光看SHOW io_method不够必须同时看日志和 strace。日志里一般会有io_method ... not supported, falling back这类字眼没有的话多半是它还没走到初始化路径。6.2 为什么 pread64 和 io_uring_enter 会同时出现这是正常现象。io_uring 只对异步 I/O 子系统管理的请求生效PostgreSQL 里有些路径仍然保留同步读比如某些临时文件、控制文件读取、小尺寸的单块读取。遇到这种情况不要认为io_uring 没生效而要看比例。我之前跑过一个测试日志里 pread64 占了七成后面一查是查询走了索引扫描而不是位图扫描单块读取根本没进异步通道。所以复现实验时一定要控制执行计划。像我这样故意关掉 indexscan、强制 bitmapscan就是为了让预读路径最大化。如果你要观察其他场景请先确认执行计划是什么再谈 I/O 调用模式。执行计划一变I/O 模式天翻地覆拿两套不同计划的日志对比只会得出错误结论。6.3 容器环境和文件系统对 io_uring 的限制容器里的 Seccomp 策略、某些云盘的块设备驱动、文件系统挂载参数都可能让 io_uring 在某些路径上不可用或回退。不要在这个问题上跟内核较劲先用最朴素的本地磁盘、普通 ext4/xfs 跑通实验再回头调容器策略。否则你会发现换个挂载点同一套配置结论就变了。排查时顺手看下dmesg | grep -i io_uring重点还是回到 strace。如果 strace 能看到 io_uring_setup 成功并返回一个 fd说明内核允许如果返回 EPERM 或 ENOSYS那基本就是权限或版本问题和 PostgreSQL 配置没有关系。6.4 并发度和 I/O 方法不是一回事最后强调一次effective_io_concurrency决定 PG 预读多少块、是否预取io_method决定底层通过什么机制去发起这些预读。两者互相配合但不能互相替代。一个常见错误是发现 io_uring 没收益就来回切 io_method却忽略了effective_io_concurrency可能只有 1预读根本没启动。这种时候调 io_method 等于白调。反过来如果effective_io_concurrency已经是 32但同步 I/O 已经成为瓶颈继续提高并发度只会让磁盘队列变深延迟反而上升。正确路径是先把基线验明白再用 strace 判断瓶颈到底在预读数量上还是预读方式上然后对症下药。7. 留在最后的操作心得实验做完之后我给自己定了几条规矩这可能比任何结论都值钱。第一生产上遇到读多写少、单查询延迟敏感的负载先跑一轮基线 strace确认瓶颈到底在不在系统调用层。很多慢查询的根因是执行计划烂或者内存命中率低跟 io_method 一点关系都没有上来就改参数纯粹是给自己找麻烦。第二改 io_method 之前把实例的编译选项、内核版本、容器权限、文件系统类型全部记在案。这四个因素任何一个不满足实验的结论就可能完全不一样。第三不要只盯着SHOW io_method日志、strace、执行计划三个信息源必须能相互印证。我最开始就是只看 GUC 显示值就以为 io_uring 已经生效最后被 strace 打脸后来才养成了这个习惯。最后分享一个特别实用的技巧用 strace 观察这类 I/O 行为时把-o指向一个空目录并在文件名上直接写前缀比如-o /tmp/aio/trace因为-f模式下每个被跟踪进程会独立生成 trace.pid 文件方便你按 pid 快速定位是哪个后端在发请求。再配合-yy你甚至能在日志里直接看到读的是哪个表文件排查对账时特别有用。这套组合拳一旦用熟io_method 在你眼里就不再是玄学而是一个可以看到、可以验证、可以安心做生产决策的系统行为了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑