1
2
3
4
5
6
7
作者:李晓辉

联系方式:

1. 微信:Lxh_Chat

2. 邮箱:939958092@qq.com
  1. Linux 性能调优系列(一) 建立科学的性能分析思路
  2. Linux 性能调优系列(二) 掌握基础性能监控命令
  3. Linux 性能调优系列(三) sysstat实现性能数据持久采集
  4. Linux 性能调优系列(四) 别只会用 sysstat,PCP 才是 Linux 性能监控的进阶玩法
  5. Linux 性能调优系列(五) 给你的服务器做一次全面体检
  6. Linux 性能调优系列(六) 想调内核参数?先搞懂 /proc、sysctl 和 sysfs
  7. Linux 性能调优系列(七) 还在手动改 sysctl?TuneD 智能性能调优实战来了
  8. Linux 性能调优系列(八) 进程太能吃资源怎么办?用 ulimit 和 systemd 给它加个"上限"
  9. Linux 性能调优系列(九) cgroup v2实战:防止一个烂进程干崩整台服务器
  10. Linux 性能调优系列(十) CPU 100% 到底是谁在忙?用 perf 把"元凶"揪出来
  11. Linux 性能调优系列(十一) 程序为什么卡住了?strace 和 ltrace 带你找到答案
  12. Linux 性能调优系列(十二) 服务器又卡了?SystemTap 带你杀到内核看看为啥
  13. Linux 性能调优系列(十三) 从 SystemTap 到 eBPF,Linux 性能追踪进入新时代
  14. Linux 性能调优系列(十四) CPU 这么忙,到底是谁决定“让谁先跑”?一文搞懂 Linux 调度器
  15. Linux 性能调优系列(十五) CPU 到底该先给谁?搞懂 Linux 实时调度
  16. Linux 性能调优系列(十六) Linux CPU 隔离实战,为什么实时线程绑核后延迟还会抖?
  17. Linux 性能调优系列(十七) CPU 占用不高却跑的慢?深挖 CPU 缓存性能瓶颈
  18. Linux 性能调优系列(十八) 程序申请了1GB内存,为什么系统却说它没用?
  19. Linux 性能调优系列(十九) 服务器硬盘慢得像蜗牛?先别急着换硬盘,这四招先把瓶颈找出来
  20. Linux 性能调优系列(二十) 磁盘 I/O 调度器怎么选?一文搞懂 mq-deadline、kyber、bfq 和 none
  21. Linux 性能调优系列(二十一) 磁盘为什么慢?从文件系统到 I/O 调优,一次讲透
  22. Linux 性能调优系列(二十二) Linux 网络调优别乱改 sysctl!先找瓶颈再调 TCP 缓冲区
  23. Linux 性能调优系列(二十三) MTU 从 1500 到 9000,巨帧真的能提升性能吗?
  24. Linux 性能调优系列(二十四) 从 CPU 省电到网卡卸载,聊聊 Linux 网络调优
  25. Linux 性能调优系列(二十五) 聊聊VirtIO、TuneD、virtio-mem 调优

上一篇我们聊了 Linux I/O 调度器,搞清楚了 none、mq-deadline、kyber 和 bfq 分别适合什么场景。

但是,磁盘性能优化不能只盯着调度器。

比如,同样一块 SSD,同样一个 Linux 系统,为什么有的业务随机读写很快,有的业务却在创建大量小文件时卡得厉害?为什么删除了几十 GB 的文件,虚拟磁盘在宿主机上的占用却没有明显下降?为什么明明是 SSD,业务的 I/O 延迟还是很高?

这些问题,有些出在存储设备,有些出在 I/O 调度器,还有一些就出在文件系统本身。

今天我们就从文件系统入手,聊聊 Linux 服务器上的 XFS 和 ext4,看看格式化参数、挂载选项、SSD 的 discard 机制到底有什么用,最后再用 fio 做一组可复现的性能测试。

文件系统到底在忙什么?

先想一个问题:我们在 Linux 中执行下面这条命令,究竟发生了什么?

1
echo "hello linux" > /data/test.txt

对于我们来说,这只是创建了一个文件,往里面写入一行文本。

但对操作系统来说,事情远没有这么简单。

文件系统至少需要处理以下几件事:

  • 目录管理:找到 /data 目录,建立 test.txt 这个目录项。
  • 索引节点管理:记录文件的权限、所有者、时间戳、数据块位置等元数据。
  • 空间分配:找到磁盘上可以存放文件内容的空间。
  • 数据写入:将文件内容交给内核和底层块设备。
  • 元数据持久化:按照文件系统的规则,记录必要的元数据变更,并通过日志等机制维护一致性。

