文件系统:从 VFS 到 XFS 挂载参数
存储服务底下压着的还是本地文件系统,它的行为直接决定上层表现。
学完这节你能做到
- 解释 VFS、inode、dentry cache 在读写路径中的位置
- 说明日志(journal)如何影响写放大
- 为存储节点选择合理的 XFS/ext4 挂载参数
分布式存储底下压着的还是本地文件系统
即使你运维的是 Ceph 或 GPFS,节点上仍然有本地文件系统在干活:MON 的 RocksDB 放在本地盘的 XFS 上,容器镜像放在本地盘上,日志也在本地盘上。而且理解本地文件系统的机制,是理解分布式文件系统的前提——CephFS 的 MDS 解决的问题,和 XFS 的 inode 解决的是同一类问题,只是尺度不同。
VFS:一层让一切都像文件的抽象
应用: open("/mnt/data/a.txt")
↓
VFS: 统一的 inode / dentry / file 抽象
↓
具体文件系统: XFS / ext4 / CephFS / NFS
↓
块层或网络
VFS 定义了三个核心对象,理解它们就理解了元数据开销从哪来:
| 对象 | 是什么 | 缓存在哪 |
|---|---|---|
| inode | 文件的元数据:大小、权限、时间戳、数据块位置 | inode cache |
| dentry | 目录项:把文件名映射到 inode | dentry cache |
| file | 一个打开的文件句柄,含当前偏移 | 进程的 fd 表 |
打开 /mnt/data/a.txt 需要逐级解析:查 / 的 dentry → 查 mnt → 查 data → 查 a.txt。路径每深一层,就多一次查找。这就是为什么深目录结构在网络文件系统上会明显更慢——每一级都可能是一次网络往返。
# 查看 inode 和 dentry 缓存占用
cat /proc/slabinfo | grep -E 'dentry|inode' | head -5
# 或者更直观
slabtop -o | head -10
一台缓存了几千万个 dentry 的机器,光 dentry cache 就能占掉几十 GB。这部分内存算在 buff/cache 里,可回收,但回收之后下一次 ls 就要重新去盘上读。这正是 CephFS MDS 缓存不足时性能锐减的同一个道理,只是发生在服务端而已。
日志(journal)与写放大
XFS 和 ext4 都是日志文件系统。它们的承诺是:元数据操作要么完整生效,要么完全没发生,掉电不会留下损坏的目录结构。
代价是同一份元数据要写两次:先写日志,再写正式位置。
创建一个文件:
1. 写 journal:记录"我要分配 inode X,在目录 D 里加一项"
2. journal 落盘(这一步可能触发 flush)
3. 写实际的 inode 和目录块
ext4 提供三种日志模式,理解它们的差别就理解了写放大:
| 模式 | 日志内容 | 安全性 | 性能 |
|---|---|---|---|
journal | 元数据 + 数据都写日志 | 最高 | 最慢,数据写两遍 |
ordered(默认) | 只记元数据,但保证数据先于元数据落盘 | 高 | 中 |
writeback | 只记元数据,不保证顺序 | 掉电可能读到旧数据 | 最快 |
XFS 只有类似 ordered 的模式,且日志实现更高效,这是它在大容量、高并发场景更受青睐的原因之一。
挂载参数:存储节点上真正该调的几个
# 格式化:XFS 通常直接用默认值即可,块大小 4K 适配绝大多数场景
mkfs.xfs -f /dev/nvme0n1p1
# 挂载
mount -o noatime,nodiratime,logbufs=8,logbsize=256k /dev/nvme0n1p1 /var/lib/ceph
| 参数 | 作用 | 建议 |
|---|---|---|
noatime | 不更新访问时间 | 必开。否则每次读都会产生一次元数据写 |
nodiratime | 目录不更新访问时间 | 建议开(noatime 已包含) |
logbufs / logbsize | XFS 日志缓冲区 | 元数据密集场景可调大 |
discard | 在线 TRIM | 不建议在生产开,改用定期 fstrim |
discard 会让每次删除都同步向 SSD 发 TRIM 命令,在删除密集时造成明显的延迟毛刺。标准做法是挂载时不加 discard,改用 fstrim 定时任务在业务低峰期批量执行:
systemctl enable fstrim.timer --now小文件问题:为什么元数据比数据更难扛
存 100 万个 4KB 文件(共 4GB)和存 1 个 4GB 文件,数据量相同,但代价天差地别:
| 100 万小文件 | 1 个大文件 | |
|---|---|---|
| inode 数量 | 100 万 | 1 |
| 元数据操作 | 至少 100 万次 create | 1 次 |
| 空间浪费 | 每个文件按块对齐,4KB 文件占满一个块 | 几乎无 |
| 顺序读吞吐 | 受随机 IOPS 限制,可能只有几百 MB/s | 接近盘的顺序上限 |
| 备份/迁移耗时 | 以小时计 | 以分钟计 |
# inode 也会耗尽,而且 df -h 看不出来
df -i
Filesystem Inodes IUsed IFree IUse% Mounted on
/dev/nvme0n1p1 524288000 523998211 289789 100% /data ← 空间没满,但文件建不了了
十有八九是 inode 耗尽。df -h 显示还有几 TB 空间,df -i 一看 IUse% 已经 100%。ext4 的 inode 数量在格式化时就固定了,事后无法扩容——只能备份数据重新格式化(mkfs.ext4 -i 调小每 inode 字节数)。XFS 的 inode 是动态分配的,不存在这个问题,这也是存储场景优先选 XFS 的一个实际理由。
一台机器 df -h 显示 /data 还有 3TB 可用,但应用写文件报 No space left on device。最该先查什么?
关于挂载参数,下面哪些做法适合存储节点?(多选)
这节课的落点
- VFS 的 inode / dentry / file 三件套决定了元数据开销的来源
- 路径每深一层就多一次查找,网络文件系统上尤其贵
- 日志保证元数据一致性,代价是写放大;不要用 writeback 换性能
noatime必开,discard不开改用fstrim.timer- 小文件的真正代价在元数据和 inode,不在容量;XFS 的动态 inode 是实际优势
延伸资料
- ·Systems Performance, 2nd Edition — Brendan Gregg