暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

跟着AI学PostgreSQL (1.09): 异步 I/O(AIO)——PG18 引入,PG19 把它做厚

瀚高PG实验室 2026-07-21
67
本篇是 Track 1「性能优化」里最年轻的一块。PG18 才把异步 I/O 正式合入主线,PG19 又在此基础上加了一批 GUC、支持了 io_uring
、并把「Streaming I/O」推广到更多代码路径。我们用 gdb 把「一次顺序扫描的读」拆成后端分期和 io worker 执行两端,看清楚 PG 到底是怎么把磁盘读从 backend 身上卸下来的。

0. 先说结论(给赶时间的你)

PostgreSQL 传统上每个 backend 自己调 pread
/preadv
做同步读,读盘的毫秒级延迟直接卡住 backend。PG18 引入 AIO 子系统:

  • backend 只负责「分期」
    (stage)一次读:填好 PgAioHandle
    ,入队,立刻返回;
  • 专门的 io worker 进程
    负责「执行」:真正发 pg_preadv
    系统调用;
  • 后端用 read_stream
    (Streaming I/O 基础设施)做预读和合并,把一堆小读合并成大块 readv

我们用 gdb 在 bigt
(600 万行)冷读时,同时抓住了这两端。


1. 场景:顺序扫描的读,以前卡在谁身上?

sql
-- 冷启动后第一次扫全表(shared_buffers 为空)
SELECT count(*) FROM bigt;   -- bigt: 6,000,000 行

在 PG17 及以前,这条语句的每个 backend 走到 heapgettup_pagemode
取下一页时,如果页不在 shared_buffers
,就得自己阻塞着等磁盘。并发一高,backend 全堵在 I/O 上,CPU 白白空闲。

PG18 之后,这个读被拆开了。我们用相同的调试实例(端口 5618,--enable-debug --enable-cassert
,PG18.4)实测。


2. gdb 实测之一:后端只做「分期」

断点:pgaio_io_start_readv
StartReadBuffers
,cold read bigt

===== pgaio_io_start_readv (backend stages an async READ) =====
ioh->state=1                       <-- HANDED_OUT:只拿到句柄,尚未执行
#0  pgaio_io_start_readv (ioh=0x7fffedee8990, fd=29, iovcnt=1, offset=8192) at aio_io.c:81
#1  FileStartReadV (ioh=..., file=22, iovcnt=1, offset=8192, ...)        at fd.c:2241
#2  mdstartreadv (ioh=..., reln=0x13748e8, forknum=MAIN_FORKNUM, blocknum=1, ...) at md.c:1030
#3  smgrstartreadv (ioh=..., reln=0x13748e8, forknum=MAIN_FORKNUM, ...)  at smgr.c:758
#4  AsyncReadBuffers (operation=0x7fffffffbc90, nblocks_progress=...)   at bufmgr.c:1954
#5  StartReadBuffersImpl (operation=..., blockNum=1, nblocks=..., flags=8, ...) at bufmgr.c:1428

同时抓到的「发起者」调用栈,正是 Streaming I/O 路径:

===== StartReadBuffers (multi-block read initiator) =====
#0  StartReadBuffers (operation=0x12ff948, blockNum=0, nblocks=..., flags=0) at bufmgr.c:1495
#1  read_stream_start_pending_read (stream=0x12ff738)            at read_stream.c:356
#2  read_stream_look_ahead (stream=0x12ff738)                    at read_stream.c:514
#3  read_stream_next_buffer (stream=0x12ff738, ...)              at read_stream.c:889
#4  heap_fetch_next_buffer (scan=0x12ff328, ...)                 at heapam.c:678
#5  heapgettup_pagemode (scan=0x12ff328, ...)                    at heapam.c:1040

关键事实

  • 后端一路走到 pgaio_io_start_readv
    (aio_io.c:81),它做的事只是 pgaio_io_stage(ioh, PGAIO_OP_READV)
    ——把读请求登记到句柄上(ioh->state=1
    PGAIO_HS_HANDED_OUT
    ),并不真正读盘
  • 真正的「发起者」是 heapgettup_pagemode → read_stream_* → StartReadBuffers
    read_stream.c
    (Streaming I/O 基础设施)在 PG18 源码里就已存在(1092 行),说明预读/合并这条能力在 18 就落地了。

3. gdb 实测之二:io worker 才做「执行」

pgaio_io_start_readv
只是分期。真正发 pg_preadv
的是 io worker 进程。我们对一个 io worker(PID 9985)挂 gdb,断 pgaio_io_perform_synchronously

===== WORKER: pgaio_io_perform_synchronously (pg_preadv) =====
ioh->op=1                        <-- PGAIO_OP_READV
#0  pgaio_io_perform_synchronously (ioh=0x7fffedfcbd00) at aio_io.c:118
#1  IoWorkerMain (startup_data=0x0, startup_data_len=0)        at method_worker.c:565
#2  postmaster_child_launch (child_type=B_IO_WORKER, ...)      at launch_backend.c:290
#3  StartChildProcess (type=B_IO_WORKER)                       at postmaster.c:3962
#4  maybe_adjust_io_workers ()                                 at postmaster.c:4393
#5  PostmasterMain (argc=3, argv=0x12c1570)                    at postmaster.c:1383
#6  main (argc=3, argv=0x12c1570)                              at main.c:227