这些工作并不是简单地把文件名转换成磁盘上的一个位置。

例如,目录项通常负责将文件名关联到索引节点,索引节点再描述文件的元数据及其数据块映射。文件系统还要管理空闲空间、目录结构、权限和崩溃恢复。

可以把文件系统理解成一个仓库管理员:应用程序只需要说出文件名,文件系统就要负责找到文件、分配存储空间,并确保相关元数据按照规定的方式更新。

文件系统位于存储链路的哪一层?

我们把 Linux 存储 I/O 的链路画出来:

flowchart LR
    A["应用程序<br/>read / write / fsync"] --> B["VFS 虚拟文件系统层"]
    B --> C["具体文件系统<br/>XFS / ext4"]
    C --> D["页缓存与文件系统元数据"]
    D --> E["块设备层<br/>blk-mq / I/O 调度器"]
    E --> F["设备驱动"]
    F --> G["SSD / HDD / 虚拟磁盘"]

VFS 是 Virtual File System,即虚拟文件系统层。它为应用程序提供统一的文件访问接口,让应用程序不必关心底层使用的是 XFS、ext4 还是其他文件系统。

文件系统负责组织文件和目录、分配空间、管理元数据;块设备层则负责把相关 I/O 请求继续传递给底层设备。

这里要注意,图中的页缓存并不是所有 I/O 都必须经过的路径。例如,应用程序使用 O_DIRECT 时,可以绕过普通的页缓存数据路径,但这不意味着所有文件系统处理和元数据操作都会被绕过。

因此,文件系统调优与 I/O 调度器调优解决的是不同层面的问题。

如果瓶颈出在文件系统元数据操作上,单纯切换 I/O 调度器不一定有效;如果瓶颈出在底层存储延迟上,修改文件系统挂载选项也未必能解决问题。

不同业务,为什么会有不同的文件系统性能?

想象一下下面三种业务。

场景一:备份服务器

每天写入几个几十 GB 的备份文件,主要关心持续写入吞吐量和文件系统容量。

场景二:容器镜像仓库

大量小文件、目录项、元数据操作,还有频繁的文件创建与删除。此时元数据处理能力可能比顺序读写带宽更重要。

场景三:数据库服务器

大量随机读写、频繁的同步写入,以及对延迟比较敏感的 fsync 操作。平均吞吐量不错,并不意味着业务就一定流畅。

所以,文件系统调优的第一条原则是:

先弄清楚业务在做什么,再决定要调什么。不要看到 SSD 就直接套用一组所谓的最佳参数。

XFS 和 ext4 到底怎么选?

在 RHEL 系列系统中,XFS 是默认的本地文件系统。Red Hat 建议在没有明确兼容性、业务或性能理由的情况下,优先使用 XFS。ext4 仍然是成熟、可靠的选择,并不是过时的文件系统。

XFS:适合大型存储和高并发场景

XFS 从设计上就重视大容量文件系统、高并发操作和可扩展性。

它的几个重要特点如下。

第一,元数据操作具备良好的并行扩展能力。

XFS 使用 allocation group(分配组,简称 AG)等机制组织文件系统空间。多个分配组能够帮助不同的操作并行进行,减少部分共享资源上的竞争。

这对于多 CPU、多线程以及大型文件系统比较有价值。

但要注意,XFS 并不是完全没有锁竞争,也不是任何多线程负载下都必然快于 ext4。

第二,支持在线扩容。

当 XFS 所在的底层块设备变大后,可以在文件系统仍然挂载的情况下扩展文件系统。

例如,已经扩大的 /dev/sdb1 可以通过以下命令扩展:

1
xfs_growfs /data

这里的 /data 必须是已经挂载的 XFS 文件系统,而且底层设备必须先具备足够的新增空间。

如果底层使用 LVM,还需要先扩展逻辑卷。xfs_growfs 不会自动扩大底层磁盘或逻辑卷。

第三,支持在线碎片整理。

XFS 提供 xfs_fsr 等工具,可以在文件系统保持挂载的情况下整理部分文件碎片。

但是,不要把它理解成必须定期运行的维护任务。SSD 上是否需要整理,要看实际碎片情况、文件访问模式和业务表现。

第四,通过元数据日志帮助恢复。

XFS 使用日志记录必要的元数据变更,以便在异常关机后恢复文件系统的一致性。

这里的日志主要是元数据日志,并不意味着所有用户数据都被完整地写入日志。

第五,不能直接缩小文件系统。

这是 XFS 一个需要提前考虑的限制。

