Linux 存储栈全景:从 VFS 到块设备层的 IO 路径解剖
一个ead() 系统调用,从敲下去到数据返回,中间穿越了多少层?如果你答不上来,那你永远无法真正理解"为啥我的 MySQL 写入这么慢"或"Nginx 大文件传输为何卡住"。
本文带你走一遍 Linux 存储 IO 的完整链路:从 VFS 虚拟文件系统 → 具体文件系统 → Page Cache → 块层(Block Layer)→ I/O 调度器 → 块设备驱动 → 硬件。每层做什么、能卡在哪、怎么观测,一次讲透。
每一层都是延迟放大器,也都是优化切入点。我们从最上面开始一层层往下拆。
02
PART 02:VFS —— 统一江湖的抽象层
VFS(Virtual File System)是 Linux 最伟大的设计之一。它定义了文件系统必须实现的通用接口。
当你执行 open("/data/log/app.log", O_RDWR):
- VFS 通过 dentry cache 查找 /data/log/app.log 的目录路径
关键观测:
`
slabtop -o | grep dentry
cat /proc/sys/fs/inode-nr
cat /proc/sys/fs/file-max
`
卡点:dentry cache 过大导致 slab 内存吃紧、路径名查找锁竞争(特别是 dcache_lock 时代,RCU 已改善)。
03
PART 03:具体文件系统 —— 数据到底存哪了
VFS 把请求分发到实际的文件系统(ext4 / xfs / btrfs / zfs)。这里才是真正决定数据组织的地方。
write() → VFS → ext4_write_begin() → ext4_reserve_space (分配物理块) → ext4_journal_start (事务开始) → ext4_write_end (数据写入页缓存) → ext4_journal_stop (事务提交) → 等待日志写入 Journal (JBD2) → 数据最终回写到 inode 指向的数据块
1. 日志(Journal)写入
ext4 data=ordered 模式下,元数据先写日志再写磁盘。日志本身是顺序 IO,看似快,但 fsync 时 必须等待日志落盘,这就是 PostgreSQL 常说"ext4 barrier 问题"的来源。
`
tune2fs -l /dev/sda1 | grep -i journal
mount -o data=writeback /dev/sda1 /data
`
2. 分配器碎片
ext4 使用 extents 树(B+树变体),大文件连续分配没问题,但小文件长期写入导致 extent 分裂,IO 路径变长。
04
PART 04:Page Cache —— 被忽略的性能放大器
Linux 会把读到的磁盘数据缓存在内存中,这个区域叫 Page Cache,以 4KB 页为基本单位。
`
/proc/sys/vm/dirty_ratio # 默认 20(%),脏页占内存比例达到此值阻塞写入进程
/proc/sys/vm/dirty_background_ratio # 默认 10(%),后台 pdflush 开始回写
/proc/sys/vm/dirty_expire_centisecs # 默认 3000(30s),脏页最大存活时间
`
血泪教训:Page Cache 导致的 MySQL 双写缓冲
很多 MySQL DBA 会发现:明明数据库有 Buffer Pool,磁盘写入还是慢。原因之一是 Page Cache 的写回机制造成了额外的 IO。
MySQL 通过 innodb_flush_method=O_DIRECT 跳过 Page Cache(直接写入磁盘),但这又把压力扔给了磁盘——需要数据库自身的 Buffer Pool 足够大。
更推荐的折中:innodb_flush_method=O_DIRECT_NO_FSYNC(MySQL 8.0+ 支持)
观测命令:
`
grep -E "^Cached|^Buffers" /proc/meminfo
cat /proc/vmstat | grep dirty
sync
`
05
PART 05:Block Layer —— 通用块层
Page Cache 下层就是 通用块层(Generic Block Layer)。它负责:
- 将文件系统发来的 bio(Block IO 请求)合并与排序
- 抽象出统一的块设备接口(不关心底下是 SSD 还是 HDD)
write() → 页缓存 → 生成 bio → 加入当前 CPU 的 plug list → schedule() 或特意 unplug 时 → bio 合并、排序 → 提交给调度器
这个机制减少了调度器的调用次数,是 Linux 内核 IO 高性能的关键设计之一。
06
PART 06:I/O 调度器 —— 把乱序变成有序
I/O 调度器决定 BIO 的下发顺序。历史上 Linux 有多个调度器:
从内核 5.0 起,多队列块层(blk-mq)已成为默认:
`
cat /sys/block/nvme0n1/queue/scheduler
echo none > /sys/block/nvme0n1/queue/scheduler
`
核心结论:NVMe SSD → none;SATA SSD → mq-deadline;HDD → BFQ 或 mq-deadline
传统 SATA(AHCI)是单队列模型,锁竞争严重。NVMe 支持 64K 队列对,每个 CPU 可独占队列,无锁设计、中断聚合。NVMe 把 IOPS 从 SATA SSD 的 ~10 万推到百万级,瓶颈从硬件转移到内核栈本身——这也是 io_uring、SPDK 应运而生的原因。
以 NVMe SSD 写 4KB 数据为例:
总共约 43us,其中真正的物理写入占 70%。优化空间主要在前三段软件栈。
| | | |
|---|
| strace -e trace=read,write,open,fsync | | |
| | | |
| | | |
| /sys/block/*/queue/scheduler | | |
| | | |
| | | |
- 定位 IO 是否在等盘:iostat -x 1,r_await > 10ms 说明盘在忙
- 看 Page Cache 刷盘压力:cat /proc/meminfo | grep -E "Dirty|Writeback"
- 看调度器队列深度:cat /sys/block/nvme0n1/queue/nr_requests
`
APP → VFS → 具体 FS → Page Cache → Block Layer → I/O Scheduler → 驱动 → 磁盘
每一层都有观测工具,每一层都有调优空间。不懂全景,调优就是瞎蒙。
下一篇我们进入 文件系统选型对决,用实测数据对比 xfs/ext4/btrfs/zfs 在不同场景下的真实表现。
预告:《文件系统选型对决:XFS vs ext4 vs Btrfs vs ZFS 性能与场景深度对比》
END