数据库和块存储引擎常同时遇到两件事:用
O_DIRECT 绕过 Page Cache
避免双重缓冲(见 #10 Direct
I/O),以及 用 io_uring 降低 syscall
开销(见 io_uring
内核内部)。两者可以组合,但对齐、posix_memalign、io_uring_register_buffers
和 IOSQE_FIXED_FILE
各有一层约束。本文把组合路径、实测数据和选型边界写清楚。
测试环境说明 下文 benchmark 在 Linux 6.6.87(WSL2)、liburing 2.14、ext4 文件系统、
/tmp上实测。块设备裸盘与 NVMe 上的绝对 IOPS 会更高;结论侧重路径差异与约束,不跨环境比绝对数值。
一、为什么要把两者放在一起
flowchart LR
APP[应用 Buffer Pool]
UR[io_uring SQ/CQ]
VFS[VFS + 文件系统]
PC[Page Cache]
DISK[块设备]
APP -->|O_DIRECT 路径| UR --> VFS --> DISK
APP -->|缓冲 I/O| UR --> VFS --> PC --> DISK
| 路径 | 数据副本 | 典型用户 |
|---|---|---|
| 缓冲 I/O + Page Cache | 用户缓冲 + 页缓存 | 通用文件、小文件 |
O_DIRECT |
仅用户缓冲(对齐后直写块层) | InnoDB 数据文件、Ceph OSD、自定义引擎 |
O_DIRECT + io_uring |
同上 + 批量提交减少 syscall | 高 IOPS 数据库、NVMe 压测工具 |
O_DIRECT
解决缓存语义;io_uring
解决提交效率。互不替代。
二、O_DIRECT 硬约束(复习)
来自 #10,组合 io_uring 前必须满足:
- 缓冲区地址、长度、文件偏移 均按逻辑块大小对齐(ext4 常见 4 KB)。
- 使用
posix_memalign(4096, size)或mmap对齐映射,不要用普通malloc。 - 文件系统与块层需支持 direct I/O;部分网络文件系统不支持或行为怪异。
O_DIRECT与mmap同一文件混用会踩坑;sendfile等页缓存路径与 direct fd 不兼容。
void *buf;
posix_memalign(&buf, 4096, 4096);
int fd = open(path, O_RDWR | O_CREAT | O_TRUNC | O_DIRECT, 0644);三、io_uring
固定缓冲区:io_uring_register_buffers
3.1 作用
每次 read/write 传统路径需要
pin 用户页。io_uring_register_buffers()
预先注册一组 iovec,内核长期持有映射,后续 SQE
可通过 buf_index 引用,减少 per-I/O
页表开销。
struct io_uring ring;
io_uring_queue_init(256, &ring, 0);
struct iovec iov = { .iov_base = aligned_buf, .iov_len = 4096 };
io_uring_register_buffers(&ring, &iov, 1);
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_write(sqe, fd, NULL, 4096, offset);
sqe->buf_index = 0; /* 使用注册缓冲区槽位 0 */
io_uring_submit(&ring);与 IORING_REGISTER_FILES(固定 fd
槽位)独立:前者优化缓冲区 pin,后者优化
fd 查找。
3.2 与
O_DIRECT 叠加时的注意点
- 注册缓冲区本身必须是 O_DIRECT 合法的对齐内存。
- 部分内核版本对
register_buffers+O_DIRECT+NULL用户地址组合的行为曾有 bug;升级内核后应回归测试。 - 本文环境实测:
O_DIRECT+ 普通io_uring_prep_write(sqe, fd, buf, ...)成功;register_buffers+buf_index路径返回 -EFAULT(14)——说明该组合在当前 WSL2 6.6 栈上未走通,生产启用前必须在目标内核上验证。
3.3 Provided Buffers(进阶)
IOSQE_BUFFER_SELECT +
io_uring_setup_buf_ring 是更新的缓冲池模型(见
io_uring
高级特性),适合网络收包;块设备 Direct I/O
场景更常见仍是 register_buffers
或每请求显式缓冲区。
四、实测:三种 io_uring 写路径(10k × 4KB)
测试程序对同一 ext4 文件顺序写 10000 次 4 KB(约 40 MB),对比:
- 缓冲 I/O + io_uring(无
O_DIRECT) O_DIRECT+ io_uring(对齐缓冲,无 register)O_DIRECT+register_buffers(本文环境失败)
buffered io_uring write: 0.284 s, ~35242 IOPS
O_DIRECT io_uring write: 0.271 s, ~36840 IOPS
O_DIRECT + register_buffers: EFAULT(未成功完成)
解读(限于本环境):
- 40 MB 规模下,缓冲与 Direct 的 IOPS 接近——瓶颈尚未到设备上限,差异主要体现在是否污染 Page Cache。
register_buffers失败说明:不能假设注册缓冲在所有 Direct 组合下开箱即用,需单独测通再上线。
完整复现可参考 liburing 示例 examples/reg-wr
与 man io_uring_register_buffers。
五、与 epoll / 同步 read 的选型
| 场景 | 更常见选择 | 原因 |
|---|---|---|
| 数据库 16KB 页随机读写 | O_DIRECT + 同步或 libaio/io_uring |
缓存语义由 Buffer Pool 掌控 |
| 高并发网络 + 小包 | epoll 或 io_uring(常非 Direct) | 延迟与连接管理优先 |
| 块设备裸盘压测 | O_DIRECT +
io_uring/libaio,direct=1 |
见 #64 基准方法论 |
| 大量空闲连接 | epoll 可能更省(见 io_uring vs epoll) | io_uring 每连接状态成本 |
io_uring 不是 epoll 的完全替代;O_DIRECT
也不是所有存储栈的默认。组合用于:已决定绕过页缓存,且需要批量异步提交。
六、工程检查清单
□ 缓冲区 4K(或设备块大小)对齐
□ open 带 O_DIRECT;ftruncate 预分配避免 ENOSPC 中途失败
□ 文件系统 block size 与 I/O 大小兼容
□ io_uring register_buffers 在目标内核实测通过
□ 监控 iowait 与 Buffer Pool 命中率,确认没有「双缓存」
□ 与 fsync/fsync 策略一致(见 #12 数据完整性)
七、与系列文章的衔接
- 页缓存路径:Page Cache、Linux I/O 栈
- io_uring 机制:内核内部、异步 I/O 综述
- liburing API:io_uring 系列、Golang 集成
参考资料
- Linux
man 2 open(O_DIRECT)、man io_uring_register_buffers - Linux
内核:
fs/direct-io.c、io_uring/opdef.c - J. Axboe, io_uring 设计文档 —— https://kernel.dk/io_uring.pdf
- 本站 #10 Direct I/O、#64 存储基准测试
复现
benchmark:examples/storage/79-o-direct-io-uring/(make && make run)
上一篇: 存储事故复盘 相关: io_uring 内核内部 · Direct I/O 详解 · Linux 异步 I/O
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【存储工程】Direct I/O 与 O_DIRECT:绕过缓存的得与失
在 Linux 的传统 I/O 路径中,应用程序通过 read() 和 write() 系统调用与文件交互时,数据并不会直接在用户空间缓冲区(User Buffer)和磁盘之间传输。内核会在两者之间插入一层页缓存(Page Cache),作为磁盘数据在内存中的缓存副本。一次典型的写入流程如下:
【存储工程】Linux 异步 I/O:从 POSIX AIO 到 io_uring
在高吞吐存储系统中,同步 I/O 是性能的天花板。本文系统梳理 Linux 下三代异步 I/O 方案——POSIX AIO、Linux Native AIO(libaio)和 io_uring——的设计原理、编程接口、性能特征与工程实践,帮助你在实际项目中做出正确的技术选型。
【存储工程】小文件问题:为什么文件数量比文件大小更致命
系统分析小文件在块分配、元数据管理、磁盘寻道和网络协议四个层面的放大效应,用数据量化 slack space、inode 开销和 syscall 成本,给出应用层聚合与对象存储归档两种工程方案。
【存储工程】磁盘空间耗尽:从 70% 到 ENOSPC 的行为退化链
逐层拆解 ext4、XFS、Btrfs、ZFS 从 70% 填充到 100% 耗尽过程中的块分配退化、碎片化加剧和 ENOSPC 故障模式,给出各文件系统的容量红线、监控阈值和应急恢复方法。