如果你创建了一个 1 TiB 的 XFS 文件系统,后来只想保留 500 GiB,不能直接使用一个类似 resize2fs 的命令把它缩小。

通常需要备份数据、重建较小的文件系统,再恢复数据。

因此,分区规划、LVM 规划和容量预留很重要。

ext4:成熟、通用,功能同样完整

ext4 是 Linux 中非常成熟的文件系统,拥有广泛的工具支持和运维经验。

它也支持日志、延迟分配、扩展区段等机制,在许多工作负载中都能获得不错的性能。

它的主要特点包括:

  • 工具成熟,排障和维护经验丰富。
  • 支持在线扩容。
  • 支持卸载后缩容,适合容量需要灵活调整的场景。
  • 在很多常见负载下,可以达到与 XFS 相近的性能。

这里有一个容易被忽略的细节:ext4 的理论文件系统容量上限,不等于某个 RHEL 版本实际支持的容量。

例如,RHEL 10 的官方资料列出的 ext4 文件系统支持上限为 50 TiB,单个文件的支持上限为 16 TiB;ext4 的理论格式容量上限则更高。实际使用时应以操作系统版本和官方支持矩阵为准。

不要用固定的 IOPS 数字来决定文件系统

文件系统性能不仅取决于 IOPS 和带宽,还与以下因素有关:

  • 文件大小和目录结构;
  • 随机读写还是顺序读写;
  • 线程数量与并发度;
  • 同步写入的比例;
  • 元数据操作频率;
  • 内核版本、文件系统特性和挂载参数;
  • 底层存储的延迟与缓存行为。

建议这样选:

场景建议
常规 RHEL 服务器优先使用 XFS
大容量存储、大文件、高并发优先考虑 XFS
需要卸载后缩容可以考虑 ext4
已有成熟的 ext4 运维规范没有明确问题时不必迁移
数据库或容器存储结合实际负载做基准测试

记住,不要为了追求某个文件系统的理论优势,就随意重建生产环境中的文件系统。 文件系统迁移涉及数据备份、停机窗口、恢复验证和业务风险,应该有明确的收益依据。

SSD 上的 discard 到底有什么用?

这是文件系统调优中一个特别容易产生误解的话题。

先看一个场景。

你的 /data 目录中有一个 100 GiB 的文件。你执行:

1
rm -f /data/bigfile.dat

随后查看文件系统空间:

1
df -h /data

你可能会发现,可供 Linux 文件系统使用的空间增加了。

但是,为什么底层 SSD 或宿主机上的虚拟磁盘文件不一定立即变小?

因为这里涉及两件不同的事情:

  1. 文件系统把原来属于该文件的数据块标记为空闲。
  2. 文件系统把这些已经不再使用的块的信息传递给底层存储。

第一件事并不保证第二件事自动发生。

discard 是什么?

discard 是一种块设备操作,用来通知底层存储:某些逻辑块已经不再需要保留原来的数据内容。

对于 SSD,这些信息可以帮助设备进行垃圾回收和磨损均衡;对于精简置备存储,它也可能帮助底层回收不再使用的空间。

不过,discard 不是简单地告诉 SSD“这些数据已经被物理擦除”,也不保证每次执行后都能立即减少宿主机磁盘占用。

Linux 有哪三种常见的 discard 方式?

方式一:手动批量丢弃

1
fstrim /data

它会扫描指定文件系统中符合条件的空闲区域,并向底层提交 discard 请求。

适合维护操作、批量删除数据之后,或者你想手动验证 discard 是否可用的场景。

方式二:周期性批量丢弃

Linux 可以通过 systemd 定时器定期运行 fstrim。

检查定时器状态:

1
2
3
4
5
6
7
8
9
10
[root@localhost ~]# systemctl status fstrim.timer
● fstrim.timer - Discard unused filesystem blocks once a week
Loaded: loaded (/usr/lib/systemd/system/fstrim.timer; enabled; preset: enabled)
Active: active (waiting) since Mon 2026-10-12 15:37:07 CST; 1h 48min ago
Invocation: a8dc49d60929462cbbe26eee443b2061
Trigger: Mon 2026-10-19 00:20:06 CST; 6 days left
Triggers: ● fstrim.service
Docs: man:fstrim

Oct 12 15:37:07 localhost systemd[1]: Started fstrim.timer - Discard unused filesystem blocks once a week.

如果系统提供了这个定时器,但没有启用,可以执行:

1
[root@localhost ~]# systemctl enable --now fstrim.timer

启用后,再查看下一次触发时间:

