PostgreSQL码农集散地

PG 19 异步 I/O 与缓冲区管理深度优化

PG 19 异步 I/O 与缓冲区管理优化

本文档解读 Andres Freund 提交的异步 I/O(io_uring)和缓冲区管理优化,这些改进提升了大规模 I/O 操作时的性能。

相关 Commit

Commit
描述
41d3d64e87af
bufmgr: Don't copy pages while writing out
f39cb8c01106
bufmgr: Make UnlockReleaseBuffer() more efficient
8df3c48e466c
Use UnlockReleaseBuffer() in more places
c0af4eb4e71e
bufmgr: Fix ordering of checks in PinBuffer()
513374a47a71
bufmgr: Return whether WaitReadBuffers() needed to wait

核心价值

  • io_uring 支持:Linux 原生异步 I/O,减少等待
  • 减少复制:页面写出时不再复制
  • 优化锁释放:更高效的缓冲区解锁

原理详解

1. 缓冲区管理器架构

1.1 核心结构 (src/backend/storage/buffer/buf_internals.h)

typedefstructBufferDesc
{

int         buf_id;           /* 缓冲区 ID */
int         state;             /* 状态标志 */
slock_t     mutex;             /* 保护此结构的锁 */
    LWLock       *content_lock;    /* 内容锁 */

/* 标识信息 */
    Oid         relfilenode;       /* 物理文件标识 */
    ForkNumber  forkNum;
    BlockNumber blockNum;
    LogicalRelFileNode rnode;
} BufferDesc;

1.2 缓冲区状态

#define BM_LOCKED           BUF_DEFINE_FLAG(0)   /* 正在被 I/O */
#define BM_DIRTY            BUF_DEFINE_FLAG(1)   /* 脏页,需要写出 */
#define BM_VALID            BUF_DEFINE_FLAG(2)   /* 数据有效 */
#define BM_IO_IN_PROGRESS   BUF_DEFINE_FLAG(4)   /* I/O 进行中 */
#define BM_IO_ERROR         BUF_DEFINE_FLAG(5)   /* I/O 错误 */

2. UnlockReleaseBuffer 优化

2.1 优化前 (f39cb8c01106 之前)

/*
 * 优化前:分离操作
 */

UnlockBuffer();         /* 第一次调用:释放内容锁 */
ReleaseBuffer();        /* 第二次调用:释放 pin,减少引用计数 */

2.2 优化后 (f39cb8c01106)

/*
 * 优化后:合并操作
 * 减少函数调用开销和锁操作
 */

UnlockReleaseBuffer(BufferDesc *buf)
{
/* 同时释放内容锁和 pin */
    LockBuffer(buf, BUFFER_LOCK_UNLOCK);
    UnpinBuffer(buf, true);
}

2.3 使用场景 (8df3c48e466c)

现在在以下场景使用 UnlockReleaseBuffer():

/* 页面写出后 */
if (buf == InvalidBuffer)
{
    UnlockReleaseBuffer(currentBuffer);
    currentBuffer = InvalidBuffer;
}

/* 错误处理路径 */
UnlockReleaseBuffer(currentBuffer);

3. 脏页写出优化

3.1 问题 (41d3d64e87af 之前)

/*
 * 优化前:写出时复制页面
 * 会产生一次额外的内存复制
 */

Page         tmppage = palloc(BLCKSZ);
CopyPage(page, tmppage);          /* 复制到临时页面 */
log_newpage_buffer(tmppage, true); /* 写出临时页面 */
pfree(tmppage);

3.2 优化后

/*
 * 优化后:直接写出页面
 * 无需复制
 */

START_CRIT_SECTION();
MarkBufferDirty(buf);
log_newpage_buffer(buf, true);     /* 直接写出页面 */
END_CRIT_SECTION();

4. io_uring 异步 I/O

4.1 核心接口 (src/backend/storage/aio/)

