“直接替换”陷阱:naive io_uring 几乎不提速
论文开篇的核心警告:把 libaio / epoll 原样换成 io_uring,Buffer Manager 只快 1.06×、Network Shuffle 只快 1.10×;只有围绕 io_uring 的能力(批量、异步、Registered Buffers 等)重新设计系统,才能拿到 2.05× / 2.31× 的端到端提升。
传统接口 vs naive io_uring vs 优化后的 io_uring
SQ / CQ 双环形缓冲区:内存映射的零拷贝通道
io_uring 采用完成模型(completion-based,区别于 epoll 的就绪模型):应用把请求写进提交队列 SQ,结果稍后出现在完成队列 CQ。两个环形缓冲区通过 mmap 由用户态与内核共享,提交与完成都不需要额外数据拷贝;完成可以乱序到达,靠用户自定义 ID(user_data)配对。
环形缓冲区状态机(交互动画)
批量 = 摊销系统调用
epoll 能一次报告多个就绪事件,但每个 read() 仍需单独系统调用;io_uring 允许先入队多个 SQE,再用一次 io_uring_enter() 提交,完成也可批量收割。论文实测:批量 16 个操作时,每操作 CPU 周期数下降约 5–6×。
但批量不是越大越好
对 SSD 写延迟的微基准(8 工作线程、1.5 MIOPS 固定吞吐):批量 1 时平均延迟 11.5 µs;批量 128 时冲到 200.9 µs,批量 256 达 317.5 µs。延迟敏感系统必须谨慎调节批量。
内核里的三条执行路径:Inline / Poll Set / io_worker
请求进入内核后有三条命运:(2a) Inline Completion 能立刻完成就立即完成(如 socket 已有数据可读);(2b) Poll Set 对可轮询操作挂内部事件处理器 io_async_wake() 非阻塞等待;(2c) io_worker 阻塞回退 对 fsync、大块读等无法异步的操作委派给工作者线程——论文实测该回退平均引入 ~7.3 µs 额外开销。
三条执行路径流程图(点选路径,播放令牌动画)
SQPoll:连提交系统调用也省掉
默认模式下应用线程调用 io_uring_enter() 切入内核;SQPoll 模式用一个专用内核线程持续轮询 SQ,提交路径完全没有用户—内核切换。代价:独占一个核;且空闲超时后线程睡眠,唤醒延迟约 30 µs。
警惕 io_worker 回退
频繁回退或大量活跃 io_worker 通常意味着 I/O 模式不佳。论文 §3.6 列出三个触发回退的阈值:块大小超过 max_hw_sectors_kb(开启 IOMMU 时可为 128 KiB)、批量请求数超过 nr_requests(裸机 1023 / 云虚拟机 127)、块大小超过 512 KiB(max_segments)。
Buffer Manager 七步渐进优化:16.5k → 546.5k tx/s
论文在单线程缓冲管理存储引擎(YCSB,100% 均匀更新,约 70% 缺页率,8× Kioxia CM7-R PCIe 5.0 SSD)上逐步叠加优化:批量写回 → 异步 Fiber → 批量读取 → Registered Buffers → NVMe Passthrough → IOPoll → SQPoll,吞吐量从 16.5k 一路爬升到 546.5k tx/s(约 33×)。瓶颈也从 I/O 延迟 转移到 CPU 周期。
优化阶梯(分步揭示每一级)
每一步在做什么?
| 步骤 | 机制 | 吞吐 (K tx/s) | 本步收益 |
|---|---|---|---|
| Posix / io_uring 同步基线 | 每次 I/O 都阻塞等待,受设备延迟支配 | 16.5 | — |
| ① +BatchEvict 批量写回 | 淘汰时积攒多个脏页,一次 io_uring_enter 批量写回,摊销写延迟 | 19.1 | +14% |
| ② +Fibers 异步事务 | Boost.fiber 协作式调度,缺页时让出 CPU 跑别的事务,隐藏 I/O 延迟 | 183.5 | ≈9.6× |
| ③ +BatchSubmit 批量读取 | 自适应批量:聚合多个 Fiber 的读请求再一次提交,降低每 I/O 周期数 | 216.6 | +18% |
| ④ +RegBufs 注册缓冲 | 缓冲池一次性注册钉住,内核 DMA 直写用户内存,免逐请求页钉住与拷贝 | 237.8 | +11% |
| ⑤ +Passthru NVMe 直通 | OP_URING_CMD 直接下发原生 NVMe 命令,绕过通用存储栈 | 300.5 | +20% |
| ⑥ +IOPoll 完成轮询 | 直接从 NVMe 队列轮询完成事件,取代中断(仅支持 O_DIRECT 块设备) | 376.4 | +21% |
| ⑦ +SQPoll 提交轮询 | 专用内核线程轮询 SQ,提交免系统调用(多花一个核) | 546.5 | +32% |
Network Shuffle:ring-per-thread 与 zero-copy 饱和 400 Gbit/s
第二个案例是 6 节点集群(ConnectX-7,每节点 400 Gbit/s 双向,对分带宽 4.8 Tbit/s)上的分布式哈希连接数据洗牌。论文拒绝“专用 I/O 线程 + 阻塞 I/O”的老架构,采用 ring-per-thread:每个工作线程持有线程本地 io_uring,计算与收发在同一线程内重叠;再用 zero-copy send / receive 消除内核—用户拷贝,最终用 16 个工作线程就饱和 400 Gbit/s 链路。
架构与带宽(分步动画)
任务调度三种模式:Default / CoopTR / DeferTR 的 IPI 抢占差异
异步操作完成时,内核要运行 task_work 把完成事件放进 CQ。默认模式下任何用户—内核切换都会处理它,线程忙时内核甚至发 IPI(处理器间中断)强行抢占——破坏缓存局部性、增加抖动。CoopTR(COOP_TASKRUN)减少 IPI,但包括 malloc() 在内的任何系统调用仍会触发处理。DeferTR(DEFER_TASKRUN)只在 io_uring_enter() 时运行 task_work,论文推荐并全程使用。
三种模式的抢占行为(时间线动画)
相对 IPI / 抢占频率对比(定性)
PostgreSQL 案例:四条原则换来 11–15% 额外加速
论文把两条案例研究提炼成四条实践原则,应用到 PostgreSQL 18 新加入的 io_uring 后端上做验证。尽管受多进程架构与文件系统依赖的约束,仍在其 io_uring 基线之上获得 11–15% 的扫描吞吐提升(摘要记为 14%)。
四条原则(GL1–GL4)与在 PostgreSQL 中的落地
| 原则 | 内容 | PostgreSQL 中的应用 |
|---|---|---|
| GL1 先确认 I/O 是瓶颈 | I/O 占比小(CPU 密集/常驻内存)时 io_uring 收益有限;用延迟/周期模型预估 | PG18 的 io_uring 后端在 I/O 密集负载上已比同步设计快至 3× → 瓶颈确认 |
| GL2 架构对齐 io_uring 能力 | 异步重叠计算与 I/O、批量摊销、ring-per-thread | PG 后端进程已批量异步读写;但多进程共享 ring 阻碍 DeferTR,文件系统阻碍 Passthrough |
| GL3 审慎选择执行模式 | 首选 DeferTR+single issuer;SQPoll 视成本;避免 io_worker 回退 | 改用 CoopTR(次优),新增 SQPoll(多后端共享一个内核线程);fsync 走传统调用不进 ring |
| GL4 启用适配的优化项 | Registered Buffers、IOPoll、zero-copy 等按负载选择 | 整个缓冲池注册为 fixed buffers(+4–6%);ext4 上开 IOPoll(至 +7.5%);叠加 SQPoll 合计 +11–15% |
PostgreSQL 优化前后对比
| 配置 | 说明 | 相对上游 PG18 io_uring 基线 |
|---|---|---|
| 上游 PG18 io_uring 后端(基线) | 异步 I/O + OS readahead;I/O 密集负载下已比旧同步设计快至 3× | 1.00× |
| + Fixed Buffers | 缓冲池整体注册,替代“4 个在途 I/O 后才 IO_ASYNC”的启发式 | +4–6% |
| + IOPoll(ext4) | 轮询完成事件取代中断 | 至 +7.5% |
| + SQPoll(共享内核线程) | 多后端共享一个 SQPoll 线程,性能损失可忽略 | 合计 +11–15%(摘要:14%) |
(参照)论文缓冲管理器的 TPC-C 结果
| 配置 | 内存内 | 内存外 |
|---|---|---|
| vmcache(阻塞读基线) | 35.5 | 2.0 |
| libaio + Fibers | 59.2 | 19.2 |
| io_uring + Fibers(naive) | 60.2 | 21.2 |
| +RegBufs | 60.7 | 23.1 |
| +Passthru | 62.5 | 25.0 |
| +IOPoll | 61.5 (↓) | 28.3 |
| +SQPoll | 62.6 (≈) | 32.9 |
总览:批量衰减曲线与端到端加速热力表
批量大小 vs 每 I/O 的 CPU 周期数(衰减曲线)
端到端 speedup 热力表(优化级别 × 工作负载)
单元格 = 相对该工作负载 naive io_uring 配置 的端到端加速比(YCSB 基线 183.5k tx/s;TPC-C 内存内 60.2k;TPC-C 内存外 21.2k;Shuffle 13.2 GiB/s)。“—” = 论文未报告该组合。
| 优化级别\工作负载 | YCSB 更新密集 (存储) | TPC-C 内存内 (计算密集) | TPC-C 内存外 (I/O 密集) | Shuffle 大元组 (网络) |
|---|---|---|---|---|
| naive io_uring(基线) | ||||
| +批量 / 异步架构改造 | ||||
| +Registered Buffers | ||||
| +NVMe Passthrough(存储) | ||||
| +IOPoll / 轮询 | ||||
| +SQPoll / +zero-copy 收发 |
论文原始数据表
表 1:性能建模所用的 I/O 数值
| 项目 | 数值 |
|---|---|
| 单次读延迟 | 70 µs |
| 单次写延迟 | 12 µs |
| 事务执行(B 树遍历+更新) | 8 264 clk |
| 单次读 I/O 处理 | 10 200 clk |
| 批量读 I/O 处理 | 5 400 clk |
| 批量写 I/O 处理 | 5 700 clk |
表 2:批量大小对 SSD 写延迟的影响
| 批量大小 | 平均延迟 (µs) | 标准差 σ (µs) |
|---|---|---|
| 1 | 11.51 | ±0.95 |
| 8 | 24.22 | ±1.71 |
| 32 | 60.62 | ±3.91 |
| 64 | 116.40 | ±12.17 |
| 128 | 200.85 | ±7.47 |
| 256 | 317.51 | ±33.88 |
实验平台(便于复现)
存储:3.7 GHz AMD 服务器(内核 6.15),8× Kioxia CM7-R PCIe 5.0 NVMe SSD(单盘 2.45M IOPS);单线程缓冲管理器 + 1 GB 缓冲池;YCSB 1000 万元组(8 B 键 + 128 B 值,约 70% 缺页率),TPC-C 1 / 100 仓库。网络:6 节点 ConnectX-7(400 Gbit/s 双向,对分 4.8 Tbit/s),内核 6.17(支持 zero-copy receive),1 MiB 传输块,morsel 驱动并行。