1
2
3
4
5
6
[root@localhost ~]# systemctl list-timers fstrim.timer
NEXT LEFT LAST PASSED UNIT ACTIVATES
Mon 2026-10-19 01:02:18 CST 6 days Mon 2026-10-12 16:00:12 CST 1h 25min ago fstrim.timer fstrim.service

1 timers listed.
Pass --all to see loaded but inactive timers, too.

需要注意,不要假设所有系统都默认启用了该定时器。先检查当前系统状态,再决定是否需要启用。

方式三:在线丢弃

在挂载文件系统时使用 discard 选项:

1
mount -o discard /dev/sdb1 /data

如果需要持久化配置,可以在 /etc/fstab 中对应的文件系统条目加入 discard。

在线丢弃会在文件系统释放块时提交相应的 discard 请求。不过,不同设备、文件系统和内核版本的实现细节可能不同,实际开销也会有差异。

Red Hat 对常规场景的建议是优先使用批量或周期性 discard;只有在工作负载确实需要在线丢弃时,才考虑持续启用 discard。

为什么我的 fstrim 会报错?

1
2
[root@localhost ~]# fstrim /mnt
fstrim: /mnt: the discard operation is not supported

不要直接认定是文件系统损坏。

首先检查底层设备:

1
2
3
4
5
6
7
8
9
[root@localhost ~]# lsblk -D
NAME DISC-ALN DISC-GRAN DISC-MAX DISC-ZERO
sr0 0 512B 0B 0
nvme0n1 0 512B 0B 0
├─nvme0n1p1 0 512B 0B 0
├─nvme0n1p2 0 512B 0B 0
└─nvme0n1p3 0 512B 0B 0
├─rlm-root 0 512B 0B 0
└─rlm-swap 0 512B 0B 0

重点观察 DISC-MAX。如果底层块设备的 discard_max_bytes 为零,说明它当前没有提供可用的物理 discard 能力。

也可以直接查看:

1
2
3
4
5
[root@localhost ~]# cat /sys/block/nvme0n1/queue/discard_max_bytes
0
[root@localhost ~]# cat /sys/block/nvme0n1/queue/discard_granularity
512

如果使用了 LVM、device-mapper、加密设备或者虚拟磁盘,还需要检查整条设备映射链。

例如,在 VMware 虚拟机中,即使客户机看到的是 NVMe 设备,也不意味着虚拟磁盘一定支持 discard。虚拟磁盘控制器、虚拟磁盘配置和宿主机存储都可能影响这项能力。

如果 DISC-MAX=0B,先查清楚设备为什么没有提供 discard,不要通过随意修改文件系统参数来解决。

格式化时有哪些参数值得了解?

文件系统的创建参数会影响文件系统布局和可用特性,有些参数创建后很难修改,有些则可以通过专用工具调整。

但这里要先强调一点:

格式化参数不是越多越好,也不是每个参数都能带来性能提升。

对于通用服务器,建议先使用合理的默认值。只有在明确知道工作负载、空间分布和目标收益时,才考虑特殊配置。

以下命令均为理解和实验用途。mkfs 会创建文件系统,可能破坏目标设备上已有的数据,切勿直接对生产磁盘执行。

XFS:索引节点和目录结构

先看 XFS 的索引节点大小参数:

1
mkfs.xfs -i size=512 /dev/sdb1

-i size=512 指定 XFS 索引节点大小为 512 字节。在支持该配置的环境中,它决定了索引节点的大小和相关元数据空间布局。

索引节点不仅保存文件属性,还可能承载扩展属性等信息。因此,修改它并不是简单地“给元数据多留点空间”,还会影响文件系统的布局和资源开销。

对于常见服务器,默认值通常已经足够,不建议仅仅因为业务有很多小文件就随意修改。

再看目录块大小:

1
mkfs.xfs -n size=8192 /dev/sdb1

-n size=8192 指定目录块大小为 8192 字节,但实际可用值还受到文件系统块大小及工具版本限制。

它并不意味着目录操作必然更快。目录查询性能还与目录项数量、目录索引、缓存命中率和实际访问模式有关。

如果没有真实测试数据,不要为了减少目录 I/O 就直接把目录块调大。

ext4:索引节点大小

ext4 可以通过 mkfs.ext4 -I 指定索引节点大小。

例如:

1
mkfs.ext4 -I 256 /dev/sdb1

这里指定每个索引节点占用 256 字节。具体默认值取决于发行版和工具配置,RHEL 系列常见配置为 256 字节。

索引节点数量和大小需要结合文件系统容量、文件数量及扩展属性需求考虑。

