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

联系方式:

1. 微信:Lxh_Chat

2. 邮箱:939958092@qq.com

上一篇我们聊了 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