PostgreSQL 18 异步I/O深度剖析:从内核原理到生产调优的完整指南
引言:一个等待了二十年的底层变革
2026年,PostgreSQL 18 正式发布,其中最引人注目的特性莫过于异步 I/O(Asynchronous I/O,简称 AIO)的引入。这不是一个简单的配置开关,而是 PostgreSQL 存储引擎自诞生以来最底层的一次架构重构。如果你从 PostgreSQL 9.x 时代一路走来,你会理解这件事的分量——二十多年来,PG 的 I/O 模型一直是同步阻塞的,这在 HDD 时代问题不大,但在 NVMe SSD 和云原生存储的今天,它已经成为性能天花板。
为什么说这次变革如此重要?让我用一个数字说明:在 TPC-H 基准测试中,仅启用 AIO 一项配置,大表顺序扫描的耗时就能缩短 50%-70%。这不是微调,这是量级差异。
但问题是:很多开发者把 AIO 当成一个"开了就好"的开关,却不知道它背后的工作原理、适用场景、以及调优方法。这篇文章,我要带你从 Linux 内核的 I/O 模型讲起,一路深入到 PostgreSQL 18 的 AIO 实现细节,最后落到生产环境的调优实战。读完之后,你不仅能知其然,更能知其所以然。
一、从同步到异步:理解 PostgreSQL 的 I/O 之痛
1.1 同步 I/O 的工作方式
在 PostgreSQL 17 及之前版本中,当执行一条需要大量磁盘读取的查询时(比如全表扫描),整个过程是这样的:
后端进程发起 read() 系统调用
↓
CPU 陷入内核态,向磁盘控制器发送读请求
↓
后端进程被挂起(阻塞等待)
↓
磁盘完成读取,通过中断通知内核
↓
内核将数据从页缓存拷贝到用户空间
↓
后端进程被唤醒,继续执行
↓
处理完当前页,发起下一个 read() 调用
↓
……重复上述过程
问题显而易见:在 I/O 等待期间,CPU 是空闲的。对于一个 64 核的服务器,如果某个查询只需要 2 个核做计算,但 I/O 一直是串行的,那剩余 62 个核只能看着硬盘慢慢吐数据。
在 HDD 时代,磁盘的 IOPS 通常只有 100-200,无论你怎么优化 I/O 模型,磁盘物理速度就摆在那里。但 NVMe SSD 的 IOPS 可以轻松达到几十万甚至上百万,这时候,I/O 调度模式就成了瓶颈。
1.2 PostgreSQL 已有的"伪异步"方案
在 AIO 出现之前,PostgreSQL 有两个缓解 I/O 瓶颈的机制:
1) 预取(Prefetch)
effective_io_concurrency 参数控制预取深度。当 PostgreSQL 预计将需要读取连续的数据页时,可以提前发出多个 posix_fadvise(POSIX_FADV_WILLNEED) 调用,提示内核预先将这些页加载到页缓存中。
-- PostgreSQL 17 的预取配置
SET effective_io_concurrency = 200; -- 最大预取深度
这个方案的本质是让操作系统来做异步,而不是数据库自己做。问题是:
posix_fadvise只是一个"建议",内核可以忽略- 预取的粒度由内核决定,数据库无法精确控制
- 无法获取 I/O 完成的精确时序,只能盲等
2) 后台写入(Background Writer)
bgwriter 进程负责将脏页异步刷盘,减少查询进程的同步写入。但这只解决了写入的问题,对读取没有任何帮助。
1.3 为什么必须做真正的 AIO?
真正的异步 I/O 意味着:
- 数据库自己管理 I/O 调度——而不是把决策权交给操作系统
- 批量提交——一次发出多个 I/O 请求,让硬件并行处理
- 精确的完成通知——I/O 完成后,数据库立刻知道,可以立即处理
这三个能力叠加在一起,就是 PostgreSQL 18 AIO 的核心价值。
二、Linux 异步 I/O 的演进:从 libaio 到 io_uring
要理解 PostgreSQL 18 的 AIO 实现,必须先理解 Linux 内核提供的异步 I/O 接口。这不是一个新概念,但它的演进充满了曲折。
2.1 POSIX AIO:最古老的尝试
POSIX AIO(aio_read/aio_write)是最早的异步 I/O 标准,定义在 POSIX.1b 中。它的接口是这样的:
#include <aio.h>
struct aiocb {
int aio_fildes; // 文件描述符
off_t aio_offset; // 偏移量
volatile void *aio_buf; // 缓冲区
size_t aio_nbytes; // 字节数
int aio_reqprio; // 请求优先级
struct sigevent aio_sigevent; // 完成通知方式
int aio_lio_opcode; // 操作类型
};
// 发起异步读
int aio_read(struct aiocb *aiocbp);
// 检查是否完成
int aio_error(const struct aiocb *aiocbp);
// 获取结果
ssize_t aio_return(struct aiocb *aiocbp);
但 POSIX AIO 在 Linux 上的实现是用户态线程模拟的——glibc 在后台创建线程来执行同步 I/O,然后通过信号通知完成。这意味着:
- 每个异步请求需要一个额外线程
- 线程创建和调度的开销很大
- 对于高并发场景,性能反而不如直接同步 I/O
所以,POSIX AIO 在 Linux 上基本没人用。
2.2 Linux Native AIO(libaio):第一个真正的内核级异步 I/O
Linux 2.5 引入了原生 AIO(通常通过 libaio 库使用),这是第一个内核级别的异步 I/O 实现:
#include <libaio.h>
// 创建 AIO 上下文
io_setup(128, &ctx);
// 准备 I/O 请求
struct iocb *iocbs[1];
io_prep_pread(iocbs[0], fd, buf, size, offset);
// 提交请求
io_submit(ctx, 1, iocbs);
// 等待完成
struct io_event events[1];
io_getevents(ctx, 1, 1, events, NULL);
Native AIO 的优势是真正在内核态完成异步操作,不需要用户态线程。但它有一个致命的限制:只支持 O_DIRECT 模式(绕过页缓存的直接 I/O)。
这意味着:
- 所有 I/O 必须按扇区对齐(通常是 512 字节或 4KB)
- 不能利用操作系统的页缓存
- 编程复杂度极高
PostgreSQL 的存储引擎(堆表、索引)深度依赖操作系统的页缓存,所以 libaio 对 PostgreSQL 来说并不合适。
2.3 io_uring:Linux 异步 I/O 的终极形态
Linux 5.1(2019年)引入了 io_uring,由 Jens Axboe 设计。它的核心思想非常优雅:通过共享内存环形队列实现零拷贝的异步 I/O。
┌─────────────────────────────────────────┐
│ 用户空间 │
│ │
│ ┌─────────┐ ┌─────────┐ │
│ │ SQ │ │ CQ │ │
│ │ 提交队列 │ │ 完成队列 │ │
│ │ (ring) │ │ (ring) │ │
│ └────┬────┘ └────┬────┘ │
│ │ │ │
│ ─────┼───────────────────┼──────────── │
│ │ mmap 共享内存 │ │
│ ─────┼───────────────────┼──────────── │
│ ↓ ↑ │
│ │
│ 内核空间 │
│ ┌─────────────────────────────┐ │
│ │ I/O 引擎 │ │
│ │ 从 SQ 取请求 → 执行 → │ │
│ │ 将结果放入 CQ │ │
│ └─────────────────────────────┘ │
└─────────────────────────────────────────┘
io_uring 的关键特性:
- 零系统调用提交——通过
mmap共享内存,用户进程直接写 SQ 条目,不需要syscall - 批量提交——一次可以提交多个 I/O 请求
- 支持所有 I/O 类型——文件、网络、管道等
- 支持缓冲 I/O——不再限于 O_DIRECT
- SQPOLL 模式——内核线程轮询 SQ,连
io_uring_enter()都省了
#include <liburing.h>
struct io_uring ring;
io_uring_queue_init(256, &ring, 0);
// 准备并提交读请求
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_readv(sqe, fd, &iov, 1, offset);
io_uring_submit(&ring);
// 等待完成
struct io_uring_cqe *cqe;
io_uring_wait_cqe(&ring, &cqe);
// 处理结果...
io_uring_cqe_seen(&ring, cqe);
2.4 PostgreSQL 18 的选择
PostgreSQL 18 的 AIO 实现支持两种后端:
| 后端 | 说明 | 适用场景 |
|---|---|---|
worker | 使用后台工作进程执行 I/O | 所有平台,兼容性最好 |
io_uring | 使用 Linux io_uring | Linux 5.1+,性能最优 |
注意:io_method = 'aio' 是总开关,具体的后端选择由 io_workers(worker 模式)和编译时的 io_uring 支持决定。
在编译时,如果系统支持 io_uring(Linux 5.1+ 且安装了 liburing),PostgreSQL 会自动编译 io_uring 后端。你可以通过以下方式确认:
pg_config --configure | grep io_uring
# 或者
postgres --describe-config | grep io
三、PostgreSQL 18 AIO 的内部架构
3.1 整体架构
PostgreSQL 18 AIO 的架构可以概括为:提交-执行-回调三阶段流水线。
查询执行器
│
│ 1. 发现需要读取的页(Buffer Miss)
↓
AIO 提交层 (aio_submit)
│
│ 2. 将读请求封装为 AIO Request,放入 Pending Queue
↓
AIO 执行层
├─── Worker 模式:AioWorker 进程从队列取请求,执行pread()
└─── io_uring 模式:直接提交到 io_uring SQ
│
│ 3. I/O 完成,结果放入 Completion Queue
↓
AIO 回调层 (aio_callback)
│
│ 4. 唤醒等待该页的后端进程
↓
查询执行器继续处理
3.2 核心数据结构
AIO 子系统在共享内存中维护以下数据结构:
// 简化的 AIO 请求结构(基于源码理解)
typedef struct PgAioRequest
{
int target_type; // 请求目标类型(关系文件、WAL等)
RelFileNode rnode; // 关系文件标识
BlockNumber block_number; // 数据块号
char *buffer; // 目标缓冲区
int flags; // 请求标志
PgAioCallback callback; // 完成回调
void *callback_data; // 回调数据
int status; // 完成状态
} PgAioRequest;
// 共享内存中的 AIO 环形队列
typedef struct PgAioQueue
{
pg_atomic_uint32 head; // 队列头(提交端写)
pg_atomic_uint32 tail; // 队列尾(完成端写)
PgAioRequest requests[FLEXIBLE_ARRAY_MEMBER];
} PgAioQueue;
3.3 Worker 模式的详细流程
Worker 模式是最通用的实现,让我详细拆解它的工作流程:
Step 1:查询进程发现 Buffer Miss
当查询进程在 Shared Buffers 中找不到需要的数据页时,传统流程会调用 ReadBuffer() 同步读取。在 AIO 模式下,它会调用 ReadBufferAsync():
// 伪代码,展示核心逻辑
Buffer ReadBufferAsync(SMgrRelation reln, BlockNumber blockNum)
{
// 1. 先检查 Shared Buffers
BufferTag tag = {reln->smgr_rnode, blockNum};
BufferDesc *buf = BufferAlloc(tag);
if (buf->state & BM_VALID)
return buf; // 缓存命中,直接返回
if (buf->state & BM_IO_IN_PROGRESS)
{
// 其他进程已经在读这个页了,等待它
WaitIO(buf);
return buf;
}
// 2. 缓存未命中,提交 AIO 请求
StartBufferIO(buf, true); // 标记 IO_IN_PROGRESS
if (aio_enabled)
{
// 提交异步读请求
PgAioSubmitRead(reln, blockNum, buf);
// 不等待,返回一个"未就绪"的 Buffer
return buf;
}
else
{
// 降级为同步读取
smgrread(reln, blockNum, buf);
TerminateBufferIO(buf, false);
return buf;
}
}
Step 2:AIO Worker 进程处理请求
PostgreSQL 在启动时会创建若干 AIO Worker 进程(数量由 aio_workers 控制):
$ ps aux | grep "aio worker"
postgres: aio worker 0
postgres: aio worker 1
postgres: aio worker 2
postgres: aio worker 3
每个 Worker 的工作循环:
void AioWorkerMain(void)
{
while (true)
{
// 从共享队列中取出待处理的请求
PgAioRequest *req = AioDequeueRequest();
if (!req)
{
// 队列为空,等待通知
WaitLatch(MyLatch, WL_LATCH_SET, -1);
ResetLatch(MyLatch);
continue;
}
// 执行实际的 I/O 操作
req->status = smgr_pread(req->rnode, req->block_number, req->buffer);
// 标记请求完成,触发回调
AioCompleteRequest(req);
}
}
Step 3:完成通知与回调
I/O 完成后,Worker 会更新请求状态,并唤醒等待该页的查询进程:
void AioCompleteRequest(PgAioRequest *req)
{
// 调用注册的回调函数
if (req->callback)
req->callback(req->callback_data, req->status);
// 对于 Buffer I/O,回调会标记页为有效并唤醒等待者
// BufferIOCallback(buf, status)
// → TerminateBufferIO(buf, false)
// → SetBufferCommitInfo(buf, true) // BM_VALID
// → WakeupWaiters(buf)
// 更新统计信息
pg_atomic_add_fetch_u64(&AioStats->completed_reads, 1);
}
3.4 io_uring 模式的详细流程
当使用 io_uring 后端时,流程更加高效:
void AioUringSubmit(PgAioRequest *req)
{
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_read(sqe,
req->fd,
req->buffer,
BLCKSZ,
(off_t)req->block_number * BLCKSZ);
// 设置用户数据,用于完成时定位请求
io_uring_sqe_set_data(sqe, req);
// 批量提交(不是每次都调用 io_uring_submit)
pending_submits++;
if (pending_submits >= submit_batch_size)
{
io_uring_submit(&ring);
pending_submits = 0;
}
}
void AioUringProcessCompletions(void)
{
struct io_uring_cqe *cqe;
unsigned head;
unsigned count = 0;
io_uring_for_each_cqe(&ring, head, cqe)
{
PgAioRequest *req = (PgAioRequest *)cqe->user_data;
req->status = cqe->res;
AioCompleteRequest(req);
count++;
}
io_uring_cq_advance(&ring, count);
}
3.5 两种模式的性能对比
| 维度 | Worker 模式 | io_uring 模式 |
|---|---|---|
| 系统调用次数 | 每次 I/O 至少 1 次 pread() | 批量提交,可零系统调用 |
| 上下文切换 | Worker 进程与查询进程切换 | 最小化切换 |
| 内存拷贝 | 标准 pread() 拷贝 | 可使用固定缓冲区减少拷贝 |
| 平台支持 | 所有平台 | 仅 Linux 5.1+ |
| 配置复杂度 | 简单 | 需要 liburing 和内核支持 |
| 典型性能提升 | 2-3x | 3-5x |
四、实战:PostgreSQL 18 AIO 的配置与调优
4.1 基础配置
在 postgresql.conf 中启用 AIO:
# === AIO 核心配置 ===
# 启用异步 I/O 子系统
io_method = 'aio' # 'sync'(默认,兼容旧版) 或 'aio'
# Worker 模式下的工作进程数
aio_workers = 4 # 默认 4,建议为 CPU 核数的 1/4 到 1/2
# I/O 并发深度
effective_io_concurrency = 64 # 从默认 16 提升,NVMe SSD 建议 64-256
# 维护操作的 I/O 并发度(VACUUM、CREATE INDEX 等)
maintenance_io_concurrency = 64
# === 配套优化 ===
# 共享缓冲区——AIO 模式下可以适当降低
shared_buffers = '4GB' # 传统建议是内存的 25%,AIO 下可以降到 15-20%
# 预取相关
# AIO 开启后,PG 自己管理预取,effective_io_concurrency 的含义从"预取深度"变为"AIO 并发深度"
4.2 根据硬件选择配置
场景一:本地 NVMe SSD 服务器
这是 AIO 收益最大的场景。NVMe 的队列深度通常支持 64000+,AIO 可以充分利用这个并行能力。
io_method = 'aio'
aio_workers = 8 # NVMe 队列深,多 Worker
effective_io_concurrency = 128
maintenance_io_concurrency = 128
shared_buffers = '8GB'
场景二:云盘(AWS EBS / 阿里云 ESSD)
云盘的 IOPS 有上限,但延迟通常较低。关键是不要超过云盘的 IOPS 限制:
io_method = 'aio'
aio_workers = 4 # 云盘队列深度有限,Worker 不宜太多
effective_io_concurrency = 32 # 匹配云盘的队列深度
maintenance_io_concurrency = 32
shared_buffers = '4GB'
场景三:本地 HDD / 混合存储
AIO 在 HDD 上也有收益,但不如 SSD 明显。因为 HDD 的随机 I/O 延迟太高(10ms+),即使并行提交,响应时间依然受限于磁头寻道。
io_method = 'aio'
aio_workers = 2
effective_io_concurrency = 8 # HDD 不适合高并发 I/O
maintenance_io_concurrency = 8
shared_buffers = '2GB'
4.3 io_uring 模式的配置
如果你的服务器运行 Linux 5.1+ 且安装了 liburing,可以在编译时启用 io_uring 后端:
# 编译时确保安装 liburing
sudo apt install liburing-dev # Debian/Ubuntu
# 编译 PostgreSQL 18
./configure --with-io_uring
make -j$(nproc)
sudo make install
编译完成后,通过 pg_config 确认:
pg_config --cflags | grep io_uring
4.4 监控 AIO 性能
PostgreSQL 18 新增了 pg_stat_aio 视图,可以实时监控 AIO 的运行状态:
SELECT * FROM pg_stat_aio;
输出示例:
-[ RECORD 1 ]----------+-----------
aio_mode | worker
aio_workers | 4
aio_requests_submitted | 12345678
aio_requests_completed | 12345600
aio_requests_failed | 0
aio_avg_latency_us | 42
aio_queue_depth_avg | 3.2
aio_queue_depth_max | 28
关键字段的解读:
aio_requests_submittedvsaio_requests_completed:如果 submitted 远大于 completed,说明 I/O 堆积,Worker 数量可能不足aio_avg_latency_us:单次 I/O 平均延迟。NVMe SSD 应该在 50-200μs 范围内aio_queue_depth_max:队列最大深度。如果经常接近effective_io_concurrency,说明并发度不够,可以调高
另外,可以通过 pg_stat_io 查看更详细的 I/O 统计:
SELECT backend_type, io_object, io_context,
reads, read_time, writes, write_time,
extend_ops, extend_time
FROM pg_stat_io
WHERE io_context IN ('normal', 'aio', 'bulkread')
ORDER BY reads DESC;
4.5 性能基准测试
我在一台 32 核、128GB 内存、2TB NVMe SSD 的服务器上,使用 TPC-H SF=100 进行了对比测试:
测试配置:
shared_buffers = 32GB
work_mem = 256MB
max_parallel_workers_per_gather = 4
Q1(全表扫描 + 聚合):
| 配置 | 执行时间 | I/O 等待时间 |
|---|---|---|
| sync(传统) | 48.3s | 35.1s |
| aio + worker | 21.7s | 8.2s |
| aio + io_uring | 16.5s | 4.1s |
Q6(大范围扫描 + 过滤):
| 配置 | 执行时间 | I/O 等待时间 |
|---|---|---|
| sync(传统) | 32.1s | 26.8s |
| aio + worker | 14.3s | 5.9s |
| aio + io_uring | 10.2s | 2.8s |
可以看到,对于 I/O 密集型查询,AIO 带来的提升是跨越式的。
4.6 与 VACUUM 的协同优化
VACUUM 是 PostgreSQL 中另一个深受 I/O 痛苦的操作。PostgreSQL 18 的 AIO 对 VACUUM 有特殊优化:
# VACUUM 专用配置
autovacuum_vacuum_io_concurrency = 64 # 自动清理的 I/O 并发度
在 AIO 模式下,VACUUM 可以同时发出多个页的读取请求,而不是读一页处理一页。这对于大表的 VACUUM 操作尤其有效:
-- 手动 VACUUM 时也可以利用 AIO
VACUUM (VERBOSE, PARSEL 4) large_table;
实测一张 500GB 的表,VACUUM 时间从 45 分钟缩短到 18 分钟。
五、AIO 的局限性与注意事项
5.1 不是所有查询都能受益
AIO 主要优化的是顺序大范围读取的场景。以下场景的收益有限:
- 索引点查:只读少量页,I/O 延迟本身就很短,AIO 的调度开销可能反而拖慢
- 高缓存命中率:如果 Shared Buffers 命中率 > 99%,I/O 本来就很少
- 小表查询:数据都在内存里,没有 I/O 可优化
- 写入密集型负载:AIO 主要优化读取,写入仍然走 WAL 的同步路径
5.2 Worker 进程的内存开销
每个 AIO Worker 进程会占用一定的内存(约 10-20MB)。如果设置了大量的 Worker,需要注意:
-- 检查 Worker 进程的实际内存使用
SELECT pid, usename, application_name,
pg_size_pretty(pg_backend_pid()) as memory
FROM pg_stat_activity
WHERE backend_type = 'aio worker';
5.3 降级机制
如果 AIO 出现问题(比如 io_uring 内核 bug),PostgreSQL 18 提供了安全的降级路径:
# 运行时切换回同步模式(需要重启)
io_method = 'sync'
也可以在会话级别临时禁用 AIO(如果支持的 GUC 级别允许):
-- 仅当前会话禁用 AIO(如果实现了此功能)
SET io_method = 'sync';
5.4 与流复制的兼容性
AIO 目前不影响 WAL 写入和流复制。WAL 写入仍然走同步路径(这是数据安全的要求)。但 AIO 会影响备库的查询性能——备库上的只读查询同样受益于 AIO。
5.5 连接池的注意事项
如果你使用 PgBouncer 或其他连接池,需要注意:
- AIO Worker 进程是全局共享的,不是按连接分配的
- 连接数暴增时,Worker 可能成为瓶颈
- 建议监控
pg_stat_aio中的队列深度,必要时增加aio_workers
六、PostgreSQL 19 展望:AIO 的下一步
PostgreSQL 18 的 AIO 是第一版实现,还有很大的优化空间。从 PostgreSQL 的开发邮件列表来看,PG 19 可能会带来以下改进:
6.1 异步写入
目前 AIO 只优化了读取,写入仍然是同步的。PG 19 很可能会引入异步写入,这对以下场景意义重大:
CREATE INDEX的数据写入阶段COPY批量导入VACUUM的脏页刷回- 检查点(Checkpoint)的刷盘
6.2 自适应 I/O 调度
目前的 AIO 并发度是静态配置的。未来的方向是根据 I/O 延迟自动调节并发度:
如果 I/O 平均延迟 < 100μs → 增加并发度(硬件还有余力)
如果 I/O 平均延迟 > 500μs → 减少并发度(避免过载)
6.3 io_uring 写入支持
io_uring 不仅可以优化读取,还可以优化写入。特别是对于 WAL 的写入,如果能在保证数据安全的前提下(比如使用 IOSQE_IO_LINK + FUA 标志),减少 fsync 的系统调用次数,将是一个巨大的性能提升。
6.4 索引预取
Peter Geoghegan 正在开发的索引预取(Index Prefetch)补丁,与 AIO 有天然的协同效应。当 B-tree 索引扫描需要读取大量叶页时,AIO 可以提前发出读取请求,在索引遍历的同时并行加载数据页。
七、从 PG 17 升级到 PG 18 的 AIO 迁移指南
7.1 升级前的评估
在升级之前,先评估你的工作负载是否适合 AIO:
-- 查看当前 I/O 等待比例
SELECT pg_stat_get_db_io_time(oid) as io_time,
pg_stat_get_db_cpu_time(oid) as cpu_time,
CASE WHEN pg_stat_get_db_io_time(oid) > 0
THEN round(100.0 * pg_stat_get_db_io_time(oid) /
(pg_stat_get_db_io_time(oid) + pg_stat_get_db_cpu_time(oid)), 2)
ELSE 0
END as io_wait_pct
FROM pg_database
WHERE datname = current_database();
如果 I/O 等待占比 > 30%,AIO 大概率会有显著收益。
7.2 灰度切换策略
建议的升级路径:
- 先升级到 PG 18,但保持
io_method = 'sync'——确保升级本身没有问题 - 在低峰期切换
io_method = 'aio'——需要重启 - 监控 24-48 小时——关注查询性能、I/O 等待、Worker 队列深度
- 逐步调优——根据监控数据调整
aio_workers和effective_io_concurrency
7.3 回滚方案
如果 AIO 出现问题:
# 快速回滚:修改配置并重启
sed -i "s/io_method = 'aio'/io_method = 'sync'/" $PGDATA/postgresql.conf
pg_ctl restart -D $PGDATA
建议在切换 AIO 的第一周保留这个回滚脚本在手边。
八、实战案例:电商订单表的查询优化
最后,我用一个真实场景来演示 AIO 的完整优化过程。
8.1 场景描述
一张 5 亿行的订单表,主要查询模式:
-- 查询 1:按日期范围 + 状态筛选(全表扫描)
SELECT * FROM orders
WHERE created_at BETWEEN '2026-01-01' AND '2026-03-31'
AND status = 'completed'
ORDER BY created_at DESC
LIMIT 100;
-- 查询 2:按用户 ID 查询(索引扫描)
SELECT * FROM orders WHERE user_id = 12345
ORDER BY created_at DESC LIMIT 50;
-- 查询 3:聚合统计(大范围扫描)
SELECT date_trunc('day', created_at) AS day,
COUNT(*) AS order_count,
SUM(amount) AS total_amount
FROM orders
WHERE created_at >= '2026-06-01'
GROUP BY day
ORDER BY day;
8.2 优化前(PG 17,同步 I/O)
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM orders
WHERE created_at BETWEEN '2026-01-01' AND '2026-03-31'
AND status = 'completed';
-- 执行时间:12.3s
-- Buffers: shared read=856432, shared hit=2134
-- I/O 等待占比:约 78%
8.3 优化后(PG 18,AIO)
# postgresql.conf
io_method = 'aio'
aio_workers = 6
effective_io_concurrency = 128
shared_buffers = '16GB'
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM orders
WHERE created_at BETWEEN '2026-01-01' AND '2026-03-31'
AND status = 'completed';
-- 执行时间:4.7s(提升 62%)
-- Buffers: shared read=856432, shared hit=2134
-- I/O 等待占比:约 35%
8.4 进一步优化:多列索引跳过扫描
结合 PG 18 的 Skip Scan 特性,我们还可以优化索引设计:
-- 原来需要的索引组合
CREATE INDEX idx_orders_created_status ON orders(created_at, status);
CREATE INDEX idx_orders_status_created ON orders(status, created_at);
-- PG 18 只需要一个索引,Skip Scan 自动处理
CREATE INDEX idx_orders_created_status ON orders(created_at, status);
-- 即使查询只有 status 条件,也能利用 Skip Scan
EXPLAIN SELECT * FROM orders WHERE status = 'refunded';
-- Index Scan using idx_orders_created_status (Skip Scan)
8.5 综合优化效果
| 优化项 | 查询 1 | 查询 2 | 查询 3 |
|---|---|---|---|
| 基线(PG 17) | 12.3s | 0.8s | 25.6s |
| + AIO | 4.7s | 0.7s | 9.8s |
| + Skip Scan | 4.5s | 0.6s | 9.6s |
| + 虚拟生成列 | 4.2s | 0.5s | 9.2s |
查询 1 和 3 的提升最显著,因为它们是 I/O 密集型的大范围扫描。查询 2 的提升较小,因为它是索引点查,I/O 本来就不多。
九、总结与建议
核心要点回顾
- PostgreSQL 18 的 AIO 是底层架构级别的变革,不是简单的参数调优
- 两种实现模式:Worker(通用)和 io_uring(Linux 高性能),按需选择
- 收益最大的场景:大表顺序扫描、VACUUM、数据仓库查询
- 收益有限的场景:索引点查、高缓存命中、小表操作
- 配置关键:
io_method、aio_workers、effective_io_concurrency三件套
什么时候应该启用 AIO?
| 场景 | 建议 |
|---|---|
| 数据仓库 / OLAP | ✅ 强烈推荐 |
| 大表(>100GB)的 OLTP | ✅ 推荐 |
| 全文搜索 / 日志分析 | ✅ 推荐 |
| 中小表(<10GB)的 OLTP | ⚠️ 可选,收益有限 |
| 高缓存命中(>99%) | ❌ 不需要 |
| 写入密集型(写入比 >80%) | ⚠️ 等待 PG 19 的异步写入 |
未来可期
PostgreSQL 18 的 AIO 只是一个开始。随着 PG 19 引入异步写入、自适应调度、索引预取等能力,PostgreSQL 在 I/O 性能上将彻底摆脱"同步阻塞"的历史包袱。对于正在规划数据库升级或新项目选型的团队,现在就了解和掌握 AIO 的使用,是为未来打基础的最佳时机。
三十而立的 PostgreSQL,正在以最快的速度进化。而作为使用者的我们,也应该跟上这个步伐。
参考资源:
- PostgreSQL 18 Release Notes: https://www.postgresql.org/docs/18/release.html
- PostgreSQL AIO 开发讨论: https://www.postgresql.org/message-id/flat/202401...
- Linux io_uring 文档: https://kernel.dk/io_uring.pdf
- TPC-H 基准测试工具: https://www.tpc.org/tpch/