对于小文件特别多的文件系统,索引节点布局确实值得关注,但不能简单地得出“索引节点越小越省空间、越快”的结论。

如果索引节点过小,可能影响某些扩展元数据的存储能力;而改变索引节点大小通常需要在创建文件系统时完成。

因此,除非你有明确的容量规划依据,否则优先使用发行版的默认值。

ext4 的 large_dir 和 huge_file

ext4 还有一些文件系统特性,例如 large_dir 和 huge_file。

large_dir 用于提升目录所能容纳的规模,适合目录中存在大量文件、并且目录大小已经接近相关限制的场景。

huge_file 则与支持大文件有关。它并不是一个为了减少普通小文件开销而应该随意关闭的功能。

检查当前文件系统的特性:

1
tune2fs -l /dev/sdb1

如果想查看功能标志,可以重点关注 Filesystem features 字段。

不要因为业务主要存放小文件,就直接执行下面这样的命令:

1
tune2fs -O large_dir,^huge_file /dev/sdb1

它不是通用的性能调优命令。修改文件系统特性可能影响兼容性、功能支持和后续挂载行为,有些操作还需要离线检查或满足特定前提。

另外,extra_isize 与 ext4 扩展索引节点字段有关。不要把它当作一个可以为了省空间而随意关闭的普通开关。

RAID 条带对齐需要手工指定吗?

XFS 提供 su、sw 等格式化参数,ext4 则有 stride 和 stripe_width 等参数,用于描述某些底层条带布局。

这些参数在特定 RAID 配置中有意义,但现代存储栈可能已经通过设备拓扑信息提供了对齐信息。

如果你使用的是硬件 RAID、SAN 或虚拟磁盘,先确认底层设备暴露出来的几何与对齐信息,不要凭经验随便填写。

对于普通 SSD 和 VMware 虚拟磁盘,优先使用合理的默认配置通常更稳妥。

挂载选项:最值得先理解的日常配置

格式化参数影响文件系统布局,而挂载选项通常可以在不重建文件系统的情况下调整。

不过,挂载选项是否适合生产环境,仍然要结合业务需求和文件系统类型判断。

atime、relatime 和 noatime

Linux 文件系统可以记录文件的访问时间,也就是 atime。

例如,你读取一个文件时,文件的访问时间可能发生更新。更新访问时间本身也会带来元数据操作。

常见选项包括:

  • atime:启用访问时间更新。
  • relatime:相对访问时间更新,减少不必要的时间戳写入。
  • noatime:不更新文件和目录的访问时间。
  • nodiratime:不更新目录的访问时间。

在现代 Linux 中,relatime 通常是常见默认行为。它并不是每读取一次文件就必须写一次磁盘,而是根据访问时间与修改时间等条件决定是否更新。

如果业务不依赖 atime,noatime 可以进一步减少相关元数据更新。但它是否能明显提升性能,取决于工作负载。

例如,文件服务器如果大量读取文件,且访问时间记录并非业务要求,可以考虑评估 noatime;但如果某个应用依赖访问时间实现清理、归档或其他逻辑,就不能随意关闭。

查看当前挂载选项:

1
2
[root@localhost ~]# findmnt -no SOURCE,FSTYPE,OPTIONS /
/dev/mapper/rlm-root xfs rw,relatime,seclabel,attr2,inode64,logbufs=8,logbsize=32k,noquota

不要只凭经验就把所有生产文件系统都改成 noatime。先确认业务是否依赖访问时间,再决定是否调整。

XFS 的 inode64

inode64 允许 XFS 在更广泛的文件系统地址范围内分配索引节点,而不局限于较低的磁盘地址范围。

它在大型文件系统中具有意义,能够避免旧式 inode 分配限制带来的空间分布问题。

现代 RHEL 环境中,这通常不是一个需要手动追求的性能开关。应以当前版本的默认行为和挂载信息为准。

XFS 的 logbsize

XFS 使用元数据日志记录必要的元数据变更。logbsize 可以控制内存中每个日志缓冲区的大小。

例如,在支持相应值的系统中,可以尝试:

1
mount -o remount,logbsize=256k /data

但这只是一个实验示例。执行之前要确认当前内核、XFS 版本和挂载选项支持该值,并且 /data 确实是 XFS 文件系统。

logbsize 不等于整个日志的大小,也不等于应用每次写入的数据块大小。

增大日志缓冲区可能改变元数据日志处理的行为,但不保证随机读 IOPS 上升,也不保证写入延迟下降。对于某些工作负载,效果可能很小,甚至没有收益。

因此,不要把 logbsize=256k 当成所有 XFS 服务器的通用最佳实践。

