第77章:磁盘 IO 优化
9 分钟阅读
第七十七章:磁盘 IO 优化
77.1 先理解:磁盘为什么总是瓶颈
CPU 的延迟是纳秒级,内存是几十纳秒,SSD 是几十微秒,机械硬盘的随机寻道是毫秒级——差了好几个数量级。所以磁盘往往是整套系统里最慢的一环,也常常是优化收益最大的地方。
理解磁盘 IO,先分清两组概念:
- 吞吐(throughput):每秒能传多少 MB。顺序读写(大文件复制、备份)主要看它
- IOPS 与延迟(latency):每秒能完成多少次 IO 操作、单次平均要等多久。随机小 IO(数据库、日志、大量小文件)主要看它们
这两类负载的优化手段完全不同,先搞清楚自己的业务属于哪一类,再谈调优。
还有一层很关键:Linux 会用空闲内存做页缓存(page cache)。读操作大多直接命中内存,根本不到磁盘;写操作先写进内存,随后由内核异步刷盘。所以看到"磁盘看起来很闲但写入很慢"时,瓶颈往往在刷盘节奏上,而不是磁盘本身——77.7 会专门讲这块。
77.2 用 iostat 看磁盘到底有多忙
| |
| |
重点看这几列:
| 字段 | 含义 | 怎么判断有问题 |
|---|---|---|
r/s、w/s | 每秒完成的读写次数(即 IOPS) | 是否已接近设备的标称能力 |
rkB/s、wkB/s | 每秒读写吞吐 | 是否已接近磁盘带宽上限 |
aqu-sz | 平均请求队列长度 | 要和设备的并行能力比较,不是简单地 >1 就算积压 |
await | 平均 IO 等待时间(毫秒,含排队) | 机械盘 > 20ms、SSD > 5ms 就值得关注 |
%util | 设备"至少有一个请求在处理"的时间占比 | SSD / NVMe 上 100% 不等于饱和(见下) |
rareq-sz / wareq-sz | 平均每个请求的大小(KB) | 远小于 4KB 说明是小 IO、随机性高 |
"
avgqu-sz > 1就说明队列积压"这个说法并不准确。 队列长度要跟设备的并行度比较:机械盘并行度基本是 1,队列 1 就意味着后面在排队;而一块 NVMe 的并行度可能上百,队列 10 反而很轻松。同理,
%util表示"设备忙的时间比例",对支持并行处理的 NVMe 来说,%util到 100% 仍可能远未触及上限。判断是否饱和要看await是否明显上升、吞吐是否已经打满,而不是只盯%util。
"
await > 100ms就是 IO 慢"也要分设备看。 机械盘await十几毫秒是正常的(寻道时间摆在那里);SSD 应该在 1ms 以内;NVMe 更是 0.1ms 量级。拿 SSD 的标准去衡量机械盘,或者反过来,都会得出错误结论。
77.3 找出是谁在读写:iotop 与 pidstat
| |
iotop 的快捷键:左右箭头切换排序字段、r 反向排序、o 切换只显示活跃进程、q 退出。
iotop显示的是进程主动发起的块设备 IO。有一类 IO 是内核替进程做的(页缓存回写、文件系统日志提交),会记到[kworker]这类内核线程头上,看不到"真凶"。这时候配合vmstat的bi/bo一起判断。补充一个很好用的命令:
pidstat -d 1(同在 sysstat 包里),按进程显示读写 KB 和 IO 延迟(iodelay)。
77.4 判断 IO 瓶颈的完整套路
| |
| |
关键组合是:b 列(处于不可中断睡眠的进程数)不为 0,同时 wa 高,就说明有进程在等 IO。bi / bo 是每秒从块设备读入 / 写出的块数。
| |
77.5 文件系统选择与挂载选项
77.5.1 常见文件系统怎么选
| 文件系统 | 特点 | 适用场景 |
|---|---|---|
| ext4 | 通用、成熟、工具链完善,支持缩容 | 系统盘、一般业务盘 |
| XFS | 大文件与高并发表现好,不支持缩容 | 数据库、日志盘、大容量存储 |
| Btrfs | 快照、校验和、压缩,功能多但更复杂 | 需要快照 / 回滚的场景 |
| ZFS | 校验、快照、压缩、缓存(ARC)齐全 | 存储服务器(需额外安装) |
必须记住一点:XFS 只能扩容、不能缩容。 规划分区时别切得太满,否则后期调整会非常被动。
77.5.2 挂载选项
| |
| |
永久生效写进 /etc/fstab:
| |
noatime是收益最直接的一项。 默认的relatime已经比老的atime好很多,但在读多写多的场景下,noatime仍能省掉一批元数据写入。
nobarrier这个挂载选项并不存在——原稿里的写法会直接被 mount 拒绝。要关闭写入屏障,ext4 上的参数是barrier=0,但代价是断电时可能丢失已提交的数据,只有在明确知道自己在做什么时才用。同理,
data=writeback虽然比默认的data=ordered快,但崩溃时已提交的文件数据可能丢失或读到旧内容。生产环境不建议为了这点性能去冒数据风险。
77.5.3 ext4 调优
| |
关于日志(journal)大小:
| |
调整日志大小必须在文件系统卸载(或只读挂载)状态下进行,在线执行会被拒绝。而且日志过大反而拖慢
fsck、占用空间,除非确实有写入吞吐压力,用默认值就好。
77.5.4 XFS 相关
| |
lazy-count=1这类参数在新版mkfs.xfs里已经是默认值,不必手写。XFS 的强项是大文件和高并发元数据操作,创建时按预期负载把agcount、日志大小规划好,比事后调参更有意义。
77.6 IO 调度器与 IO 优先级
77.6.1 调度器选哪个
| |
| |
| 调度器 | 特点 | 建议 |
|---|---|---|
none | 基本不重排序,直接下发 | NVMe、高性能 SSD(设备自身并行度已足够高,内核不必插手) |
mq-deadline | 尽量保证请求在期限内完成 | 通用默认值,SATA SSD 与机械盘都合适 |
kyber | 轻量、低开销 | 高速设备与特定场景 |
bfq | 按进程公平分配带宽 | 桌面、对交互体验有要求的场景(开销最大) |
| |
原稿里写的
echo mq-deadline > /sys/block/sda/queue/scheduler在普通用户下必然失败——>是当前 shell 打开的,sudo管不到它。
永久生效用 udev 规则:
| |
77.6.2 ionice:给进程分配 IO 优先级
nice 只管 CPU,管 IO 的是 ionice:
| |
要提醒的是:只有 CFQ / BFQ 这类调度器才真正实现 IO 优先级。 在
none/mq-deadline下,ionice的 best-effort 基本不生效(内核会直接忽略)。指望靠它在 NVMe 上限制备份任务是不现实的。在 NVMe 上要限速,应该用 cgroup v2 的
io.max,或者干脆给工具本身限速(如rsync --bwlimit=50m、tar配合pv -L 50m)。
77.7 页缓存与脏页刷盘:最容易被忽略的一块
Linux 把空闲内存拿来做页缓存,写操作先落到内存,随后由内核的回写线程慢慢刷盘。参数不合理时会出现两种典型症状:
- 周期性 IO 抖动:
dirty_ratio太大,脏页累积到很高才集中狂写,业务能感觉到规律性的卡顿 - 写延迟毛刺:某个进程写了一大批数据但没到阈值,某一刻被强制同步刷盘阻塞住
| |
| |
调优经验:
- 数据库、虚拟化宿主机这类对写延迟敏感的机器,通常把这两个比例调小(例如 5 / 10),让刷盘更平滑
- 大内存机器上更应该改用绝对字节数(
vm.dirty_background_bytes/vm.dirty_bytes):128GB 内存按 20% 算就是 25GB 脏页,一旦触发同步刷盘,卡顿会非常明显 - 绝对不要设成 0——那会让每次写都同步落盘,性能断崖式下跌
| |
另一块相关的参数是
vm.swappiness,它属于内存优化的范畴,在下一章会讲到——但它在数据库服务器上同样重要,因为不必要的换页会带来大量随机 IO。
77.8 其他值得了解的优化点
- SSD 的 TRIM:定期
sudo fstrim -av,或者启用fstrim.timer,让 SSD 知道哪些块已无用,维持长期写入性能 fio做基准测试:优化前后用fio跑同一套随机读 / 随机写 / 顺序写负载,用数据对比而不是凭感觉blktrace/bpftrace:想看"一个 IO 请求在内核里各阶段各花了多久",用它们能精确到排队、合并、下发- 队列与预读:
/sys/block/*/queue/nr_requests、read_ahead_kb(顺序读场景适当加大预读能提升吞吐) - 对齐:分区与 RAID 条带对齐(现代分区工具默认已对齐)
- 换硬件往往最有效:机械盘换 SSD、SATA 换 NVMe,收益通常比任何软件调参都大。先排除硬件瓶颈,再谈调参
本章小结
| 工具 / 参数 | 用途 |
|---|---|
iostat -x 1 | 看每块盘的 IOPS、吞吐、await、%util |
iotop -o | 找出哪个进程在读写磁盘 |
pidstat -d 1 | 按进程查看 IO 量与延迟 |
vmstat 1 | 用 b 列和 wa 判断是否在等 IO |
mount -o noatime | 最直接、收益最稳的挂载优化 |
调度器 / ionice | 控制 IO 的处理顺序与优先级 |
vm.dirty_* | 控制脏页回写节奏,消除周期性卡顿 |
排查思路:
graph LR
A["业务变慢"] --> B{"vmstat: wa 高、b 不为 0?"}
B -->|是| C["iostat -x 1 看是哪块盘"]
C --> D{"await 高还是吞吐已满?"}
D -->|"await 高"| E["随机 IO 太多<br/>考虑 SSD / 加缓存 / 改访问模式"]
D -->|"吞吐已满"| F["顺序带宽打满<br/>考虑换盘或并行"]
B -->|否| G["不是磁盘瓶颈<br/>回头查 CPU / 内存 / 网络"]
E --> H["用 iotop 定位到具体进程"]
F --> H一句话提醒:磁盘优化的第一原则是"减少 IO",第二原则是"让 IO 尽量顺序化",第三才是调参数。加缓存、合并小写入、批量提交,效果通常远好于改调度器。
下一章我们看网络优化。