You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用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_sizetime(us)
12302266
82667608
642896108
2563001141

我的环境: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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.28 16:32:44