ext4 的 journal_ioprio

ext4 支持 journal_ioprio,用于设置日志 I/O 的优先级。

例如:

1
journal_ioprio=3

该选项的具体效果与内核实现及 I/O 调度机制有关。数值越小通常表示优先级越高,但它并不是一个可以直接保证业务延迟下降的参数。

只有在明确了解当前调度机制、日志负载以及业务瓶颈的情况下,才值得考虑调整。

修改挂载选项的正确方式

先检查:

1
findmnt /data

如果只想临时测试某个选项,可以在确认支持后使用重新挂载的方式。

例如:

1
mount -o remount,noatime /data

如果需要永久生效,再修改 /etc/fstab 对应条目。

但要注意,重新挂载不代表所有选项都能随时修改,也不代表修改后一定有性能收益。涉及生产文件系统时,应先确认选项的支持情况,并安排验证和回滚方案。

用 fio 验证文件系统调优有没有效果

讲了这么多参数,最后还是要回到测试。

如果没有测试,只是把 noatime、logbsize 等选项加上,然后觉得系统变快了,这并不能证明调优有效。

因为磁盘性能还会受到以下因素影响:

  • 页缓存命中率;
  • 文件系统缓存与元数据缓存;
  • SSD 自身的缓存和垃圾回收;
  • 虚拟机调度和宿主机负载;
  • 并发线程数与 I/O 队列深度;
  • 文件系统空间分布和碎片情况。

安装 fio

在 RHEL 或 Rocky Linux 中,可以先检查软件包是否可用:

1
[root@localhost ~]# dnf install fio -y

准备独立的测试目录

先确认 /data 是你打算测试的文件系统:

1
2
findmnt /data
df -h /data

确保目录有足够的可用空间,并且不会影响生产业务。

创建一个专门用于测试的目录:

1
mkdir -p /data/fio-lab

不要把测试文件直接写到生产数据库文件、虚拟机镜像或其他真实业务数据上。

先跑一个随机混合读写测试

下面是一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
[root@localhost ~]# fio --name=db_test \
--directory=/data/fio-lab \
--filename=fio-testfile \
--size=1G \
--time_based \
--runtime=60 \
--ramp_time=10 \
--ioengine=libaio \
--direct=1 \
--bs=4k \
--rw=randrw \
--rwmixread=70 \
--numjobs=4 \
--iodepth=32 \
--group_reporting
db_test: (g=0): rw=randrw, bs=(R) 4096B-4096B, (W) 4096B-4096B, (T) 4096B-4096B, ioengine=libaio, iodepth=32
...
fio-3.36
Starting 4 processes
db_test: Laying out IO file (1 file / 1024MiB)
Jobs: 1 (f=1): [m(1),_(3)][100.0%][eta 00m:00s]
db_test: (groupid=0, jobs=4): err= 0: pid=41948: Mon Oct 12 17:32:44 2026
read: IOPS=53.0k, BW=207MiB/s (217MB/s)(18.3GiB/90509msec)
slat (usec): min=2, max=1943, avg= 7.02, stdev=23.77
clat (usec): min=16, max=30510k, avg=1205.71, stdev=60718.42
lat (usec): min=61, max=30510k, avg=1212.73, stdev=60718.45
clat percentiles (usec):
| 1.00th=[ 293], 5.00th=[ 644], 10.00th=[ 824], 20.00th=[ 938],
| 30.00th=[ 988], 40.00th=[ 1020], 50.00th=[ 1057], 60.00th=[ 1090],
| 70.00th=[ 1139], 80.00th=[ 1221], 90.00th=[ 1401], 95.00th=[ 1631],
| 99.00th=[ 2040], 99.50th=[ 2212], 99.90th=[ 2835], 99.95th=[ 3097],
| 99.99th=[ 3785]
bw ( KiB/s): min=288742, max=342872, per=100.00%, avg=320099.95, stdev=2656.69, samples=477
iops : min=72185, max=85717, avg=80024.15, stdev=664.17, samples=477
write: IOPS=22.7k, BW=88.7MiB/s (93.0MB/s)(8031MiB/90509msec); 0 zone resets
slat (usec): min=2, max=1847, avg= 7.52, stdev=24.34
clat (usec): min=27, max=30510k, avg=1357.15, stdev=73706.63
lat (usec): min=102, max=30510k, avg=1364.67, stdev=73706.70
clat percentiles (usec):
| 1.00th=[ 314], 5.00th=[ 701], 10.00th=[ 889], 20.00th=[ 1004],
| 30.00th=[ 1057], 40.00th=[ 1090], 50.00th=[ 1123], 60.00th=[ 1172],
| 70.00th=[ 1237], 80.00th=[ 1336], 90.00th=[ 1565], 95.00th=[ 1811],
| 99.00th=[ 2311], 99.50th=[ 2606], 99.90th=[ 3523], 99.95th=[ 3884],
| 99.99th=[ 4555]
bw ( KiB/s): min=122900, max=148203, per=100.00%, avg=137196.89, stdev=1203.51, samples=477
iops : min=30723, max=37052, avg=34298.35, stdev=300.89, samples=477
lat (usec) : 20=0.01%, 50=0.01%, 100=0.02%, 250=0.63%, 500=2.25%
lat (usec) : 750=4.03%, 1000=22.24%
lat (msec) : 2=69.26%, 4=1.57%, 10=0.02%, 20=0.01%, >=2000=0.01%
cpu : usr=0.44%, sys=14.97%, ctx=311701, majf=0, minf=146
IO depths : 1=0.0%, 2=0.0%, 4=0.0%, 8=0.0%, 16=0.0%, 32=100.0%, >=64=0.0%
submit : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
complete : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.1%, 64=0.0%, >=64=0.0%
issued rwts: total=4796818,2055996,0,0 short=0,0,0,0 dropped=0,0,0,0
latency : target=0, window=0, percentile=100.00%, depth=32