铁证:这是一个由 postmaster 以 B_IO_WORKER
类型专门启动的辅助进程,它的主循环 IoWorkerMain
反复调用 pgaio_io_perform_synchronously
(aio_io.c:118),而该函数里就是:

c
case PGAIO_OP_READV:
    result = pg_preadv(ioh->op_data.read.fd, iov,
                       ioh->op_data.read.iov_length,
                       ioh->op_data.read.offset);   aio_io.c:128

真正阻塞在磁盘 I/O 上的,是 io worker,不是你的业务 backend。backend 把读分期入队后就去干别的(或等 latch 被唤醒),io worker 并发地把一批读刷到磁盘。


4. 源码结构:AIO 子系统怎么组织的

源码目录 src/backend/storage/aio/

文件
作用
aio.c
核心:句柄池、提交/等待、回调
aio_io.c
pgaio_io_start_readv
(分期)/ pgaio_io_perform_synchronously
(执行,内含 pg_preadv
method_worker.c
io_method=worker
:提交队列 + 唤醒空闲 worker;IoWorkerMain
是 worker 主循环
method_sync.c
io_method=sync
:后端自己同步执行(无 worker)
method_io_uring.c
io_method=io_uring
PG19 才可用
read_stream.c
Streaming I/O:预读、合并、look-ahead,PG18 已存在

调用分工(对照 gdb):

后端(backend):
  heapgettup_pagemode
    → read_stream_next_buffer read_stream_look_ahead   (read_stream.c)
    → StartReadBuffers → StartReadBuffersImpl → AsyncReadBuffers
    → smgrstartreadv → mdstartreadv → FileStartReadV
    → pgaio_io_start_readv   ★ 仅分期 (state=HANDED_OUT),入队

io worker 进程:
  IoWorkerMain
    → pgaio_io_perform_synchronously   ★ 真正执行
    → pg_preadv  (系统调用,阻塞在这里)

pgaio_worker_submit_internal
(method_worker.c:244)的逻辑很典型:把分期好的 IO 插进 AioWorkerSubmissionQueue
,选一个空闲 worker 用 SetLatch
唤醒;队列满时剩下的退化为 pgaio_io_perform_synchronously
同步执行(保并发)。


5. 实例里的异步 I/O 长什么样

调试实例(PG18.4)实际配置:

sql
SELECT name, setting, unit FROM pg_settings
WHERE name IN ('io_method','io_workers','io_combine_limit','io_max_concurrency');
-- io_method        | worker
-- io_workers       | 3          <-- PG18 叫 io_workers,PG19 改名 io_max_workers
-- io_combine_limit | 16         <-- 16 个块 = 128kB(合并上限)
-- io_max_concurrency | 64

进程视角(ps
),确实拉起了 3 个专用进程:

postgres: io worker 0
postgres: io worker 1
postgres: io worker 2

pg_stat_activity.backend_type = 'io worker'
也能查到这 3 个进程——它们就是 IoWorkerMain
的实体。


6. PG19 视角:把 AIO 做厚

PG19 在 PG18 的 AIO 骨架上加了一批东西(已对照 PG19 官方文档核实):

维度
PG18
PG19
工作进程数 GUC
io_workers
(默认 3)
改名 io_max_workers
(本机 PG19 beta 实测默认 4;最终 GA 默认值以官方文档为准)
最小 worker
新增 io_min_workers
(默认 2
单进程并发度
io_max_concurrency
(默认 64)
PG19 默认同为 64(启动期可设;并非 -1 自动算)
I/O 方法
worker
sync
新增 io_uring
worker
/io_uring
/sync
合并上限
io_combine_limit
(PG18 默认 16 块=128kB)
PG19 默认 32 块=256kB;新增 io_max_combine_limit
(默认 16 块=128kB,作为前者的硬上限静默截断)
worker 生命周期
新增 io_worker_idle_timeout
(默认 1min=60000ms)、io_worker_launch_interval
(默认 100ms)
Streaming I/O
read_stream
基础设施已落地(顺序扫描等)
推广到更多 consumers(vacuum、bitmap heap scan 等)

几个要点:

  1. io_workers
    → io_max_workers
    :PG19 把它从「固定数量」变成「最大数量」,并配合 io_min_workers
    让 postmaster 按需启动/回收 worker(对应源码 maybe_adjust_io_workers
    )。
  2. io_uring
    方法
    :PG19 实验性支持 Linux io_uring
    ,比 worker 进程模型更轻量,省去进程间队列和上下文切换。生产环境是否上,看内核版本与压测。
  3. io_max_concurrency
    默认 64
    (PG18/PG19 一致):控制单个 io worker 能并发发起的 I/O 数——大多数场景不用手调。
  4. Streaming I/O 是贯穿 PG18→19 的主线
    :PG18 已把 read_stream.c
    写进源码,PG19 把它用得更广。理解 AIO,先理解 read_stream

资深视角:AIO 改的是 I/O 调度范型,且与 effective_io_concurrency
衔接。
PG18 的 AIO 首版主要覆盖关系页读取(顺序扫描、bitmap heap scan、索引扫描的预取);PG19 进一步把 checkpoint 写、部分 WAL 写也纳入 AIO 调度。无论哪种 io_method
,上层都是 read_stream
在做预读与合并;而 read_stream
向前看多远,由 effective_io_concurrency
(本机 PG18 实测默认 16)界定——它现在不再是老式的 posix_fadvise
软建议,而是直接决定交给 io worker 的预取并发度。所以有人问"开了 AIO 还要不要管 effective_io_concurrency
",答案是:它仍是预取窗口的大小,调大能让 io worker 更饱和地并行读。

注意:PG18 的 io_method
只支持 worker
和 sync
io_uring
是 PG19 才出现的选项。调试实例(PG18.4)跑的是 worker


7. 小结

  • 一次「冷的顺序扫描读」被拆成后端分期pgaio_io_start_readv
    ,只登记句柄)和 io worker 执行pgaio_io_perform_synchronously
    → pg_preadv
    )两端。
  • 阻塞在磁盘 I/O 上的是 io worker,不是业务 backend——这是 PG18 AIO 的核心收益。
  • read_stream
    (Streaming I/O)负责预读与合并,是 AIO 的上层驱动器,PG18 已存在。
  • PG19 把 worker 数量改成 io_max_workers
    (本机 beta 实测默认 4,GA 以官方文档为准)+ io_min_workers
    (默认 2),新增 io_max_concurrency
    、合并上限与 worker 生命周期 GUC,并实验性支持 io_uring

