暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

PolarDB PostgreSQL 异步 I/O 特性解析

原创 内核开发者 2025-04-28
324

1. 概述

距离 PostgreSQL 异步 I/O(AIO)v1.0 版本的patch发布已悄然过去了四年,近日master 分支终于迎来了一次备受期待的重要更新(commit da72269)。与此同时,PostgreSQL 还陆续整合了多项与 I/O 性能优化相关的特性。本文将聚焦其中两项关键改进——异步 I/O 和流式 I/O,对其核心内容进行简要解析与探讨。

2. 异步 I/O 基础设施

在 PostgreSQL 中,引入异步 I/O(Asynchronous I/O, AIO)的主要优势在于能够启用直接 I/O(Direct I/O, DIO),从而显著提升吞吐量、降低 CPU 使用率,并有效减少延迟。在异步 I/O 被引入之前,PostgreSQL 主要依赖操作系统来隐藏同步 I/O 的开销,但在预取和回写方面仍存在明显不足。

以 WAL 提交为例,可以直观地对比常规提交与异步 I/O 场景下的差异:

常规写入流程:
并行写入 WAL 缓冲区(WAL buffer) -> 等待缓冲区空洞填充 -> 执行写操作(write)-> 刷盘(flush)。
这一过程存在串行化的等待,容易导致性能受限。

异步 I/O 场景:
并行写入 WAL 缓冲区 -> 不同事务日志并行刷盘(flush)。
在异步 I/O 的支持下,刷盘操作可以DIO执行并且支持并行操作(需要额外的位点管理机制及对可见行的设计)。

在异步 I/O 的 v1.0 版本中(参见相关讨论),尽管初步实现了异步能力,但仍存在一些设计上的局限性,比如:后端进程可能持有大量未释放的 AIO 句柄,导致资源占用过高;缺乏有效的 I/O 合并机制,无法充分利用批量操作的优势;对流式读取的支持不够完善,难以满足复杂场景的需求等等,相比之下,v2.0 版本在多个关键领域进行了显著改进:

●资源管理:优化了 AIO 句柄的分配与回收机制,避免资源泄漏。

●性能优化:引入了高效的 I/O 合并策略,进一步提升了吞吐量。

●错误处理:增强了容错能力,确保系统在异常情况下的稳定性。

●API 设计:提供了更简洁、灵活的接口,便于场景集成与扩展。

这些改进不仅解决了 v1.0 版本中的主要瓶颈,还大幅提升了异步 I/O 功能的稳定性和效率。通过这些优化,PostgreSQL 在复杂 I/O 场景下的表现得到了显著增强,同时也为未来的性能扩展和功能开发奠定了坚实的基础。

2.1子系统设计

AIO v2.0 的核心基础设施围绕两个关键标准展开:

1.Backend 与 I/O 解耦
通过引入  或 (负责进度和结果接管),实现了后端进程与 I/O 操作的分离,从而有效避免了死锁问题。

2.AIO 中间状态存储在共享内存中
这一设计确保了 AIO 状态的全局可见性,同时支持多进程间的高效协作。

此外,AIO v2.0 的核心子系统包含以下几个关键设计模块。

2.1.1 AIO Methods:执行 IO 的方法

目前提供了三种实现方式,并支持未来扩展其他实现:

●:使用 AIO API 模拟同步 I/O。

●:通过专用 Worker 进程同步执行 I/O 操作。

●:基于 Linux 的  机制实现高效的异步 I/O。

2.1.2 AIO Handles:AIO 过程管理的核心控制结构

要执行一个 I/O 操作,首先需要通过  获取句柄(Handle),然后通过  定义具体的操作(如读取或写入文件)。

典型流程如下:上层模块(如 )获取句柄 → 逐层传递到低层模块(如 →  → )→ 最终在中定义具体操作。

注意点:1.句柄资源有限;2.句柄可重用;3.状态及时更新。

2.1.3 AIO Callbacks:状态回调管理

通过将多个回调函数(Callbacks)与句柄绑定,系统可以在 I/O 操作的不同阶段执行特定的任务。例如:

●在  中,更新  状态并验证页面内容。

●在  中,检查 I/O 操作是否成功。

这种设计提高了灵活性,使系统能够根据不同模块的需求定制化处理逻辑,不同回调的作用可见下图描述:



2.1.4 AIO Targets:I/O 对象

用来记录需要IO操作的对象,不同的目标对象由不同的元信息描述,例如:

●常规页面(Page)使用 、 和  进行标识。

●WAL 文件的 I/O 则通过  和  描述。

这种分层设计确保了不同类型的 I/O 对象能够被清晰地管理和操作。

2.1.5 AIO Wait References:I/O 等待过程

对于子系统设计而言,句柄是有限的关键资源,为了实现快速重用,系统设计了以下机制:

●通过  获取 AIO 句柄的引用。

●通过  等待该引用的 I/O 操作完成。

相关函数定义如下:

cstatic PgAioHandle *pgaio_io_from_wref(PgAioWaitRef *iow, uint64 *ref_generation);
static void pgaio_io_wait(PgAioHandle *ioh, uint64 ref_generation);

这种机制确保了句柄资源的高效利用,同时避免了资源竞争和浪费。

2.1.6. AIO Errors:错误处理