/*
 * PgAioHandle - AIO 操作句柄
 * 定义于 src/include/storage/aio_internal.h
 */

structPgAioHandle
{

    uint8   state;           /* 状态 */
    uint8   target;          /* 操作目标 */
    uint8   op;              /* 操作类型 (PGAIO_OP_READV, PGAIO_OP_WRITEV) */
    uint8   flags;           /* 标志位 */
// ...
    int32   result;          /* IO 操作原始结果 */
    PgAioOpData op_data;     /* 操作数据 */
    PgAioTargetData target_data;  /* 目标数据 */
};

/*
 * PgAioUringContext - io_uring 实例上下文
 * 每个后端有一个 io_uring 实例
 */

typedefstructPgAioUringContext
{

    LWLock       completion_lock;         /* 保护完成队列 */
structio_uringio_uring_ring;/* io_uring 实例 */
} PgAioUringContext;

4.2 异步处理判断 (pgaio_uring_should_use_async())

/*
 * 决定是否使用异步处理
 * 位于 src/backend/storage/aio/method_io_uring.c
 */

staticbool
pgaio_uring_should_use_async(PgAioHandle *ioh, size_t io_size)
{
/*
     * 直接 IO 不需要强制异步处理
     */

if (!(ioh->flags & PGAIO_HF_BUFFERED))
returnfalse;

/*
     * IO 队列深度较大时,异步处理开销更小
     */

if (dclist_count(&pgaio_my_backend->in_flight_ios) > 4)
returntrue;

/*
     * 大 IO 时,并行内存拷贝收益更大
     * 阈值: 4 * BLCKSZ (通常是 4 * 8192 = 32768 bytes)
     */

if (io_size >= (BLCKSZ * 4))
returntrue;

returnfalse;
}

4.3 提交 IO (pgaio_uring_submit())

/*
 * 提交 IO 到 io_uring
 * 位于 src/backend/storage/aio/method_io_uring.c line 415
 */

staticint
pgaio_uring_submit(uint16 num_staged_ios, PgAioHandle **staged_ios)
{
structio_uring *uring_instance = &pgaio_my_uring_context->io_uring_ring;

for (int i = 0; i < num_staged_ios; i++)
    {
        PgAioHandle *ioh = staged_ios[i];
structio_uring_sqe *sqe;

        sqe = io_uring_get_sqe(uring_instance);
        pgaio_uring_sq_from_io(ioh, sqe);
    }

/* 提交到内核 */
    ret = io_uring_submit(uring_instance);
// ...
}

4.4 异步标志设置

/*
 * 对于大 IO,设置 IOSQE_ASYNC 标志
 * 这让 io_uring 在后台线程执行 IO
 */

if (pgaio_uring_should_use_async(ioh, io_size))
    io_uring_sqe_set_flags(sqe, IOSQE_ASYNC);

5. PinBuffer 顺序修复 (c0af4eb4e71e)

5.1 问题背景

在 PinBuffer() 函数中,锁获取的顺序非常重要。错误的顺序可能导致死锁或条件变量信号丢失。

5.2 修复内容

修复涉及调整 PinBuffer() 中检查 BM_VALID 和 BM_LOCKED 标志的顺序,确保在 buffer header spinlock 被持有时不会尝试等待条件变量。

staticbool
PinBuffer(BufferDesc *buf, BufferAccessStrategy strategy,
bool skip_if_not_valid)

{
// ...

    old_buf_state = pg_atomic_read_u64(&buf->state);
for (;;)
    {
/* 先检查是否需要跳过非有效缓冲区 */
if (unlikely(skip_if_not_valid && !(old_buf_state & BM_VALID)))
returnfalse;

/* 如果 buffer 正在被 I/O 操作,spinlock 会被持有,此时不能等待 */
if (unlikely(old_buf_state & BM_LOCKED))
        {
            old_buf_state = WaitBufHdrUnlocked(buf);
continue;
        }

/* 执行原子操作增加引用计数 */
        buf_state = old_buf_state;
        buf_state += BUF_REFCOUNT_ONE;
// ...
    }
}

