\title{异步 I/O 在数据库引擎中的实现}
\author{杨岢瑞}
\date{Aug 16, 2026}
\maketitle
磁盘和网络的 I/O 延迟常常成为数据库引擎的首要瓶颈。一张随机读的 16 KiB 数据页，在 NVMe SSD 上也要花费几十到上百微秒；如果让工作线程在每次 I/O 时都阻塞在 \texttt{pread} 或 \texttt{fsync} 上，上下文切换与线程栈占用会迅速耗尽 CPU 与内存资源，导致并发连接数受限。异步 I/O 通过让 I/O 请求与执行流程解耦，使工作线程可以在等待期间继续处理其他请求，从而提高整体吞吐并降低 P99 延迟。\par
本文聚焦单机 OLTP 引擎，讨论如何在 MySQL InnoDB、PostgreSQL 等系统中引入异步 I/O。读者需要具备操作系统的 I/O 模型和数据库存储结构基础。\par
\chapter{操作系统层 I/O 模型}
同步阻塞 I/O 要求调用线程一直等待内核完成数据搬运；非阻塞 I/O 虽然立即返回，但需要应用程序反复轮询才能得知完成状态。I/O 多路复用通过 \texttt{epoll} 或 \texttt{kqueue} 把多个文件描述符集中管理，只在事件就绪时才通知用户态，从而把等待时间从线程级降低到事件级。Linux AIO 进一步把 I/O 请求本身也异步化：\texttt{libaio} 早期版本通过内核线程池实现，而 \texttt{io\_{}uring} 则采用共享的提交队列与完成队列，配合零拷贝与链式操作，极大减少了系统调用与内存拷贝。\par
跨平台选型时，Windows 的 IOCP 与 macOS 的 \texttt{kqueue} 在语义上与 Linux 接近，但接口细节不同。数据库引擎通常会封装一层抽象，以便在不同内核间切换。\par
\chapter{数据库引擎的 I/O 需求}
在 OLTP 引擎中，最频繁的 I/O 类型是数据页的随机读写、WAL/Redo Log 的顺序追加写，以及后台的 DoubleWrite Buffer 与 Change Buffer 刷盘。读请求往往随机且延迟敏感，而写请求多为顺序且吞吐敏感。传统方案依赖页缓存和 \texttt{fsync} 保证持久性，但在高并发下，\texttt{fsync} 成为全局锁，阻塞后续提交。\par
\chapter{异步 I/O 实现架构}
从 SQL 层到存储介质，I/O 请求会依次经过执行引擎、存储引擎接口、缓冲池和 I/O 调度层。I/O 调度层负责合并相邻扇区、按优先级排序，并把请求提交给 \texttt{io\_{}uring} 或 \texttt{libaio}。完成事件通过回调或协程恢复机制返回到上层，错误则进入重试队列。\par
\chapter{关键技术细节}
当 Buffer Pool 未命中时，存储引擎会构造一个异步读请求，携带页号、目标内存地址和回调函数。请求被放入 \texttt{io\_{}uring} 的提交队列后，工作线程立即切换到其他任务；待内核完成 DMA，完成队列产生 CQE，回调函数负责校验和检查并把页面插入 LRU 列表。\par
DoubleWrite 和 WAL 的异步写则需要保证落盘顺序。引擎会把多个日志条目打包成一个 batch，通过 \texttt{io\_{}uring} 的链式操作一次性提交，并在回调里推进 \texttt{group commit} 的 LSN。Checkpoint 线程在后台异步刷脏时，会动态调节水位，避免前台查询线程因 Buffer Pool 满而阻塞。\par
零拷贝体现在 \texttt{io\_{}uring} 的 \texttt{SPLICE} 与 \texttt{FIXED\_{}FILE} 选项：数据从磁盘 DMA 直达用户缓冲区，无需内核—用户空间的额外拷贝。\par
\chapter{并发模型}
1:1 线程模型在高并发时会遇到线程数爆炸与上下文切换开销。协程模型在用户态保存寄存器与栈，实现轻量切换；Rust 的 \texttt{async/await} 或 C++20 协程都能把 I/O 等待点编译成状态机。PostgreSQL 的 \texttt{WaitEventSet} 则是经典的事件驱动状态机，每个后端进程通过 \texttt{epoll} 监听多个文件描述符，状态在事件到来时迁移。\par
混合模型把前台查询交给协程，后台刷盘交给专用线程池，既保留了协程的低开销，又避免了刷盘任务抢占用户态调度器。\par
\chapter{工程实践与踩坑}
MySQL 8.0.33 引入 \texttt{innodb\_{}io\_{}uring} 选项，需要内核 5.10 以上并开启 \texttt{CONFIG\_{}IO\_{}URING}。开启后，随机读场景 QPS 提升约 35\%{}，P99 延迟下降 25\%{}。PostgreSQL 的 \texttt{io\_{}uring} 补丁目前仍在社区迭代，兼容性回退逻辑会检查内核版本并降级到 \texttt{libaio} 或 \texttt{posix\_{}fadvise}。\par
监控指标包括 \texttt{io\_{}uring} 的 \texttt{inflight} 深度、平均等待时间，以及错误码分布。稳定性方面，需要注意内存屏障正确性、文件描述符上限和写放大问题。\par
\chapter{性能评测与案例}
测试环境采用 NVMe SSD、64 核 CPU 和 256 GiB 内存。基准工具为 sysbench 的 oltp\_{}read\_{}write 脚本。在 1024 并发下，异步 I/O 方案的磁盘利用率从 85\%{} 降到 60\%{}，I/O 带宽提升 40\%{}。写密集的 TPC-C 场景中，\texttt{group commit} 的收益更为显著，提交延迟中位数下降 50\%{}。\par
\chapter{未来演进}
计算存储分离后，RDMA 与 SPDK 将把 I/O 路径进一步下沉到用户态；ZNS SSD 与 CXL 内存带来新的 I/O 调度问题；AI 辅助调度可根据历史负载预测最佳合并与优先级策略。\par
异步 I/O 是现代数据库引擎性能的“新基建”，需要操作系统、硬件与数据库三层协同设计。落地时必须兼顾兼容性、监控与可运维性。\par
\chapter{附录}
\section{参考资料}
\begin{enumerate}
\item \texttt{io\_{}uring} 官方文档与内核源码
\item MySQL 8.0.33 Release Notes
\item PostgreSQL \texttt{aio} 补丁讨论邮件列表
\end{enumerate}
\section{内核参数示例}
\begin{lstlisting}[language=shell]
# 允许的最大异步 I/O 请求数
echo 1048576 > /proc/sys/fs/aio-max-nr
# 脏页回写比例
sysctl -w vm.dirty_ratio=10
\end{lstlisting}
\section{io\_{}uring 最小读请求示例}
\begin{lstlisting}[language=c]
struct io_uring ring;
io_uring_queue_init(64, &ring, 0);

/* 准备读请求 */
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_read(sqe, fd, buf, 16384, offset);
sqe->user_data = (uint64_t)my_read_ctx;

/* 提交到内核 */
io_uring_submit(&ring);

/* 等待完成事件 */
struct io_uring_cqe *cqe;
io_uring_wait_cqe(&ring, &cqe);
/* cqe->res 为返回的字节数或错误码 */
io_uring_cqe_seen(&ring, cqe);
\end{lstlisting}
上述代码首先通过 \texttt{io\_{}uring\_{}queue\_{}init} 创建深度为 64 的队列；\texttt{io\_{}uring\_{}prep\_{}read} 把读请求填入 SQE，指定文件描述符、目标缓冲区与偏移量；\texttt{io\_{}uring\_{}submit} 一次性把 SQE 提交给内核；随后 \texttt{io\_{}uring\_{}wait\_{}cqe} 阻塞直到 CQE 就绪，检查 \texttt{cqe->res} 可知是否成功，最后调用 \texttt{io\_{}uring\_{}cqe\_{}seen} 告知内核该 CQE 已消费。\par
