编程 PostgreSQL 18 异步I/O深度剖析:从内核原理到生产调优的完整指南

2026-06-19 08:56:28 +0800 CST views 19

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 意味着:

  1. 数据库自己管理 I/O 调度——而不是把决策权交给操作系统
  2. 批量提交——一次发出多个 I/O 请求,让硬件并行处理
  3. 精确的完成通知——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 的关键特性:

  1. 零系统调用提交——通过 mmap 共享内存,用户进程直接写 SQ 条目,不需要 syscall
  2. 批量提交——一次可以提交多个 I/O 请求
  3. 支持所有 I/O 类型——文件、网络、管道等
  4. 支持缓冲 I/O——不再限于 O_DIRECT
  5. 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_uringLinux 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-3x3-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_submitted vs aio_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.3s35.1s
aio + worker21.7s8.2s
aio + io_uring16.5s4.1s

Q6(大范围扫描 + 过滤):

配置执行时间I/O 等待时间
sync(传统)32.1s26.8s
aio + worker14.3s5.9s
aio + io_uring10.2s2.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 主要优化的是顺序大范围读取的场景。以下场景的收益有限:

  1. 索引点查:只读少量页,I/O 延迟本身就很短,AIO 的调度开销可能反而拖慢
  2. 高缓存命中率:如果 Shared Buffers 命中率 > 99%,I/O 本来就很少
  3. 小表查询:数据都在内存里,没有 I/O 可优化
  4. 写入密集型负载: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 灰度切换策略

建议的升级路径:

  1. 先升级到 PG 18,但保持 io_method = 'sync'——确保升级本身没有问题
  2. 在低峰期切换 io_method = 'aio'——需要重启
  3. 监控 24-48 小时——关注查询性能、I/O 等待、Worker 队列深度
  4. 逐步调优——根据监控数据调整 aio_workerseffective_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.3s0.8s25.6s
+ AIO4.7s0.7s9.8s
+ Skip Scan4.5s0.6s9.6s
+ 虚拟生成列4.2s0.5s9.2s

查询 1 和 3 的提升最显著,因为它们是 I/O 密集型的大范围扫描。查询 2 的提升较小,因为它是索引点查,I/O 本来就不多。

九、总结与建议

核心要点回顾

  1. PostgreSQL 18 的 AIO 是底层架构级别的变革,不是简单的参数调优
  2. 两种实现模式:Worker(通用)和 io_uring(Linux 高性能),按需选择
  3. 收益最大的场景:大表顺序扫描、VACUUM、数据仓库查询
  4. 收益有限的场景:索引点查、高缓存命中、小表操作
  5. 配置关键io_methodaio_workerseffective_io_concurrency 三件套

什么时候应该启用 AIO?

场景建议
数据仓库 / OLAP✅ 强烈推荐
大表(>100GB)的 OLTP✅ 推荐
全文搜索 / 日志分析✅ 推荐
中小表(<10GB)的 OLTP⚠️ 可选,收益有限
高缓存命中(>99%)❌ 不需要
写入密集型(写入比 >80%)⚠️ 等待 PG 19 的异步写入

未来可期

PostgreSQL 18 的 AIO 只是一个开始。随着 PG 19 引入异步写入、自适应调度、索引预取等能力,PostgreSQL 在 I/O 性能上将彻底摆脱"同步阻塞"的历史包袱。对于正在规划数据库升级或新项目选型的团队,现在就了解和掌握 AIO 的使用,是为未来打基础的最佳时机。

三十而立的 PostgreSQL,正在以最快的速度进化。而作为使用者的我们,也应该跟上这个步伐。


参考资源:

复制全文 生成海报 PostgreSQL AIO 异步I/O io_uring 性能优化

推荐文章

PHP 唯一卡号生成
2024-11-18 21:24:12 +0800 CST
Python 基于 SSE 实现流式模式
2025-02-16 17:21:01 +0800 CST
Nginx负载均衡详解
2024-11-17 07:43:48 +0800 CST
使用Vue 3和Axios进行API数据交互
2024-11-18 22:31:21 +0800 CST
js一键生成随机颜色:randomColor
2024-11-18 10:13:44 +0800 CST
程序员茄子在线接单