引言
一次 read() 从应用出发,要穿过 VFS、页缓存、文件系统、块层调度器、设备驱动,最终到达磁盘——这条IO 栈上每一层都在决定延迟与吞吐。本文自上而下拆解每层职责,对比缓冲 IO 与直接 IO、认识多队列时代的 mq-deadline/none 调度器、介绍异步 IO 新贵 io_uring,再落到 RAID/LVM 布局与 iostat/fio 这两把「测量之尺」,最后给出一份存储性能调优清单。
前置:/linux-filesystem-disk/(文件系统与磁盘基础)。性能监控方法论见 /linux-performance-tuning/。
目录
- 1. IO 栈全景:从 VFS 到设备
- 2. VFS 与页缓存
- 3. 缓冲 IO 与直接 IO
- 4. IO 调度器:mq-deadline 与 none
- 5. io_uring:异步 IO 新时代
- 6. 文件系统与 RAID/LVM 层
- 7. iostat 与 iowait 分析
- 8. fio 压测实战
- 9. 性能调优清单
- 10. 速查表
- 延伸阅读
1. IO 栈全景:从 VFS 到设备
IO 栈是分层的管道,每层只做一件事,层与层之间通过通用接口解耦:
应用层 open/read/write/fsync
↓ VFS(虚拟文件系统:抽象所有文件系统)
页缓存层 缓存热数据、异步写回
↓ 具体文件系统(ext4/xfs/btrfs…)→ 块映射
块层 blk-mq 队列 + 调度器(mq-deadline/none)
↓ 设备驱动 → DMA
设备 SSD/NVMe/HDD
# 看设备拓扑
lsblk
# 看挂载点用的文件系统
df -hT
# 看每个块设备当前使用的 IO 调度器
cat /sys/block/sda/queue/scheduler
| 层 | 职责 | 关键对象 |
|---|---|---|
| VFS | 统一 open/read/write 接口 | 文件、inode、dentry |
| 页缓存 | 读命中、写合并、异步落盘 | page cache、dirty pages |
| 文件系统 | 逻辑块 ↔ 物理块映射、元数据 | ext4/xfs、inode、日志 |
| 块层 | 队列、合并、排序、调度 | blk-mq、request queue |
| 设备层 | 与硬件交换数据 | 驱动、DMA、中断 |
心智:「慢」可能来自任何一层——先定位再优化。文件系统元数据慢、调度器合并差、设备本身慢,症状都是「IO 慢」,但解法完全不同。
2. VFS 与页缓存
页缓存(page cache)是 IO 栈的「内存加速器」:读过的页留在 RAM,写先落缓存再异步刷盘。
# 当前脏页与缓存量
grep -E 'Dirty|Writeback|PageTables|Cached' /proc/meminfo
# 手动刷盘
sync
# 文件 IO 是否命中缓存(cache hit 高说明工作集已热)
# 可用 cachestat(bcc 工具)或 strace 观测
写路径是典型的写回(write-back):write() 只把数据拷进页缓存就返回,内核按水位线异步写回磁盘。这也是「明明写了,突然断电丢数据」的原因——fsync/fdatasync 才保证落盘。
# 强制落盘
fsync 是系统调用,命令行里常用 sync
# 关闭某目录的「写缓存延迟」不现实,事务型应用自行 fsync
# 例如 SQLite/PostgreSQL 都有 fsync 相关参数
记忆:页缓存让「重复读」和「集中写」都变快,但它不是内存泄漏。
free里的buff/cache高是好事;真正要盯的是Dirty——如果 Dirty 持续很大且磁盘写不出去,才是瓶颈。
3. 缓冲 IO 与直接 IO
缓冲 IO 走页缓存(默认),直接 IO(O_DIRECT)绕过页缓存直达磁盘——各有适用场景:
| 对比 | 缓冲 IO | 直接 IO (O_DIRECT) |
|---|---|---|
| 是否过页缓存 | 是(可复用热数据) | 否(每次直达磁盘) |
| 写返回时机 | 写进缓存即返回 | 提交给设备才返回 |
| 适合场景 | 通用、缓存命中率高 | 数据库日志/大文件流式读写 |
| 对齐要求 | 无 | 需按扇区/逻辑块对齐 |
# dd 直写(跳过页缓存,测真实磁盘)
dd if=/dev/zero of=/tmp/f bs=1M count=1024 oflag=direct
# 对比带缓存的写
dd if=/dev/zero of=/tmp/f bs=1M count=1024
# 程序里用 open() 传 O_DIRECT 即可
心法:数据库常用「直接 IO + 自己管理缓存」二选一:要么信 OS 页缓存,要么 O_DIRECT 全自己来,不要双倍缓存。日志型小写(WAL)尤其适合 O_DIRECT——少一层拷贝,fsync 语义更直接。
# 应用主动落盘语义对比
fsync(fd); # 数据+元数据都落盘
fdatasync(fd); # 只落数据(更快,元数据另算)
4. IO 调度器:mq-deadline 与 none
内核 IO 调度器决定「请求以什么顺序发给设备」——NVMe 时代很多调度已从「排序」退化为「尽量直通」。
# 查看/切换调度器
cat /sys/block/nvme0n1/queue/scheduler
echo none > /sys/block/nvme0n1/queue/scheduler
echo mq-deadline > /sys/block/sda/queue/scheduler
# 持久化:加内核参数 elevator=deadline 或 udev 规则
| 调度器 | 机制 | 适用 |
|---|---|---|
| none | 直接进设备队列,几乎不排序 | NVMe/SSD(随机 IO 本身快,排序收益低) |
| mq-deadline | 按截止时间保证最老请求不被饿死 | HDD/SATA SSD(需要合并与公平) |
| kyber | 按延迟目标动态限流 | 云盘/SSD 上追求延迟稳定 |
| bfq(旧) | 按权重公平分配带宽 | 桌面多任务/共享场景 |
# 调整队列深度(决定并发在途请求数)
cat /sys/block/nvme0n1/queue/nr_requests
echo 1024 > /sys/block/nvme0n1/queue/nr_requests
记忆:「SSD 用 none,HDD 用 mq-deadline」是最常用的经验起点。调度器排序本身有 CPU 开销,对百万级 IOPS 的 NVMe 来说收益不抵开销,直通反而更好。
5. io_uring:异步 IO 新时代
io_uring 用「内核与应用共享的环形队列」实现高性能异步 IO——应用提交请求到 SQ、内核完成写入 CQ,全程避免一次系统调用的拷贝与上下文切换开销。
应用提交 SQ 条目 → 内核异步执行 → 完成写入 CQ → 应用收割结果
(共享内存 ring,而非逐次 syscall)
# 安装 liburing 开发库(Debian 系)
sudo apt install liburing-dev
# 最小例子:io_uring 提交一个 read
gcc -O2 -o uread uread.c -luring
| 对比 | 传统同步 read | libaio | io_uring |
|---|---|---|---|
| 系统调用次数 | 每次 IO 一次 | 提交/收割各一次 | 极少量(可批量) |
| 是否支持缓冲 IO | 是 | 基本仅 O_DIRECT | 两者都支持 |
| 复杂度 | 低 | 中 | 中高 |
| 典型场景 | 普通应用 | 数据库 | 数据库、存储引擎、网络服务 |
# 生态里已用上 io_uring 的例子
# RocksDB、PostgreSQL、nginx 都提供 io_uring 后端开关
心法:io_uring 是「一次设计换吞吐」——IOPS 越高收益越大,小请求大批量场景最划算。业务侧先确认真瓶颈(iostat 高 util + 应用线程卡在 read),再引入 io_uring 才值得。
6. 文件系统与 RAID/LVM 层
文件系统管「怎么组织数据」,RAID/LVM 管「底层盘怎么组合」——两层叠加决定吞吐与冗余:
# 软 RAID1(两块盘镜像)
mdadm --create /dev/md0 --level=1 --raid-devices=2 /dev/sdb /dev/sdc
# LVM:PV → VG → LV
pvcreate /dev/sdb && vgcreate vg0 /dev/sdb
lvcreate -L 100G -n data vg0
mkfs.xfs /dev/vg0/data
| 级别 | 冗余 | 读写 | 适用 |
|---|---|---|---|
| RAID0 | 无 | 读写均摊(条带) | 性能优先、可承受丢盘 |
| RAID1 | 镜像 | 读可并行 | 系统盘/数据库日志 |
| RAID5 | 单盘校验 | 读好、写有校验开销 | 大容量通用存储 |
| RAID10 | 镜像+条带 | 读写好 | 数据库高性能首选 |
文件系统挂载选项也直接改变行为:
# 禁用 atime 更新,减少元数据写
mount -o noatime /dev/vg0/data /data
# xfs 针对大文件 IO 的常用挂载项
mount -o noatime,largeio /data
# SSD 开启 TRIM 保持长期性能
systemctl enable --now fstrim.timer
记忆:数据库盘别省 RAID10;顺序大文件用 RAID0/5 即可。条带大小要跟 IO 大小匹配(数据库 8-64KB 随机块对齐条带),否则一次逻辑读变多次物理读。
7. iostat 与 iowait 分析
iostat 是看「设备到底多忙」的第一工具——比 top 的 iowait 更精确:
iostat -x 1 5 # 扩展指标,每 1 秒采样 5 次
iostat -d 1 # 只看设备
| 指标 | 含义 | 判读 |
|---|---|---|
| await | 请求从进队到完成平均耗时 | 高 = 设备排队或本身慢 |
| r_await / w_await | 读写分别的 await | 读写分开看更准 |
| svctm | 设备服务时间(近似) | 接近 await 则无排队 |
| %util | 设备忙的百分比 | 接近 100% 且 await 高 = 饱和 |
| r/s, w/s | 每秒读写请求数 | 与期望 IOPS 对比 |
# iowait 是什么:CPU 等待 IO 完成的时间占比
top -1
# 真正排障时组合:iostat 看设备 + pidstat -d 看哪个进程在 IO
pidstat -d 1 5
铁律:%util=100% 不等于「盘坏了」,只说明设备是瓶颈。SSD 在高并发下 util 可能虚高(忙等待也算 util),要结合 await 与延迟分位数判断,而不是只看 %util 一个数。
# 看延迟分布比均值更有意义
# fio 或 bcc 的 biolatency 工具给出直方图
8. fio 压测实战
fio 是工业标准的 IO 压测工具——用 job 文件描述「读/写/随机/队列深度」:
sudo apt install fio
# 一条命令随机读 64KB、队列深度 32、测 60 秒
fio --name=randread --rw=randread --bs=64k --iodepth=32 \
--size=4G --numjobs=4 --direct=1 --time_based --runtime=60 \
--filename=/tmp/test --group_reporting
# 完整 job 文件:顺序写 + 随机读两段
[global]
direct=1
ioengine=libaio # 或 io_uring
size=4G
[seqwrite]
rw=write
bs=128k
iodepth=16
[randread]
rw=randread
bs=4k
iodepth=64
| rw 模式 | 含义 | 关注指标 |
|---|---|---|
| read / write | 顺序读写 | 带宽(MiB/s) |
| randread / randwrite | 随机读写 | IOPS、延迟分位数 |
| rw=rw | 混合读写 | 比例 rwmixread |
记忆:压测要贴着真实负载配参数——数据库在线业务测
randread 8k iodepth=32,日志系统测write 4k fsync=1。direct=1绕过页缓存才能测设备真性能,否则测的是缓存。
9. 性能调优清单
一份按「设备 → 队列 → 文件系统 → 应用」顺序执行的调优清单:
# 1. 调度器:SSD 用 none,HDD 用 mq-deadline
echo none > /sys/block/nvme0n1/queue/scheduler
# 2. 队列深度与预读
echo 1024 > /sys/block/nvme0n1/queue/nr_requests
blockdev --setra 4096 /dev/sda # 顺序读预读(KB)
# 3. 文件系统挂载
mount -o noatime /dev/vg0/data /data
# 4. SSD 定期 TRIM
systemctl enable --now fstrim.timer
# 5. 对齐校验:分区/条带对齐到 4K
fdisk -l /dev/sda # 看 Start 扇区是否 8 的倍数
# 6. 脏页参数配合写吞吐(见内存专题)
sysctl -w vm.dirty_background_ratio=5
sysctl -w vm.dirty_ratio=30
# 7. 应用侧:日志与数据分离盘、调大连接池/队列并发
心法:调优顺序 = 先保证设备健康(TRIM/固件)→ 再选对调度器与队列 → 然后文件系统挂载项 → 最后才调应用并发。每步用 fio 前后对比,数字说话,别凭感觉。
10. 速查表
| 需求 | 命令/参数 |
|---|---|
| 看设备忙度 | iostat -x 1 |
| 看哪个进程在 IO | pidstat -d 1 |
| 切换调度器 | echo none > /sys/block/*/queue/scheduler |
| 改队列深度 | echo 1024 > /sys/block/*/queue/nr_requests |
| 压测随机读 | fio --rw=randread --bs=4k --iodepth=64 |
| 压测顺序写 | fio --rw=write --bs=128k --iodepth=16 |
| 绕过缓存测盘 | dd ... oflag=direct |
| 禁用 atime | mount -o noatime |
| SSD 保养 | systemctl enable --now fstrim.timer |
| 软 RAID 建盘 | mdadm --create /dev/md0 --level=10 ... |
| LVM 建卷 | pvcreate → vgcreate → lvcreate |
| 强制落盘 | 应用内 fsync/fdatasync |
一句话记忆:IO 栈自上而下是 VFS→页缓存→文件系统→块层→设备;SSD 配 none、HDD 配 mq-deadline,追求低延迟可上 io_uring;压测用 fio、看盘用 iostat,调优每步前后对比数字。
延伸阅读
- /linux-filesystem-disk/ — 文件系统与磁盘基础
- /linux-performance-tuning/ — 性能监控与调优方法论
- /linux-kernel-tuning/ — sysctl 内核参数调优
- /linux-backup-disaster-recovery/ — 存储冗余与备份策略
- /linux-containers-isolation/ — 容器 IO 限制与隔离
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。