错误处理的设计目标是保证系统的稳定性,任何查询都能感知到 I/O 异常。为此,发起 I/O 的进程需满足以下两点

●错误传递机制:发起 I/O 的后端需在本地内存中分配  结构体指针

●结果填充:在 AIO 句柄被复用前,系统将填充该结构体,包含:

■操作状态( 枚举值)

■错误详情(通过  解码)

这种设计增强了系统的容错能力,还可以提供详细的错误诊断信息,便于问题定位和修复,其执行流程如下图所示:





2.2 案例介绍

基于上述子系统的关键结构,以下通过实际场景来分析 AIO v2.0 的使用场景。具体代码描述参考链接(已经有详细的注解),这里概括流程如下:









2.3. 核心方法介绍

基于上述核心结构,PostgreSQL 的 AIO 子系统通过一系列核心方法实现了异步 I/O 处理能力,具体包括以下几个方面:

●I/O 句柄管理:获取、设置、释放 I/O 句柄。

●I/O 操作准备:准备读取、写入、同步等操作。

pgaio_io_prep_readv 和 pgaio_io_prep_writev:-分别为读取和写入操作准备 I/O 句柄。
pgaio_io_get_op 和 pgaio_io_get_op_data:-获取 I/O 句柄的操作类型和相关数据。

●I/O 目标管理:设置和获取 I/O 目标。

pgaio_io_set_target:为 I/O 句柄设置目标对象。
pgaio_io_get_target_description:-获取 I/O 目标的描述信息。

●I/O 回调机制:注册和触发回调函数。

pgaio_io_register_callbacks:-为 I/O 句柄注册回调函数。
pgaio_io_complete_shared 和 pgaio_io_complete_local:-分别在共享内存和本地内存中更新 I/O 操作的状态。

●I/O 等待机制:等待或检查 I/O 操作完成。

●批量 I/O 操作:支持批量提交和处理 I/O 操作。

pgaio_enter_batchmode 和 pgaio_exit_batchmode:-进入和退出批量 I/O 模式。
pgaio_submit_staged:-提交已准备好的批量 I/O 操作。

3. 流式 I/O(Streaming I/O)

流式 I/O 的核心在于通过批量读取和预读机制,取代传统的单个缓冲区随机读操作。这种方式不仅提升了连续读场景下的性能,还为后续异步及并发 I/O 子系统的实现奠定了坚实基础。

3.1 解析

流式 I/O 主要适用于连续读取的场景,例如堆扫描(Heap Scan)和清理操作(Vacuum)。其核心设计是将原本单一阶段的缓冲区读取过程()分解为两个独立的阶段:

1.准备阶段(StartReadBuffer)
在此阶段,初始化并设置必要的参数与条件,为后续的数据加载做好准备。

2.等待阶段(WaitReadBuffers)
该阶段负责处理实际的数据加载及其相关的等待操作,确保数据能够正确加载到缓冲区中。

示例代码如下:

if (StartReadBuffer(&operation,
&buffer,
blockNum,
flags))
WaitReadBuffers(&operation);

其中StartReadBuffer的核心逻辑StartReadBuffersImpl见下面描述。

3.1.2 StartReadBuffer 核心逻辑

 的核心逻辑由  实现,主要包括以下步骤:

for (int i = 0; i < actual_nblocks; ++i)
{
bool found;

buffers[i] = PinBufferForBlock(operation->rel,
operation->smgr,
operation->persistence,
operation->forknum,
blockNum + i,
operation->strategy,
&found);

if (found)
{
/*
* 如果缓冲区已经包含所需数据,则终止读取操作。
*/
actual_nblocks = i + 1;
break;
}
else
{
/* 将当前数据块加入需要读取的范围 */
io_buffers_len++;

/*
* 检查单次 IO 操作可以覆盖多少个连续的数据块。
*/
if (i == 0 && actual_nblocks > 1)
{
maxcombine = smgrmaxcombine(operation->smgr,
operation->forknum,
blockNum);

流程可以概括为:1.缓冲区分配;2.单次IO操作的数据块数量限制;3.IO信息填充;4.预读建议提交;5.函数返回逻辑。

3.1.3WaitReadBuffers 方法解析

 方法用于等待异步读取操作完成,并确保缓冲区的内容被正确加载和验证。其主要操作如下:

if (!WaitReadBuffersCanStartIO(buffers[i], false))
如果数据块已经被其他后端加载完成,则跳过实际的 IO 操作,并将其标记为命中(hit)。
发起 IO 操作:

如果当前数据块尚未加载完成,则调用 smgrreadv 进行批量读取(scatter-read)。

if (!PageIsVerifiedExtended((Page) bufBlock, io_first_block + j,
PIV_LOG_WARNING | PIV_REPORT_STAT))
调用 TerminateLocalBufferIO 或 TerminateBufferIO 设置缓冲区为有效状态(BM_VALID),并唤醒任何等待者。
记录 IO 完成:

4. 总结

PostgreSQL的异步I/O(AIO)功能作为性能优化的关键组成部分,在提高高并发场景下的系统性能方面发挥着核心作用。目前,这一技术正在针对应用场景进行进一步的适配与完善工作,后面也会持续关注,欢迎大家讨论。

最后修改时间:2025-04-28 14:03:37
「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论