PostgreSQL 19 中的内核异步读取(io_uring)

Lobsters Hottest 新闻

摘要

PostgreSQL 19 通过 io_uring 引入了内核异步读取功能,实现了直接的异步缓冲 I/O,从而在不使用专用工作进程的情况下提高性能。

<p><a href="https://lobste.rs/s/omfy64/kernel_asynchronous_reads_postgresql_19">评论</a></p>
查看原文
查看缓存全文

缓存时间: 2026/07/02 16:13

# PostgreSQL 19 中的内核异步读取 (io_uring) 来源:https://dev.to/franckpachot/iouring-buffered-reads-in-postgresql-19-iouring-mcn 在上一篇文章 (https://dev.to/franckpachot/multi-block-buffered-reads-in-postgresql-19-io-combine-prefetch-3dl2) 中,我执行了一个受益于异步顺序扫描的查询。尽管操作系统级别的读取调用仍然是同步的(`pread64()`、`preadv()`),但 PostgreSQL 的 IO 工作者会分发这些调用并管理异步 IO 队列。Linux 提供了异步缓冲 I/O,PostgreSQL 可以通过 `io_uring` 系统调用直接使用。 在这篇文章中,我将使用 `io_uring` IO 方法(而不是 `worker`)运行同样的查询。因为我是在一个启用了安全计算模式(seccomp)的 Docker 容器中运行,该模式禁用了 `io_uring` 系统调用,所以我启动了一个禁用 seccomp 的容器: ``` docker run -d --name pg19 \ -p 5432:5432 \ -e POSTGRES_PASSWORD=xxx \ --security-opt seccomp=unconfined \ postgres:19beta1 \ -c io_method=io_uring ``` 进入全屏模式退出全屏模式 我连接上了(`PGUSER=postgres PGPASSWORD=xxx PGHOST=localhost psql`)并检查了配置: ``` postgres=# \dconfig io_* List of configuration parameters Parameter | Value ---------------------------+---------- io_combine_limit | 128kB io_max_combine_limit | 128kB io_max_concurrency | 64 io_max_workers | 8 io_method | io_uring io_min_workers | 2 io_worker_idle_timeout | 1min io_worker_launch_interval | 100ms (8 rows) ``` 进入全屏模式退出全屏模式 这与上一篇文章类似,但使用了不同的 `io_method`。我执行了同样的受益于 io_combine 的查询(不涉及大型 TOAST 文档的查询): ``` postgres=# explain (analyze, buffers, io, costs off) select count(*),avg(length(data)) from smalldocs; QUERY PLAN ------------------------------------------------------------------------------------------------------ Finalize Aggregate (actual time=941.539..943.440 rows=1.00 loops=1) Buffers: shared hit=15019 read=131281 dirtied=801 written=432 -> Gather (actual time=941.398..943.428 rows=3.00 loops=1) Workers Planned: 2 Workers Launched: 2 Buffers: shared hit=15019 read=131281 dirtied=801 written=432 -> Partial Aggregate (actual time=939.501..939.502 rows=1.00 loops=3) Buffers: shared hit=15019 read=131281 dirtied=801 written=432 -> Parallel Seq Scan on smalldocs (actual time=0.033..155.375 rows=341333.33 loops=3) Prefetch: avg=74.32 max=91 capacity=94 I/O: count=8247 waits=54 size=15.92 in-progress=4.97 Buffers: shared hit=15019 read=131281 dirtied=801 written=432 Worker 0: Prefetch: avg=74.41 max=91 capacity=94 I/O: count=2695 waits=30 size=15.93 in-progress=4.98 Worker 1: Prefetch: avg=73.99 max=91 capacity=94 I/O: count=2760 waits=13 size=15.88 in-progress=4.95 Planning: Buffers: shared hit=5 Planning Time: 0.094 ms Execution Time: 943.470 ms (20 rows) ``` 进入全屏模式退出全屏模式 这个计划与上一个类似,因为 `io combine`(显示为 prefetch)在 worker 和 io_uring 中的工作方式相同。现在的不同之处在于,我不再看到任何 `postgres: io worker` 进程,因为这些由内核管理。 我对 PostgreSQL 后端和并行工作者使用了 strace: ``` # echo 3 | sudo tee /proc/sys/vm/drop_caches && strace -fyye trace=io_uring_enter,io_uring_setup,io_uring_enter,io_uring_register -s 0 -qq \ -p $(pgrep -fd, "postgres: ") -T -o /dev/stdout 2143284 io_uring_enter(4, 1, 0, 0, NULL, 8) = 1 <0.000016> 2143284 io_uring_enter(4, 0, 1, IORING_ENTER_GETEVENTS, NULL, 8) = -1 EINTR (Interrupted system call) <0.000307> 2143284 io_uring_enter(4, 1, 0, 0, NULL, 8) = 1 <0.000019> 2143284 io_uring_enter(4, 1, 0, 0, NULL, 8) = 1 <0.000025> 2143284 io_uring_enter(4, 1, 0, 0, NULL, 8) = 1 <0.000025> 2143284 io_uring_enter(4, 0, 1, IORING_ENTER_GETEVENTS, NULL, 8) = 0 <0.001068> 2143284 io_uring_enter(4, 1, 0, 0, NULL, 8) = 1 <0.000034> 2143284 io_uring_enter(4, 1, 0, 0, NULL, 8) = 1 <0.000041> 2143284 io_uring_enter(4, 0, 1, IORING_ENTER_GETEVENTS, NULL, 8) = 0 <0.000714> 2143284 io_uring_enter(4, 1, 0, 0, NULL, 8) = 1 <0.000024> 2143284 io_uring_enter(4, 0, 1, IORING_ENTER_GETEVENTS, NULL, 8) = 0 <0.000720> 2143284 io_uring_enter(4, 1, 0, 0, NULL, 8) = 1 <0.000028> 2143284 io_uring_enter(4, 0, 1, IORING_ENTER_GETEVENTS, NULL, 8) = 0 <0.000505> 2143284 io_uring_enter(4, 1, 0, 0, NULL, 8) = 1 <0.000015> 2143284 io_uring_enter(4, 1, 0, 0, NULL, 8) = 1 <0.000046> 2143284 io_uring_enter(4, 0, 1, IORING_ENTER_GETEVENTS, NULL, 8) = 0 <0.000406> 2143284 io_uring_enter(4, 2, 0, 0, NULL, 8) = 2 <0.000043> 2143284 io_uring_enter(4, 1, 0, 0, NULL, 8) = 1 <0.000025> 2143284 io_uring_enter(4, 1, 0, 0, NULL, 8) = 1 <0.000088> 2143284 io_uring_enter(4, 1, 0, 0, NULL, 8) = 1 <0.000209> 2143284 io_uring_enter(4, 1, 0, 0, NULL, 8) = 1 <0.000096> 2143284 io_uring_enter(4, 1, 0, 0, NULL, 8) = 1 <0.000020> 2143284 io_uring_enter(4, 1, 0, 0, NULL, 8) = 1 <0.000018> 2143284 io_uring_enter(4, 1, 0, 0, NULL, 8) = 1 <0.000026> 2143284 io_uring_enter(4, 1, 0, 0, NULL, 8) = 1 <0.000024> 2143284 io_uring_enter(4, 1, 0, 0, NULL, 8) = 1 <0.000017> 2143284 io_uring_enter(4, 1, 0, 0, NULL, 8) = 1 <0.000016> 2143284 io_uring_enter(4, 1, 0, 0, NULL, 8) = 1 <0.000031> 2143284 io_uring_enter(4, 1, 0, 0, NULL, 8) = 1 <0.000053> 2143284 io_uring_enter(4, 1, 0, 0, NULL, 8) = 1 <0.000022> 2143284 io_uring_enter(4, 1, 0, 0, NULL, 8) = 1 <0.000013> 2143284 io_uring_enter(4, 1, 0, 0, NULL, 8) = 1 <0.000020> 2143284 io_uring_enter(4, 1, 0, 0, NULL, 8) = 1 <0.000016> 2143284 io_uring_enter(4, 0, 1, IORING_ENTER_GETEVENTS, NULL, 8) = 0 <0.000560> ... ``` 进入全屏模式退出全屏模式 这个系统调用是 `io_uring_enter(fd, to_submit, min_complete, flags, sig, sigsz)`,所以: - `io_uring_enter(4, 1, 0, 0, NULL, 8) = 1` 表示:内核,我的提交队列里有一个新的 I/O 请求。我不想等待。内核确认消耗了一个提交。 - `io_uring_enter(4, 0, 1, IORING_ENTER_GETEVENTS, NULL, 8) = 0` 表示:我什么都没提交。等待至少一个完成事件可用。返回值是 0,因为这次调用没有新的提交。短暂的时间显示完成事件很快可用。 io_uring 追踪显示,PostgreSQL 不会为单个读取操作而等待。相反,后端持续通过 `io_uring_enter(..., 1, 0, ...)` 提交请求,并通过 `io_uring_enter(..., 0, 1, IORING_ENTER_GETEVENTS, ...)` 从完成队列中检索已完成的请求。大多数完成会立即发生,这表明读取流保持了足够的 I/O 活动,以便在需要时结果立即可用。这种行为与 EXPLAIN 统计数据一致,该数据显示预取队列很深,组合读取很大,并且尽管有数千次 I/O 操作,等待却很少。 与 worker 实现的区别不在于 PostgreSQL 读取了什么,而在于这些读取是如何提交和完成的: | `io_mode=sync` | `io_mode=worker` | `io_mode=io_uring` | |----------------|------------------|---------------------| | postgres backend | postgres backend | postgres backend | | ↳ `pread64()` | ↳ `preadv()` (async) | ↳ `io_uring_enter()` | | ↳ kernel | ↳ postgres: io worker (async) | ↳ kernel | | | ↳ `pread64()` 或 `preadv()` | | | | ↳ kernel | | 我使用五个并行 PostgreSQL 查询工作者运行了同样的查询: ``` postgres=# \! echo 3 | sudo tee /proc/sys/vm/drop_caches 3 postgres=# set max_parallel_workers_per_gather = 5; SET postgres=# explain (analyze, buffers, io, settings, costs off) select count(*),avg(length(data)) from smalldocs; QUERY PLAN -------------------------------------------------------------------------------------------------------- Finalize Aggregate (actual time=48293.834..48295.976 rows=1.00 loops=1) Buffers: shared hit=9776 read=136512 -> Gather (actual time=48271.604..48295.958 rows=6.00 loops=1) Workers Planned: 5 Workers Launched: 5 Buffers: shared hit=9776 read=136512 -> Partial Aggregate (actual time=48237.365..48237.365 rows=1.00 loops=6) Buffers: shared hit=9776 read=136512 -> Parallel Seq Scan on smalldocs (actual time=1.863..47814.487 rows=170666.67 loops=6) Prefetch: avg=80.29 max=92 capacity=94 I/O: count=8661 waits=8429 size=15.76 in-progress=4.97 Buffers: shared hit=9776 read=136512 Worker 0: Prefetch: avg=82.96 max=91 capacity=94 I/O: count=1434 waits=1418 size=15.95 in-progress=4.93 Worker 1: Prefetch: avg=80.43 max=91 capacity=94 I/O: count=1406 waits=1394 size=15.87 in-progress=4.89 Worker 2: Prefetch: avg=77.46 max=92 capacity=94 I/O: count=1428 waits=1384 size=15.66 in-progress=4.91 Worker 3: Prefetch: avg=81.88 max=91 capacity=94 I/O: count=1451 waits=1417 size=15.69 in-progress=5.02 Worker 4: Prefetch: avg=76.47 max=91 capacity=94 I/O: count=1434 waits=1395 size=15.69 in-progress=5.03 Settings: max_parallel_workers_per_gather = '5' Planning Time: 0.060 ms Execution Time: 48296.009 ms (25 rows) ``` 进入全屏模式退出全屏模式 平均每个进程有 5 个 I/O 操作正在进行中,总共 30 个。随着执行继续,负载平均值上升,因为不可中断的等待被计数为可运行任务: ``` top - 15:32:43 up 23 days, 40 min, 1 user, load average: 25.11, 10.84, 4.17 Threads: 1031 total, 1 running, 1030 sleeping, 0 stopped, 0 zombie %Cpu(s): 2.4 us, 1.5 sy, 0.0 ni, 0.0 id, 95.3 wa, 0.1 hi, 0.7 si, 0.0 st GiB Mem : 23.6 total, 14.9 free, 5.9 used, 2.8 buff/cache GiB Swap: 4.0 total, 3.8 free, 0.2 used. 14.0 avail Mem PID USER VIRT S %CPU %MEM TIME+ COMMAND WCHAN 2187030 root 0.0m I 1.3 0.0 0:04.41 [kworker/u8:4-iscsi_q_1] - 2211481 opc 221.9m R 1.3 0.0 0:02.65 top - 2143284 100998 251.3m S 1.0 0.7 0:47.66 postgres: postgres postgres 10.0.2.100(35862) EXPLAIN arm64_sys+ 2212667 100998 247.1m S 1.0 0.2 0:00.38 postgres: parallel worker for PID 73 arm64_sys+ 2212669 100998 247.1m S 1.0 0.1 0:00.39 postgres: parallel worker for PID 73 arm64_sys+ 2212670 100998 247.1m S 1.0 0.1 0:00.39 postgres: parallel worker for PID 73 arm64_sys+ 2212671 100998 247.1m S 1.0 0.2 0:00.39 postgres: parallel worker for PID 73 arm64_sys+ 2212668 100998 247.1m S 0.7 0.2 0:00.40 postgres: parallel worker for PID 73 arm64_sys+ 2212257 100998 251.3m D 0.3 0.7 0:00.07 postgres: postgres postgres 10.0.2.100(35862) EXPLAIN generic_f+ 2210102 root 0.0m I 0.3 0.0 0:01.36 [kworker/u8:1-xfs-cil/sdb] - 2212682 100998 247.1m D 0.3 0.2 0:00.02 postgres: parallel worker for PID 73 generic_f+ 2212694 100998 247.1m D 0.3 0.2 0:00.02 postgres: parallel worker for PID 73 generic_f+ 2212794 100998 247.1m D 0.3 0.2 0:00.01 postgres: parallel worker for PID 73 generic_f+ 4157944 opc 23.1m D 0.3 0.1 18:26.08 /usr/bin/fuse-overlayfs -o lowerdir=/data/opc/share/containers/storage/overlay/l/JU5OB2S2NJVEHXCBJJ2RUU7R4M:/data/opc/share/containers/storage/overlay/l/SCFJOZKVJCNPWIHHO5L+ wait_on_p+ 1 root 380.1m S 0.0 0.1 7:11.80 /usr/lib/systemd/systemd --switched-root --system --deserialize 18 - ``` 进入全屏模式退出全屏模式 使用 `io_uring` 时的行为比使用 `worker` 方法更微妙。使用同步的 `pread64()` 或 `preadv()` 时,调用进程会阻塞直到读取完成,在 I/O 期间可能会进入不可中断睡眠状态(`D` 状态)。下面是运行 `io_method=worker` 且 `max_parallel_workers_per_gather = 5` 时的 `top` 输出: ``` top - 18:06:57 up 23 days, 3:14, 1 user, load average: 8.15, 8.05, 4.48 Threads: 1014 total, 1 running, 1013 sleeping, 0 stopped, 0 zombie %Cpu(s): 2.3 us, 3.3 sy, 0.0 ni, 3.5 id, 89.8 wa, 0.2 hi, 0.9 si, 0.0 st MiB Mem : 24132.3 total, 16070.1 free, 6064.6 used, 1997.6 buff/cache MiB Swap: 4095.9 total, 3878.6 free, 217.3 used. 14712.0 avail Mem PID USER VIRT S %CPU %MEM TIME+ COMMAND 2221405 opc 227264 R 1.3 0.0 1:57.53 top 2291054 100998 234816 S 1.3 0.1 0:00.23 postgres: parallel worker for PID 75 2291057 100998 234816 S 1.3 0.1 0:00.23 postgres: parallel worker for PID 75 2280024 root 0 I 1.0 0.0 0:05.86 [kworker/u8:1-iscsi_q_1] 2287675 100998 235968 S 1.0 0.7 0:06.82 postgres: postgres postgres 10.0.2.100(39936) EXPLAIN 1680775 opc 24896 D 0.7 0.1 59:06.99 /usr/bin/fuse-overlayfs -o lowerdir=/data/opc/share/containers/storage/overlay/l/JU5OB2S2NJVEHXCBJJ2RUU7R4M:/data/opc/share/containers/storage/overlay/l/SCFJOZKV+ 2265340 root 0 I 0.7 0.0 0:09.13 [kworker/u8:0-iscsi_q_1] 2291053 100998 234816 D 0.7 0.1 0:00.23 postgres: parallel worker for PID 75 2291055 100998 234816 D 0.7 0.1 0:00.23 postgres: parallel worker for PID 75 2291056 100998 234816 D 0.7 0.1 0:00.23 postgres: parallel worker for PID 75 2287499 100998 231808 D 0.3 0.6 0:00.74 postgres: io worker 0 2287500 100998 231808 D 0.3 0.5 0:00.53 postgres: io worker 1 2288950 100998 231808 D 0.3 0.5 0```

相似文章

探索使用 io_uring 的自动缓冲区管理

Lobsters Hottest

本文详细介绍了在 UringMachine(一个用于异步 I/O 的 Ruby gem)中使用 io_uring 的缓冲区环实现自动缓冲区管理。它解释了缓冲区环如何通过允许内核使用应用程序提供的缓冲区来实现高效的多重读取/接收操作。

Zig 0.16 中的异步 I/O:今日视角

Lobsters Hottest

Zig 0.16 推出了新的 std.Io 接口,用于跨平台 I/O。zio 库通过栈式协程和操作系统级异步 API 提供了完整的异步实现,无需每个任务一个线程即可实现高效的并发任务。

展望 Postgres 19

Hacker News Top

PostgreSQL 19 beta 引入了关键特性,如 REPACK CONCURRENTLY、分区拆分与合并以及增强的逻辑复制,为生产数据库管理提供了实用改进。