Run status group 0 (all jobs):
READ: bw=207MiB/s (217MB/s), 207MiB/s-207MiB/s (217MB/s-217MB/s), io=18.3GiB (19.6GB), run=90509-90509msec
WRITE: bw=88.7MiB/s (93.0MB/s), 88.7MiB/s-88.7MiB/s (93.0MB/s-93.0MB/s), io=8031MiB (8422MB), run=90509-90509msec

Disk stats (read/write):
dm-0: ios=5587061/2395682, sectors=44696488/19165501, merge=0/0, ticks=5444516/2336731, in_queue=7781247, util=100.00%, aggrios=5587061/2395679, aggsectors=44696488/19165509, aggrmerge=0/4, aggrticks=5123495/2229020, aggrin_queue=7352515, aggrutil=76.75%
nvme0n1: ios=5587061/2395679, sectors=44696488/19165509, merge=0/4, ticks=5123495/2229020, in_queue=7352515, util=76.75%

这个测试模拟的是 4 KiB 随机混合读写,读请求比例为 70%,写请求比例为 30%。

它适合用于观察文件系统和存储链路在该类负载下的表现,但不代表所有数据库的真实工作负载。

参数解释如下。

参数含义
--directory测试文件所在目录
--filename测试文件名
--size=1G每个 job 的文件大小
--time_based按指定时间持续测试
--runtime=60测试运行时间为 60 秒
--ramp_time=10先预热 10 秒,再统计
--ioengine=libaio使用 Linux 原生异步 I/O 接口
--direct=1使用直接 I/O,绕过普通页缓存数据路径
--bs=4k每次 I/O 的块大小为 4 KiB
--rw=randrw随机混合读写
--rwmixread=70读操作比例为 70%
--numjobs=4创建 4 个并发 job
--iodepth=32每个 job 请求队列深度目标为 32
--group_reporting汇总多个 job 的统计结果

需要注意,--direct=1 并不意味着绕过文件系统,也不意味着测出来的就是裸设备的绝对性能。它主要避免普通页缓存对数据读写结果的影响,文件系统元数据处理、设备缓存、虚拟化开销等仍然可能影响结果。

另外,这个命令可能会创建并写入测试文件。如果同名文件已经存在,fio 可能覆盖或改变其内容,所以一定要使用独立的测试目录和测试文件。

重点看哪些指标?

fio 输出中,建议重点关注:

IOPS:每秒完成的 I/O 操作数。

在相同块大小和相同工作负载下,IOPS 越高通常意味着处理能力越强。

BW:带宽。

表示每秒传输的数据量。大块顺序读写通常更关注带宽,而小块随机读写往往更关注 IOPS 和延迟。

clat:完成延迟。

它反映 I/O 请求从提交到完成的相关延迟统计。应同时关注平均值和百分位延迟,例如 p95、p99。

lat:总延迟。

fio 还会报告请求延迟的相关统计。具体含义要结合 fio 的输出字段、I/O 引擎和统计口径来理解。

不要只看平均延迟。如果 p99 延迟很高,意味着少量请求可能明显慢于大多数请求,这在数据库和交互型业务中尤其值得关注。

为什么不能只看 util?

很多人会把磁盘利用率当作性能好坏的直接判断依据。

