使用IoUring批量提交IO请求却降低吞吐量的问题咨询
问题:IoUring批量提交IO后耗时反而增加的原因及单线程IO瓶颈疑问
问题描述
我尝试用IoUring加速大文件读取(3.6GB),按照资料所说批量提交IO请求可提升吞吐量,编写了以下Rust代码:
主逻辑代码:
let mut file = fs::File::open(&path).unwrap(); // 将文件分割为多个段,读取偏移量以正确读取各段 let offsets = read_offsets(&mut file); // 初始化uring let mut ring = IoUring::new(1024).unwrap(); // 从命令行参数读取batch_size let batch_size = args.batch_size; let rounds = (offsets.len() - 1) / batch_size; // 预分配缓冲区避免重复分配 let mut buffers = vec![Vec::new(); batch_size]; let now = std::time::Instant::now(); for i in 0..rounds { let base = i * batch_size; batch_read( &file, &mut ring, &mut buffers, &offsets, base, batch_size, ) }
核心批量读取函数batch_read:
#[allow(clippy::uninit_vec)] fn batch_read( file: &fs::File, ring: &mut IoUring, buffers: &mut [Vec<u8>], offsets: &[u64], base: usize, batch_size: usize ) { for j in 0..batch_size { let mut submission = ring.submission(); let start = offsets[base + j]; let end = offsets[base + j + 1]; let buf = buffers.get_mut(j).unwrap(); let len = (end - start) as usize; buf.clear(); buf.reserve(len); unsafe { buf.set_len(len); } let read_e = opcode::Read::new(types::Fd(file.as_raw_fd()), buf.as_mut_ptr(), len as _) .offset64(start as i64) .build(); unsafe { submission.push(&read_e).unwrap(); } } // 批量提交并等待所有请求完成 ring.submit_and_wait(batch_size).unwrap(); let completions = ring.completion(); for _ in completions {} }
但测试发现增大batch_size后,读取耗时反而上升,测试数据如下:
| batch_size | time(us) |
|---|---|
| 1 | 2302266 |
| 8 | 2667608 |
| 64 | 2896108 |
| 256 | 3001141 |
我的环境:Ubuntu 20.04系统,内核版本5.15.0-67-generic,文件系统为btrfs,由4块NVMe SSD组成RAID0,单块SSD顺序IO可达2.8GB/s,确认未达IO瓶颈。
更新疑问:单线程能否达到IO瓶颈?有博客称单线程仅能发起一个未完成IO,IoUring是否受此限制?若是,批量提交仅能减少enter系统调用?
问题分析与解答
1. 批量增大后耗时增加的原因
你的代码存在几个关键缺陷,导致批量越大性能越差:
- 重复获取提交队列开销:每次循环都执行
let mut submission = ring.submission();,重复加锁、获取提交队列,带来不必要的同步开销,batch_size越大,累积开销越高。 - 同步等待全批次完成:
ring.submit_and_wait(batch_size)会阻塞线程直到当前批次所有IO完成,相当于把批量IO变成了串行处理——虽然提交是批量的,但必须等一批全结束才处理下一批,完全没利用IoUring的异步并发能力。当batch_size增大时,单批的调度延迟和同步等待开销会被放大。 - 缓冲区操作累积开销:每次循环里的
buf.clear()、reserve、set_len操作,在batch_size变大时,内存操作的累积开销也会显著增加。
2. 单线程与IoUring的IO并发疑问
- 传统阻塞IO确实是单线程一次只能处理一个未完成IO,但IoUring完全打破了这个限制:
- IoUring通过内核空间的提交/完成队列,允许用户态一次性提交多个IO请求,内核会异步处理这些请求,无需用户线程阻塞等待单个IO完成。只要硬件(比如你的RAID0多SSD)支持并发IO,单线程使用IoUring就能同时发起多个未完成请求,充分利用IO带宽,完全可以达到甚至超过硬件的IO瓶颈。
- 批量提交的作用不止减少系统调用:
- 减少
enter系统调用次数是一部分,但更核心的是让内核可以对批量IO做调度优化(比如合并相邻IO、按磁盘物理顺序重排请求),提升IO处理效率。但你的代码因为同步等待全批次完成,根本没发挥这一优势。
- 减少
优化建议
- 单次获取提交队列:在
batch_read中先获取一次提交队列,循环填充所有请求后再提交,避免重复锁开销。 - 取消同步全批次等待:改用
ring.submit()提交请求,之后定期轮询完成队列,或者在提交多批请求后再统一等待,让多批IO并发处理。 - 优化缓冲区管理:预先分配好所有需要的缓冲区,避免每次循环的内存操作;或者使用IoUring的固定注册缓冲区(registered buffers),减少内存拷贝开销。
内容的提问来源于stack exchange,提问作者YjyJeff
相关产品推荐
相关产品推荐

