文件系统和磁盘是最容易被"混为一谈"的两个层次。开发说"磁盘慢",可能实际是文件系统配置问题;运维说"文件系统满了",可能实际是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是测试磁盘性能的金标准——学会用它建立基线
"磁盘是系统中最慢的组件,但也是最容易被错误诊断的瓶颈。"