8. 复现附录(gdb 配方)

调试环境:PG18.4(--enable-debug --enable-cassert
),端口 5618,用户 postgres18
,数据目录 /pgdata/postgres18/data

关键坑(踩过)

  • gdb 断点命令里的 printf
    用 %p
    会报 *Wrong number of arguments* 并 detach——用 %d
    /%x
    或纯文本。
  • 后端侧断 pgaio_io_start_readv
    只在冷读(shared_buffers 为空)时触发;暖读走缓存不进 AIO 路径。
  • 要抓 worker 侧执行,必须 gdb -p <io worker 的 PID>
    ,且需 handle SIGUSR1 nostop noprint pass
    (worker 间用 SIGUSR1 通信,否则 gdb 会被信号打断)。
  • 多 worker 时把 gdb 用 timeout
    后台化会被 ssh 会话回收导致输出丢失;用前台直跑 + setsid
    启动读更稳。

后端侧

# 冷读(重启实例清空 shared_buffers 后)
SELECT pg_sleep(25); SELECT count(*) FROM bigt;
# gdb 断点
break pgaio_io_start_readv
break StartReadBuffers

调用链:heapgettup_pagemode → read_stream_next_buffer → read_stream_look_ahead → read_stream_start_pending_read → StartReadBuffers → StartReadBuffersImpl → AsyncReadBuffers → smgrstartreadv → mdstartreadv → FileStartReadV → pgaio_io_start_readv

worker 侧

# 取 worker PID
SELECT pid FROM pg_stat_activity WHERE backend_type='io worker';
# gdb(前台)
handle SIGUSR1 nostop noprint pass
break pgaio_io_perform_synchronously
continue

调用链:IoWorkerMain → pgaio_io_perform_synchronously → pg_preadv


9. 官方文档核对表

断言
来源
结论
io_method
默认 worker
,可选 worker
/io_uring
/sync
,仅启动期可设
PG19 docs 19.4
io_min_workers
默认 2,仅 worker
方法生效
PG19 docs 19.4
io_max_workers
本机 PG19 beta 实测默认 4(PG18 为 io_workers
=3,GA 默认以官方文档为准)
PG19 docs 19.4 + PG19(5619) 实例 SHOW
io_max_concurrency
默认 -1(自动,≤64)
PG19 docs 19.4
io_combine_limit
(PG18 默认 16 块/128kB → PG19 默认 32 块/256kB);io_max_combine_limit
默认 16 块/128kB 作为硬上限
PG19 docs 19.4 + 本机 PG19(5619) SHOW
实测
✅(io_max_workers 本机 beta 实测 4,GA 以文档为准)
AIO 子系统(pgaio_*
IoWorkerMain
read_stream
)存在于 PG18 源码
PG18.4 源码树
✅(gdb 实锤)
后端分期、worker 执行 pg_preadv
的分工
gdb 实测
✅(两端调用栈)

文章转载自瀚高PG实验室,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论