实际上,设备利用率高并不一定意味着性能差,利用率低也不一定代表性能好。

例如,同样的业务负载下,如果 IOPS 提高、延迟下降,而设备利用率略有下降,这可能说明处理效率有所改善。但必须结合实际 I/O 数量、延迟、吞吐量和测试条件综合判断。

如果你希望对比文件系统参数的效果,应该尽量保持以下条件一致:

  • 相同的虚拟机配置和宿主机负载;
  • 相同的文件系统和测试文件大小;
  • 相同的 fio 参数;
  • 相同的测试时间和预热过程;
  • 多次重复测试,并比较中位数或稳定趋势。

特别是在 VMware 虚拟机中,宿主机的 CPU、内存、存储缓存和其他虚拟机的负载都可能影响结果。一次测试出现 5% 的差异,并不足以证明某个挂载参数确实有收益。

文件系统调优的正确顺序

讲到这里,我们可以把整个流程串起来。

flowchart TD
    A["确认业务负载"] --> B["确认文件系统类型"]
    B --> C["检查存储设备与挂载选项"]
    C --> D["建立 fio 基线"]
    D --> E["一次只修改一个参数"]
    E --> F["重复测试并比较结果"]
    F --> G{"收益是否稳定?"}
    G -- "是" --> H["记录配置并制定回滚方案"]
    G -- "否" --> I["恢复原配置,继续定位瓶颈"]

实际操作中,可以按照下面的顺序执行。

第一步:先看文件系统。

1
2
findmnt -t xfs,ext4
df -Th

确认文件系统类型、挂载点和可用空间。

第二步:检查挂载选项。

1
findmnt -no SOURCE,FSTYPE,OPTIONS /data

先弄清楚系统当前在用什么配置,不要在不了解默认值的情况下盲目增加参数。

第三步:确认底层存储能力。

1
lsblk -D

如果要使用 discard,先确认设备确实支持。

第四步:建立基准测试。

使用与业务相近的 fio 负载,记录 IOPS、带宽和延迟。

第五步:一次只改一个参数。

例如先验证 noatime 是否有收益,再单独测试其他参数。不要同时修改五六项配置,否则即使结果变好了,也很难确定到底是哪项修改起了作用。

第六步:记录结果并准备回滚。

保存测试命令、原始配置、修改后的配置和测试结果。只有收益稳定、没有引入额外风险,才值得将配置推广到生产环境。

最后总结

Linux 文件系统调优并不是把所有看起来能提升性能的参数都打开,而是理解文件系统如何管理数据、元数据和空间,然后针对具体负载验证配置。

对于大多数 RHEL 服务器,可以先记住这几条:

  • 没有明确理由时,优先使用发行版默认的 XFS。
  • 需要缩容、已有成熟 ext4 规范等场景,可以考虑 ext4。
  • SSD 和精简置备存储需要关注 discard,但必须确认整条存储链路支持。
  • noatime、logbsize 等参数并不是通用的性能加速开关。
  • 格式化参数会影响文件系统布局,不要在生产磁盘上随意试验。
  • 用 fio 做可重复的基准测试,结合 IOPS、带宽和延迟判断是否真的改善。

上一篇我们聊的是 I/O 调度器,这一篇继续往上走到了文件系统。

把设备、I/O 调度器、文件系统和应用负载放在一起分析,才算真正建立起 Linux 存储性能调优的思路。

下一次遇到磁盘慢的问题,不要急着改参数。先找到瓶颈,再提出假设,最后用数据验证。这才是生产环境中最可靠的调优方法,好了,我们一起来总结一下IO方面的排查思路。

flowchart TD
    A["业务出现 I/O 延迟"] --> B["检查 CPU、内存、I/O 等待"]
    B --> C["iostat -xz 观察设备指标"]
    C --> D["确认设备与文件系统映射"]
    D --> E["检查应用 I/O 模式"]
    E --> F{"主要瓶颈在哪里?"}
    F -->|进程或应用| G["检查进程、同步写入与请求模式"]
    F -->|块设备队列| H["检查调度器与队列参数"]
    F -->|文件系统| I["检查元数据、挂载参数与空间"]
    F -->|底层存储| J["检查设备延迟、宿主机与存储负载"]
    H --> K["建立基准测试"]
    I --> K
    J --> K
    G --> K
    K --> L["一次修改一个参数"]
    L --> M["重复测试并比较延迟、IOPS、带宽"]
    M --> N{"收益稳定且风险可控?"}
    N -->|是| O["记录配置并逐步应用"]
    N -->|否| P["回滚并继续定位"]
    P --> K