期待 Postgres 18:使用异步 I/O 加速磁盘读取
期待 Postgres 18:使用异步 I/O 加速磁盘读取
By Lukas Fittl
2025年5月7日
随着本周 Postgres 18 Beta 1 的发布,一项历时多年的努力以及 Postgres 中的重大架构转变正在成形:异步 I/O (AIO)。这些功能仍在积极开发中,但它们代表了 Postgres 处理 I/O 方式的根本性变化,为性能带来了显著提升的潜力,尤其是在延迟通常是瓶颈的云环境中。
虽然在最终版本发布之前的 Beta 阶段,某些功能可能仍会进行调整或删除,但现在是测试和验证 Postgres 18 在实践中表现的最佳时机。在 Postgres 18 中,AIO 仅限于读取操作;写入仍然是同步的,尽管未来版本可能会扩展支持。
在这篇文章中,我们将解释什么是异步 I/O,它在 Postgres 18 中如何工作,以及它对性能优化意味着什么。
为什么异步 I/O 很重要
Postgres 历来在同步 I/O 模型下运行,这意味着每个读取请求都是一个阻塞系统调用。数据库必须暂停并等待操作系统返回数据才能继续。这种设计引入了不必要的 I/O 等待,尤其是在存储通常是网络连接的云环境中(例如,Amazon EBS),并且 I/O 可能具有超过 1 毫秒的延迟。
在一个简化的模型中,我们可以这样说明差异,忽略 Linux 内核可能进行的任何预取/批处理:
你可以把同步 I/O 想象成一位想象中的图书管理员,一次只检索一本书,返回后再获取下一本。随着逻辑操作的物理读取数量增加,这种低效率会加剧。
异步 I/O 通过允许程序并发发出多个读取请求来消除该瓶颈,而无需等待先前的读取返回。在异步程序流程中,I/O 请求被安排读取到内存位置,程序等待这些读取完成,而不是单独发出每个读取。
Postgres 17 的读取流如何铺平道路
在 Postgres 中实现异步 I/O 的工作已经进行了很多年。 Postgres 17 引入了一个重要的内部抽象,随着读取流 API 的引入。这些内部更改标准化了跨不同子系统发出读取操作的方式,并简化了使用 posix_fadvise()
来请求操作系统提前预取数据的方式。
但是,这种咨询机制仅提示内核将数据加载到 OS 页面缓存中,而不是加载到 Postgres 自己的共享缓冲区中。 Postgres 仍然必须为每个读取发出系统调用,并且 OS 预读行为并不总是保持一致。
即将发布的 Postgres 18 版本消除了这种间接性。通过真正的异步读取,数据由数据库本身直接提取到共享缓冲区中,从而绕过了对内核级别启发式的依赖,并实现了更可预测、更高吞吐量的 I/O 行为。
Postgres 18 中的新 io_method 设置
为了控制用于异步 I/O 的机制,Postgres 18 引入了一个新的配置参数:io_method
。此设置确定了底层如何分派读取操作,以及它们是以同步方式处理,还是卸载到 I/O 工作进程,或者通过 io_uring
直接提交到内核。
io_method
设置必须在 postgresql.conf 中设置,并且必须重启后才能更改。它控制 Postgres 将使用的 I/O 实现,并且对于在 Postgres 18 中调整 I/O 性能至关重要。io_method
有三个可能的设置,当前的默认设置(截至 Beta 1)是 worker
。
io_method = sync
Postgres 18 中的 sync
设置镜像了 Postgres 17 中实现的同步行为。读取仍然是同步的和阻塞的,使用 posix_fadvise()
在 Linux 内核中实现预读。
io_method = worker
worker
设置利用在后台运行的专用 I/O 工作进程,这些工作进程独立于查询执行来检索数据。主后端进程将读取请求排队,这些工作进程与 Linux 内核交互以提取数据,然后将其传递到共享缓冲区中,而不会阻塞主进程。
可以通过新的 io_workers
设置来配置 I/O 工作进程的数量,默认为 3
。这些工作进程始终运行,并且在所有连接和数据库之间共享。
io_method = io_uring
这种特定于 Linux 的方法使用 io_uring
,这是一种在内核版本 5.1 中引入的高性能 I/O 接口。异步 I/O 自内核版本 2.5 以来已在 Linux 中可用,但它在很大程度上被认为效率低下且难以使用。 io_uring
在 Postgres 和内核之间建立一个共享环形缓冲区,从而最大限度地减少了系统调用开销。这是最有效的选项,完全消除了对 I/O 工作进程的需求,但仅在较新的 Linux 内核上可用,并且需要与 io_uring
支持兼容的文件系统和配置。
重要说明: 截至 Postgres 18 Beta 1,异步 I/O 支持顺序扫描、位图堆扫描和维护操作(如 VACUUM
)。
异步 I/O 实践
异步 I/O 在存储是网络连接的云环境中(例如,Amazon EBS 卷)提供最显着的收益。在这些设置中,单个磁盘读取通常需要多个毫秒,与本地 SSD 相比,这会引入大量的延迟。
使用传统的同步 I/O,每个读取都会阻塞查询执行,直到数据到达,从而导致 CPU 时间闲置和吞吐量降低。相比之下,异步 I/O 允许 Postgres 并行发出多个读取请求,并在等待结果时继续处理。这减少了查询延迟,并可以更有效地利用可用的 I/O 带宽和 CPU 周期。
AWS 上的基准测试:读取性能翻倍,并且从 io_uring 中获得更大的收益
为了评估异步 I/O 的性能影响,我们在 AWS 上对一个具有代表性的工作负载进行了基准测试,比较了使用不同 io_method
设置的 Postgres 17 和 Postgres 18。工作负载在各个版本中保持相同,从而使我们能够隔离新 I/O 基础设施的效果。
我们已经在 AWS c7i.8xlarge 实例(32 个 vCPU,64 GB RAM)上进行了测试,为 Postgres 提供了一个专用的 100GB io2
EBS 卷,并提供了 20,000 个预置 IOPS。测试表的大小为 3.5GB:
CREATE TABLE test(id int);
INSERT INTO test SELECT * FROM generate_series(0, 100000000);
test=# \dt+
List of relations
Schema | Name | Type | Owner | Persistence | Access method | Size | Description
--------+------+-------+----------+-------------+---------------+---------+-------------
public | test | table | postgres | permanent | heap | 3458 MB |
(1 row)
在测试运行之间,我们清除了 OS 页面缓存 (sync; echo 3 > /proc/sys/vm/drop_caches
),并重新启动了 Postgres,以收集冷缓存结果。热缓存结果表示第二次运行查询。我们为每个配置重复了完整的测试运行多次,保留了三次中的最佳结果。
虽然我们还使用并行查询进行了测试,但为了使结果更易于理解,以下所有结果都禁用了并行查询 (max_parallel_workers_per_gather = 0
)。
冷缓存结果:
Postgres 17 使用同步 I/O,建立了基线。它显示了持续的读取延迟,但吞吐量受到在发出下一个 I/O 请求之前需要完成每个 I/O 请求的限制:
test=# SELECT COUNT(*) FROM test;
count
-----------
100000001
(1 row)
Time: 15830.880 ms (00:15.831)
Postgres 18 在配置为 io_method = sync
时,执行几乎相同,这证实了在不启用异步 I/O 的情况下,行为保持不变:
test=# SELECT COUNT(*) FROM test;
count
-----------
100000001
(1 row)
Time: 15071.089 ms (00:15.071)
但是,当我们切换到使用 worker
方法时,使用 3 个 I/O 工作进程(默认值)会显示出明显的改进:
test=# SELECT COUNT(*) FROM test;
count
-----------
100000001
(1 row)
Time: 10051.975 ms (00:10.052)
我们观察到通过增加 I/O 工作进程的数量获得了一些收益,但是最大的改进来自于利用 io_uring
:
test=# SELECT COUNT(*) FROM test;
count
-----------
100000001
(1 row)
Time: 5723.423 ms (00:05.723)
当我们绘制此图时(以毫秒为单位测量运行时,越低越好),很明显 Postgres 18 在冷缓存情况下表现得更好:
对于冷缓存测试,与传统的 sync
方法相比,worker
和 io_uring
都提供了持续的 2-3 倍的改进。
虽然 worker
由于其并行性为热缓存测试提供了一些好处,但 io_uring
在冷缓存测试中始终表现更好,并且其较低的系统调用开销和减少的进程协调将使 io_uring
成为在 Postgres 18 中最大限度地提高 I/O 性能的推荐设置。
这种磁盘读取的性能变化对基础设施规划具有重要的影响,尤其是在云环境中。通过减少 I/O 等待时间,异步读取可以大大提高查询吞吐量,减少延迟和 CPU 开销。对于读取繁重的工作负载,这可能会转化为更小的实例大小或更好地利用现有资源。
调整 effective_io_concurrency
在 Postgres 18 中,effective_io_concurrency
变得更加有趣,但仅当与异步 io_method
(例如 worker
或 io_uring
)一起使用时才更有意义。以前,此设置仅建议 OS 使用 posix_fadvise
预取数据。现在,它直接控制 Postgres 在内部发出多少异步预读请求。
预读的块数受到 effective_io_concurrency
和 io_combine_limit
的影响,遵循一般公式:
maximum read-ahead = effective_io_concurrency × io_combine_limit
这使 DBA 和工程师可以更好地控制 I/O 行为。最佳值需要进行基准测试,因为它取决于你的 I/O 子系统。例如,更高的值可能有益于具有高延迟但又支持高并发的云环境,例如具有高预置 IOPS 的 AWS EBS。
在进行基准测试时,我们还测试了更高的 effective_io_concurrency
(介于 16 和 128 之间),但没有看到明显的差异。但是,这可能是由于使用了简单的测试查询。
值得注意的是,Postgres 17 中 effective_io_concurrency 的先前默认值为 1,现在已提高到 16,基于 Postgres 社区所做的基准测试。
使用 pg_aios 监控正在进行的 I/O
如前所述,以前版本的 Postgres 使用同步 I/O,可以很容易地发现读取延迟:后端进程会在等待磁盘访问时被阻止,并且 pganalyze 等监控工具可以可靠地将 IO / DataFileRead
作为这些停顿期间的等待事件显示出来。
例如,在这里,我们可以清楚地看到 Postgres 17 同步 I/O 中的等待事件。
使用 Postgres 18 中的异步 I/O,后端等待行为会发生变化。使用 io_method = worker
时,后端进程将读取委托给单独的 I/O 工作进程。因此,后端可能显示为空闲或显示新的 IO / AioIoCompletion
等待事件,而 I/O 工作进程显示实际的 I/O 等待事件:
SELECT backend_type, query, state, wait_event_type, wait_event
FROM pg_stat_activity
WHERE backend_type = 'client backend' OR backend_type = 'io worker';
backend_type | state | wait_event_type | wait_event
----------------+--------+-----------------+-----------------
client backend | active | IO | AioIoCompletion
io worker | | IO | DataFileRead
io worker | | IO | DataFileRead
io worker | | IO | DataFileRead
(4 rows)
使用 io_method = io_uring
时,读取操作直接提交到内核并异步完成。后端不会阻塞传统的 I/O 系统调用,因此即使 I/O 正在进行,此活动也从 Postgres 端不可见。
为了帮助调试正在进行的 I/O 请求,新的 pg_aios
视图可以显示 Postgres 内部状态,即使在使用 io_uring
时也是如此:
SELECT * FROM pg_aios;
pid | io_id | io_generation | state | operation | off | length | target | handle_data_len | raw_result | result | target_desc | f_sync | f_localmem | f_buffered
-------+-------+---------------+--------------+-----------+-----------+--------+--------+-----------------+------------+---------+--------------------------------------------------+--------+------------+------------
91452 | 1 | 4781 | SUBMITTED | read | 996278272 | 131072 | smgr | 16 | | UNKNOWN | blocks 383760..383775 in file "base/16384/16389" | f | f | t
91452 | 2 | 4785 | SUBMITTED | read | 996147200 | 131072 | smgr | 16 | | UNKNOWN | blocks 383744..383759 in file "base/16384/16389" | f | f | t
91452 | 3 | 4796 | SUBMITTED | read | 996409344 | 131072 | smgr | 16 | | UNKNOWN | blocks 383776..383791 in file "base/16384/16389" | f | f | t
91452 | 4 | 4802 | SUBMITTED | read | 996016128 | 131072 | smgr | 16 | | UNKNOWN | blocks 383728..383743 in file "base/16384/16389" | f | f | t
91452 | 5 | 3175 | COMPLETED_IO | read | 995885056 | 131072 | smgr | 16 | 131072 | UNKNOWN | blocks 383712..383727 in file "base/16384/16389" | f | f | t
(5 rows)
在 Postgres 18 中优化 I/O 性能时,了解这些行为变化和异步执行的影响至关重要。
注意:异步 I/O 使 I/O 时间信息难以解释
异步 I/O 引入了执行时间报告方式的转变。当后端不再直接阻塞磁盘读取时(worker
或 io_uring
的情况),执行 I/O 所花费的完整时间可能不会反映在 EXPLAIN ANALYZE
输出中。这可能会使 I/O 绑定的查询看起来比以前需要更少的 I/O 工作。
首先,让我们在 Postgres 17 的冷缓存上在 EXPLAIN ANALYZE
中运行先前的查询:
test=# EXPLAIN (ANALYZE, BUFFERS, TIMING OFF) SELECT COUNT(*) FROM test;
QUERY PLAN
--------------------------------------------------------------------------------------------------------
Aggregate (cost=1692478.40..1692478.41 rows=1 width=8) (actual rows=1 loops=1)
Buffers: shared read=442478
I/O Timings: shared read=14779.316
-> Seq Scan on test (cost=0.00..1442478.32 rows=100000032 width=0) (actual rows=100000001 loops=1)
Buffers: shared read=442478
I/O Timings: shared read=14779.316
Planning:
Buffers: shared hit=13 read=6
I/O Timings: shared read=3.182
Planning Time: 8.136 ms
Execution Time: 18006.405 ms
(11 rows)
我们在 14.8 秒内读取了 442,478 个缓冲区。
现在,我们使用默认设置 (io_method = worker
) 在 Postgres 18 上重复测试:
test=# EXPLAIN (ANALYZE, BUFFERS, TIMING OFF) SELECT COUNT(*) FROM test;
QUERY PLAN
-----------------------------------------------------------------------------------------------------------
Aggregate (cost=1692478.40..1692478.41 rows=1 width=8) (actual rows=1.00 loops=1)
Buffers: shared read=442478
I/O Timings: shared read=7218.835
-> Seq Scan on test (cost=0.00..1442478.32 rows=100000032 width=0) (actual rows=100000001.00 loops=1)
Buffers: shared read=442478
I/O Timings: shared read=7218.835
Planning:
Buffers: shared hit=13 read=6
I/O Timings: shared read=2.709
Planning Time: 2.925 ms
Execution Time: 10480.827 ms
(11 rows)
我们在 7.2 秒内读取了 442,478 个缓冲区。
虽然通过并行查询,我们可以获得所有并行工作进程的 I/O 时间的摘要,但是对于 I/O 工作进程,则不会发生此类摘要。我们看到的是等待 I/O 完成的等待时间,而忽略了可能在幕后发生的任何并行性。
从技术上讲,这不是一个行为改变,因为即使在 Postgres 17 中,报告的时间也是花费在等待 I/O 上的时间,而不是花费在执行 I/O 上的时间,例如,从未考虑用于预读的内核 I/O 时间。
从历史上看,I/O 计时通常等同于 I/O 工作量,而不仅仅是查看共享缓冲区读取计数,以便与 OS 页面缓存命中区分开来。现在,在 Postgres 18 中,解释 I/O 计时需要更加谨慎:异步 I/O 可能会在查询计划中隐藏 I/O 开销。
结论
总而言之,即将发布的 Postgres 18 标志着 I/O 处理方式发生重大演变的开始。虽然目前仅限于读取,但异步 I/O 已经为高延迟云环境中的性能显着提升打开了大门。
但是,其中一些收益伴随着权衡。工程团队将需要调整他们的可观察性实践,学习计时和等待事件的新语义,并且可能需要重新审视先前影响有限的调整参数,例如 effective_io_conurrency
。
总结
- Postgres 18 中对异步 I/O 的支持在新
io_method
设置下引入了worker
(作为默认值)和io_uring
选项。 - 基准测试显示,在云环境中,读取密集型工作负载的吞吐量提高了 2-3 倍。
- 可观察性实践需要发展:
EXPLAIN ANALYZE
可能会低估 I/O 工作量,而像pg_aios
这样的新视图将有助于提供见解。 - 像 pganalyze 这样的工具将适应这些更改,以继续显示相关的性能见解。
随着 Postgres 开发的继续,未来的版本(19 及更高版本)可能会带来异步写入支持,从而进一步减少现代工作负载中的 I/O 瓶颈,并启用 Direct I/O 的生产使用。
参考
- PostgreSQL
io_method
GUC (Postgres 18) - PostgreSQL
effective_io_concurrency
- PostgreSQL Shared Buffers and Buffer Management
pg_stat_activity
Viewpg_stat_io
Viewpg_aios
View (New in Postgres 18)posix_fadvise()
System Call- Linux io_uring Man Page
- 5mins of Postgres: Waiting for Postgres 17: Streaming I/O for sequential scans & ANALYZE