文件系统和磁盘是最容易被"混为一谈"的两个层次。开发说"磁盘慢",可能实际是文件系统配置问题;运维说"文件系统满了",可能实际是inode耗尽。理解I/O栈的每一层,才能找准真正的瓶颈。
第5章 文件系统与磁盘:穿透I/O栈的迷雾
5.1 故事:那个"磁盘满了"的周三
周三上午,某视频平台的转码服务报警:"磁盘使用率98%"
运维小张慌了,开始清理日志:
find /var/log -name "*.log" -mtime +7 -delete
df -h使用率降到了85%,但服务还是报错:"No space left on device"
"明明还有15%空间啊?"小张困惑。
他检查了inode:
df -i
# Filesystem Inodes IUsed IFree IUse% Mounted on
# /dev/sda1 655360 655360 0 100% /inode 100%了。
继续查:
# 找小文件最多的目录
find /data -xdev -printf '%h\n' | sort | uniq -c | sort -rn | head
# 123456 /data/thumbnails
ls /data/thumbnails | wc -l
# 6000000原来转码服务生成的缩略图是按毫秒时间戳命名的,每个视频几千张。600万个文件,每个文件平均2KB,inode用光了,磁盘空间还剩15%。
根因:不是磁盘空间问题,是文件系统设计问题——小文件过多耗尽inode。
修复:
- 缩略图按视频ID分目录存储(每目录不超过1万文件)
- 定期合并小文件到对象存储(S3/OSS)
- 文件系统从ext4换到XFS(动态inode分配)
5.2 文件系统类型对比
graph TD
A["文件系统选择"] --> B[ext4]
A --> C[XFS]
A --> D[ZFS]
A --> E[Btrfs]
B --> B1["通用,成熟"]
B --> B2["固定inode"]
B --> B3["小文件多易出问题"]
C --> C1["大文件,数据库"]
C --> C2["动态inode"]
C --> C3["延迟分配优化"]
D --> D1["企业级,数据完整性"]
D --> D2["写时复制"]
D --> D3["压缩/快照"]
E --> E1["开发测试"]
E --> E2["类似ZFS"]
E --> E3[成熟度和ZFS
style B fill:#e3f2fd
style C fill:#e8f5e9
style D fill:#fff3e0| 特性 | ext4 | XFS | ZFS | Btrfs |
|---|---|---|---|---|
| 最大文件 | 16TB | 8EB | 16EB | 16EB |
| 最大卷 | 1EB | 8EB | 256ZB | 16EB |
| inode | 固定分配 | 动态分配 | 动态 | 动态 |
| 数据校验 | ❌ | ❌ | ✅ | ✅ |
| 压缩 | ❌ | ❌ | ✅ | ✅ |
| 快照 | ❌ | ❌ | ✅ | ✅ |
| 写时复制 | ❌ | ❌ | ✅ | ✅ |
| 适用场景 | 通用 | 大文件/DB | 数据安全 | 开发测试 |
5.3 I/O栈全景
graph TD
A["应用程序
read/write/mmap"] --> B["VFS
虚拟文件系统"]
B --> C[ext4/XFS/ZFS]
C --> D["日志层
JBD2"]
D --> E["页缓存
Page Cache"]
E --> F{Direct I/O?}
F -->|"否"| G["脏页回写
writeback"]
F -->|"是"| H["绕过缓存"]
G --> I["块层
Block Layer"]
H --> I
I --> J["I/O调度器"]
J --> K["设备驱动"]
K --> L["硬件队列
NVMe/SATA"]
L --> M["磁盘/SSD"]
style E fill:#e8f5e9
style I fill:#fff3e0
style M fill:#ffebee关键层次:
| 层次 | 功能 | 性能调优点 |
|---|---|---|
| VFS | 统一接口,抽象具体文件系统 | dentry缓存 |
| 文件系统 | 文件组织、元数据管理 | 挂载参数 |
| 页缓存 | 减少磁盘I/O | 缓存命中率 |
| 块层 | I/O请求合并、排序 | 调度器选择 |
| 设备驱动 | 硬件交互 | 队列深度 |
| 硬件 | 物理I/O执行 | SSD/HDD/NVMe |
5.4 I/O调度器
调度器对比
graph LR
subgraph "CFQ 完全公平队列"
A1["按进程分组"]
A2["时间片轮转"]
A3["适合桌面"]
end
subgraph "Deadline"
B1["读写分离队列"]
B2["防止饥饿"]
B3["适合数据库"]
end
subgraph "NOOP"
C1["简单FIFO"]
C2["无排序"]
C3["适合SSD"]
end
subgraph "BFQ"
D1["预算机制"]
D2["带宽保证"]
D3["适合交互式"]
end| 调度器 | 算法 | 适用场景 |
|---|---|---|
| CFQ | 按进程分组,时间片轮转 | 桌面系统(已过时) |
| Deadline | 读写分离,防止饥饿 | 数据库、高IOPS |
| NOOP | 简单FIFO | SSD、NVMe(推荐) |
| BFQ | 预算机制,带宽保证 | 交互式、多媒体 |
| Kyber | 双队列,快速设备 | NVMe(新选择) |
# 查看当前调度器
cat /sys/block/sda/queue/scheduler
# [mq-deadline] kyber none
# 对于NVMe,通常用none(即NOOP)
cat /sys/block/nvme0n1/queue/scheduler
# none [mq-deadline] kyber
# 修改调度器
echo none > /sys/block/nvme0n1/queue/scheduler
# 永久生效(grub)
GRUB_CMDLINE_LINUX_DEFAULT="elevator=none"5.5 磁盘性能分析
核心指标
# iostat输出解读
iostat -xz 1
Device r/s w/s rkB/s wkB/s rrqm/s wrqm/s %rrqm %wrqm r_await w_await aqu-sz %util
nvme0n1 1000 500 4000 2000 200 100 20 20 0.50 0.30 0.75 45.0
# r/s, w/s:每秒读/写I/O数(IOPS)
# rkB/s, wkB/s:每秒读/写吞吐量
# r_await, w_await:每次读/写的平均等待时间(ms)
# aqu-sz:平均队列长度
# %util:设备忙碌时间百分比关键指标解读:
| 指标 | 健康范围 | 超标含义 |
|---|---|---|
| %util | <60% | 接近100%表示饱和 |
| await | SSD<1ms, HDD<10ms | 越高说明排队越久 |
| aqu-sz | 接近1 | 持续>1说明有排队 |
SSD vs HDD vs NVMe
graph TD
subgraph HDD
A1["旋转延迟 5-10ms"]
A2["寻道延迟 2-10ms"]
A3["IOPS 100-200"]
A4["吞吐 100-200MB/s"]
end
subgraph SSD
B1["无机械延迟"]
B2["FTL映射"]
B3["IOPS 10K-100K"]
B4["吞吐 500MB-3GB/s"]
B5["写入放大 WA"]
end
subgraph NVMe
C1["PCIe直连"]
C2["多队列 64K"]
C3["IOPS 500K+"]
C4["吞吐 3-7GB/s"]
C5["延迟 10μs"]
end
style A1 fill:#ffebee
style C1 fill:#e8f5e95.6 fio:磁盘性能测试标准工具
# ===== 随机读IOPS测试(NVMe) =====
fio --name=randread --directory=/mnt/nvme \
--rw=randread --bs=4k --size=4G \
--numjobs=4 --iodepth=32 --runtime=60 \
--direct=1 --group_reporting
# 输出:
# randread: (groupid=0, jobs=4)
# read: IOPS=125k, BW=488MiB/s
# ===== 混合读写(模拟数据库) =====
fio --name=mix --directory=/mnt/nvme \
--rw=randrw --rwmixread=70 --bs=8k --size=4G \
--numjobs=8 --iodepth=64 --runtime=120 \
--direct=1
# ===== 顺序写(模拟日志) =====
fio --name=seqwrite --directory=/mnt/nvme \
--rw=write --bs=1M --size=10G \
--numjobs=1 --direct=15.7 挂载参数调优
# 数据库专用挂载(ext4)
mount -t ext4 -o noatime,nodiratime,nobarrier,data=writeback /dev/sdb1 /data
# 参数解释:
# noatime:不更新访问时间,减少写操作
# nodiratime:不更新目录访问时间
# nobarrier:禁用写屏障(SSD安全,HDD有风险)
# data=writeback:写回模式,最快但crash可能丢数据
# 对于XFS(推荐数据库)
mount -t xfs -o noatime,nobarrier,largeio,inode64 /dev/sdb1 /data
# 对于ZFS
zfs set compression=lz4 tank/data
zfs set recordsize=128K tank/data # 数据库用8K或16K5.8 本章总结
graph TD
A["磁盘I/O问题"] --> B{看iostat}
B -->|"util高"| C["设备饱和"]
B -->|"await高"| D["排队或设备慢"]
B -->|"aqu-sz高"| E["请求堆积"]
C --> F["换更快设备/NVMe"]
D --> G["调调度器/扩容"]
E --> H["减并发/加队列深度"]
A --> I{文件系统}
I -->|"inode满"| J["换XFS/ZFS"]
I -->|"元数据慢"| K["优化挂载参数"]
I -->|"缓存miss"| L["加大内存/Page Cache"]
style F fill:#e8f5e9
style G fill:#e8f5e9
style H fill:#e8f5e9核心要点:
- inode和磁盘空间是两个独立指标——小文件场景关注inode
- I/O调度器对SSD用none/NOOP,对HDD用deadline
- iostat的await比util更能反映真实延迟
- 挂载参数能显著提升性能——noatime和barrier选择需谨慎
- fio是测试磁盘性能的金标准——学会用它建立基线
"磁盘是系统中最慢的组件,但也是最容易被错误诊断的瓶颈。"