Linux 磁盘 I/O 性能优化:调整请求队列长度 nr_requests
高负载服务器的磁盘 IO 打满时,除了换硬件,还有一个内核参数值得试:I/O 请求队列长度 nr_requests。它决定每个块设备能同时排队多少个请求,调大之后多任务场景下磁盘调度余地更大,延迟往往能降下来。这篇记录在 Rocky 9 上查看和调整它的方法,查看、调整、验证动作均在 Rocky 9.3 容器实测,实测同时暴露了一个”写不进去”的坑,一并给出。
实验环境
- Rocky Linux 9.3 一次性容器(
docker run --privileged,读写 /sys 需要); - 实测对象:容器可见的 vda(virtio 虚拟盘,默认 256)与 loop 盘(默认 128);
- iostat 来自 sysstat 12.5.4;
- 物理机上 SATA/SAS 盘同样走 /sys 这套接口,操作方式一致。
请求队列长度影响什么
Linux 里每个磁盘设备都有自己的 I/O 请求队列,队列长度就是设备可以缓存的请求数量。队列更长,内核就能把更多请求攒在一起做调度和合并,在高负载、多任务的环境下效果明显;代价是管理这些请求需要消耗更多内核内存——每个排队请求都占内存,队列翻倍开销跟着翻倍。
一句话机制:nr_requests 是调度器的”原料池”,池子大,合并与重排的空间才大。
顺带认识同目录下的邻居 read_ahead_kb:预读大小,决定内核在一次读请求后面”顺手”多读多少。实测 virtio 盘默认 8192(8MB),比很多老资料写的 128 大得多——发行版已经替你调过一轮,动它之前先看实际值。顺序读为主的负载(大文件拷贝、日志扫描)调大它往往比调 nr_requests 收益更直接,两个参数经常一起评估。
另一个容易混淆的概念是硬件队列深度(NCQ 层面的 queue depth):nr_requests 管的是内核软件队列攒多少请求,硬件队列深度管的是盘一次能接收多少条,两者在链路上下游。调大 nr_requests 只是在内核侧多攒货,盘端瓶颈(机械盘寻道、虚盘的宿主机限速)不会因此消失。
调多大合适:没有万能值。常见做法是从基线翻倍起步(128→256→512),每档用实例三的 iostat 采一组对比,await 下降、aqu-sz 上升才说明方向对了;内存紧张的机器每次翻倍后盯一眼可用内存。NVMe 盘自带深硬件队列,通常不需要动它。
实例一:查看当前值
直接读 /sys 文件系统,虚拟盘和 loop 盘各看一眼(默认值各不同):

验证点:命令输出一个数字(SATA/SAS 真实盘常见 128,NVMe 常见 1023/1024),先记下这个基线再动手。
实例二:调整
写同一个文件即可调整,调大观察:
1 | # 设置请求队列长度为 256 |
实测结果分两派:不是所有设备都接受调整。vda 把 256 调到 512 成功、读回即见新值;loop 盘做同样的写入,内核直接拒绝:

Invalid argument 且原值不变——内核拒绝时不会写坏任何东西,但如果命令写在脚本里没检查错误,就会以为调成功了。真实 SATA/SAS 盘上从 128 调到 256 通常可行;虚拟化环境(virtio、loop)的队列深度受虚拟化层约束,可能不接受。
容器与云环境还有一层:非特权容器里 /sys 是只读挂载,写入直接报 Read-only file system(实测),这不是参数问题而是权限边界——到宿主机上或用特权方式操作。
验证点:echo 后必须再 cat 一次读回确认,看到目标数字才算改成功。
注意这条写的是运行时状态,重启即失效,确认有效后需要持久化。两种常见做法:udev 规则(设备出现即应用)或开机脚本:
1 | # /etc/udev/rules.d/71-nr_requests.rules(配置示例) |
实例三:用 iostat 观察效果
调整的价值要用数据确认。一边对盘制造持续写入,一边用 iostat 采样:
1 | iostat -dx /dev/sda 1 5 # 每 1 秒采一次,共 5 次 |
实测输出(sysstat 12.5.4,直写期间采样):

对照方法:调队列前后各采一组,重点看三列——w_await/await(单次写/全部 IO 平均等待,主指标)、aqu-sz(队列平均深度,验证调大的队列是否真被用上)、%util(盘忙碌占比,长期逼近 100% 说明盘本身是瓶颈,调参数救不了)。三列的判读口径:
| 指标 | 含义 | 调参后的健康方向 |
|---|---|---|
await / w_await / r_await |
单次 IO 平均等待(毫秒,全/写/读) | 应下降 |
aqu-sz |
平均队列深度 | 适度上升(说明队列被用上),但不该贴着 nr_requests 顶格 |
%util |
盘忙碌时间占比 | 高位运行说明盘接近饱和,此时应考虑分流或换硬件 |
同时开着 top 盯内存,确认调整没有拖累其他部分。
使用场景
- 高负载服务器:数据库服务器、文件服务器这类 IO 密集型服务,调大队列长度通常有收益。
- 多任务环境:多个 IO 密集型应用同时跑时,更长的队列能改善响应时间和处理效率。
反过来说,低负载机器调它没有意义——队列根本排不满,合并无从谈起。
错误写法对比
错误一:echo 完不检查。echo 256 > ... 被内核拒绝时 shell 会报 write error,脚本里不看退出码、不再读回确认,调整就成了”以为生效了”。
1 | # 错:写完就走 |
错误二:把 nr_requests 当独立开关调。它和 I/O 调度器、单请求大小共同决定行为,队列调大但调度器不匹配盘的类型,收益可能为零甚至倒退。
错误三:拿它救物理瓶颈。%util 长期 100% 的盘,排队再长也只是把等待从应用侧挪到内核侧,平均延迟不会变好——先分流冷数据、加缓存或换盘,参数是最后一档。
注意事项
- 调整任何系统设置前先备份原值(就是实例一的 cat 输出),操作尽量安排在低峰期。
- 先在测试环境验证效果,再考虑上生产,调大不一定都是正收益。
- 队列长度和内存占用成正比,内存紧张的机器上调大可能导致整体性能反而下降。
- 这个参数和 I/O 调度器是配合关系,调队列长度时留意当前调度器是否匹配盘的类型。
小结
回到开场的 IO 打满:nr_requests 给的是”调度余地”这张牌——先 cat 基线,echo 后读回验证,再用 iostat 的 await/aqu-sz 确认收益。实测提醒两件事:不是所有设备都接受调大(Invalid argument),以及调大生效不等于调大有用。这张牌打完还没起色,再考虑换硬件也不迟。改完参数别忘了让它活过重启:udev 规则或开机脚本持久化,下一次重启巡检时再 cat 一遍确认。
本文由作者 2020-2024 年间的 CSDN 博客文章重构而来,原发布于 CSDN。