关键点:当 BM_LOCKED 标志被设置时,表示有其他进程正在对该 buffer 进行 I/O 操作,此时 spinlock 会被持有。应使用 WaitBufHdrUnlocked() 等待,而不是直接操作。

6. WaitReadBuffers 返回值 (513374a47a71)

/*
 * 函数现在返回是否实际等待了
 */

staticbool
WaitReadBuffers(List *buffers)
{
bool waited = false;

    foreach(cell, buffers)
    {
        BufferDesc *buf = (BufferDesc *) lfirst(cell);

if (buf->state & BM_IO_IN_PROGRESS)
        {
            waited = true;  /* 需要等待 */
            WaitIO(buf);
        }
    }

return waited;  /* 返回是否等待了 */
}


使用实践

1. 监控缓冲区活动

-- 查看缓冲区统计
SELECT
    buffers_alloc,
    buffers_checkpoint,
    buffers_clean,
    buffers_backend,
    buffers_backend_fsync
FROM pg_stat_bgwriter;

-- 查看共享缓冲区使用
SELECT
count(*) as total_buffers,
count(*) FILTER (WHERE state & 2 != 0) as dirty_buffers,
count(*) FILTER (WHERE state & 16 != 0) as io_in_progress
FROM pg_buffer_descriptors;

2. 调优参数

# 共享缓冲区大小
shared_buffers = 8GB

# 后端写出阈值
backend_write_threshold = 0.25

# 检查点触发写出
checkpoint_completion_target = 0.9

# 异步 I/O(如果内核支持)
effective_io_concurrency = 200

# 同步 I/O 行为
synchronous_commit = on

3. 监控 AIO 使用

-- 查看异步 I/O 配置
SHOW effective_io_concurrency;

-- 查看系统异步 I/O 能力
-- Linux: cat /proc/sys/fs/aio-max-nr

4. 性能诊断

4.1 高 I/O 等待

-- 查看正在等待 I/O 的查询
SELECT
    pid,
    state,
    wait_event_type,
    wait_event,
query
FROM pg_stat_activity
WHERE state != 'idle'
AND wait_event_type = 'IO';

4.2 脏缓冲区积压

-- 查看脏缓冲区数量
SELECT
    datname,
    n_秧nserted,
    n_dead_tup,
    n_mod_since_analyze
FROM pg_stat_database;

-- 查看检查点活动
SELECT * FROM pg_stat_checkpoints;

5. Linux io_uring 配置

# 检查 io_uring 支持
cat /proc/sys/kernel/osfaddr

# 如果需要,调整限制
echo 1000000 > /proc/sys/fs/aio-max-nr

6. 常见问题处理

6.1 高缓冲区命中率

-- 如果命中率低,考虑增加 shared_buffers
SELECT
sum(blks_hit) as hits,
sum(blks_read) asreads,
sum(blks_hit) * 100.0 / NULLIF(sum(blks_hit) + sum(blks_read), 0) as hit_ratio
FROM pg_stat_database;

6.2 检查点 I/O 尖峰

# 使用更平滑的检查点
checkpoint_completion_target = 0.95

# 增加检查点缓冲区
checkpoint_timeout = 15min


总结

异步 I/O 和缓冲区管理优化对 PostgreSQL 的 I/O 性能有显著影响。

关键要点:

  1. UnlockReleaseBuffer:合并锁释放操作,减少开销
  2. 脏页写出:消除不必要的页面复制
  3. io_uring:支持 Linux 原生异步 I/O
  4. 避免重复等待:I/O 已进行时不重复等待

对 DBA 的影响:

  1. 高 I/O 场景下性能提升
  2. 检查点期间更平滑的 I/O
  3. 更好的大规模数据操作性能
  4. 无需额外配置,自动生效