1. 为什么你需要fio:存储性能测试的“听诊器”

最近接手了一个新项目,公司新采购了一套SAN存储,准备用来跑核心数据库。设备上架、连线、初始化都搞定了,领导走过来问:“这新存储性能怎么样?能扛得住咱们的业务高峰吗?” 我当时心里就咯噔一下。说“应该没问题”太敷衍,拍胸脯保证又怕打脸。这时候,你就需要一个像“听诊器”一样的工具,能实实在在地“听”出存储系统的“心跳”和“脉搏”,给出有数据支撑的答案。这个工具,就是 fio。

你可能听说过一些硬盘测速命令,比如 dd 或者 hdparm。用 dd 写个文件读个文件,也能看个大概速度。但我得说,那就像用体温计量汽车发动机温度——完全不专业,也测不准。dd 通常是单线程、顺序的大块IO,而且很容易被操作系统缓存干扰,测出来的数字经常虚高,跟真实的企业级混合负载场景相差甚远。

fio(Flexible I/O Tester)就不同了。它是专门为模拟真实世界I/O负载而生的神器。你可以用它轻松制造出各种“压力场景”:比如模拟数据库的随机小块读写(OLTP),或者备份程序的大块顺序读写(OLAP)。你可以控制读写比例、队列深度、线程数量、数据块大小,甚至测试时长。测出来的指标,像 IOPS(每秒读写次数)、带宽(吞吐量)、延迟(响应时间),都是评估存储性能的黄金标准。

我自己在评估SSD、机械盘阵列、甚至云硬盘的时候,fio都是第一步。它能帮你快速暴露存储的瓶颈:是延迟太高,还是带宽不足?是随机读写拉胯,还是顺序性能不行?有了这些数据,你才能有理有据地写评估报告,做容量规划,或者跟供应商“友好交流”。接下来,我就手把手带你从零开始,玩转fio,完成一次专业的存储性能体检。

2. 轻松搞定fio安装:两种方法任你选

工欲善其事,必先利其器。安装fio很简单,主要有两种方式:通过系统包管理器安装和从源码编译安装。我建议新手直接用第一种,省时省力;如果你需要最新的特性或者特定的版本,再考虑第二种。

2.1 包管理器安装:最快上手

对于大多数主流Linux发行版,用自带的包管理器安装是最快的。这能确保安装的版本和系统兼容,并且自动处理好依赖关系。

  • 在CentOS/RHEL/Fedora系统上,使用 yum 或 dnf:

    # CentOS 7 或 RHEL 7
    sudo yum install -y fio
    # CentOS 8/RHEL 8 或 Fedora
    sudo dnf install -y fio
    
  • 在Ubuntu/Debian系统上,使用 apt:

    sudo apt update
    sudo apt install -y fio
    

安装完成后,立刻验证一下:

fio --version

如果输出了类似 fio-3.xx 的版本信息,恭喜你,安装成功了!整个过程可能一分钟都不要,非常适合快速开始测试。

2.2 源码编译安装:获取最新能力

有时候,系统仓库里的fio版本比较老,而新版本可能修复了一些bug或增加了对新IO引擎的支持。这时从源码编译安装就是更好的选择。别怕编译,步骤很清晰。

首先,我们需要安装编译依赖。不同的系统命令稍有不同:

# 对于基于RPM的系统(CentOS/RHEL/Fedora)
sudo yum groupinstall -y "Development Tools"
sudo yum install -y libaio-devel

# 对于基于Debian的系统(Ubuntu/Debian)
sudo apt update
sudo apt install -y build-essential libaio-dev

接下来,去fio的官方GitHub仓库下载最新稳定版的源码包。你可以用 wget 直接拉取。这里以3.35版本为例(请检查官网是否有更新):

wget https://github.com/axboe/fio/archive/refs/tags/fio-3.35.tar.gz

下载完成后,解压并进入目录:

tar -zxvf fio-3.35.tar.gz
cd fio-fio-3.35

经典的编译安装三步曲:

./configure
make
sudo make install

./configure 会检查你的系统环境并生成编译配置。make 是编译过程,可能会花一两分钟。sudo make install 会把编译好的 fio 程序安装到系统的标准路径(通常是 /usr/local/bin/)。

安装后,再次用 fio --version 确认版本。如果遇到命令找不到的问题,可能是因为 /usr/local/bin/ 不在你的 PATH 环境变量里,可以退出终端重新登录,或者直接使用 /usr/local/bin/fio 来运行。

2.3 验证安装:跑一个“Hello World”测试

安装好了,总得试试能不能用。我们来跑一个最简单的测试,验证fio的基本功能。这个测试会创建一个1GB的临时文件,用4个线程进行顺序读,跑10秒钟。

fio --filename=/tmp/test_fio --direct=1 --rw=read --bs=1m --size=1G --numjobs=4 --runtime=10 --group_reporting --name=verify_test

我来简单解释下这条命令在干什么:--filename 指定测试用的文件(这里是个临时文件);--direct=1 非常重要,它表示绕过系统缓存,直接对磁盘操作,这样测出来的数据才是真实的磁盘性能;--rw=read 就是顺序读;--bs=1m 是每次I/O操作的数据块大小为1MB;--size=1G 是测试文件总大小;--numjobs=4 开了4个并发线程;--runtime=10 只跑10秒;--group_reporting 让结果汇总显示,更清晰;--name 给这个测试任务起个名。

运行后,你会看到屏幕上刷出一堆信息,最后会有类似这样的汇总:

Run status group 0 (all jobs):
   READ: bw=215MiB/s (225MB/s), 215MiB/s-215MiB/s (225MB/s-225MB/s), io=2147MiB (2251MB), run=10001-10001msec

看到 bw= 后面跟着一个速度,并且没有报错,就说明你的fio安装完全正确,可以开始正式的性能探险了。这个速度只是你本地磁盘的速度,不用太在意数值。

3. 吃透fio核心参数:像搭积木一样设计测试

fio的强大,在于它极其灵活的参数体系。刚看到那一长串参数可能会头晕,别担心,我们可以把它们分成几类来理解:测试目标、负载模式、并发控制、数据设定和引擎选项。一旦掌握,你就能像搭积木一样,组合出任何你想要的测试场景。

3.1 测试目标与负载模式:你想测什么?

这是最核心的部分,决定了测试的性质。

  • --filename=/dev/sdb1 或 --filename=/mnt/testfile:指定测试对象。这里有个关键选择:如果你想测试裸设备(比如还没格式化的磁盘或LUN)的极限性能,就用设备路径,如 /dev/sdb。如果你想测试文件系统的性能(这才是大多数应用的真实场景),就应该指定一个在文件系统上的文件路径,比如 /mnt/san_volume/test.dat。警告:如果对已有数据的设备做写测试,一定要小心,数据会被覆盖!
  • --rw=:定义读写模式。这是创造负载的“剧本”。
    • read / write:纯粹的顺序读/写。模拟大文件连续传输,比如视频流、备份。
    • randread / randwrite:纯粹的随机读/写。模拟数据库、虚拟机的磁盘操作,这是对存储延迟的终极考验。
    • rw / randrw:混合读写。更贴近真实应用。对于混合模式,你需要用 --rwmixread 或 --rwmixwrite 来指定读写比例,例如 --rw=randrw --rwmixread=70 表示70%随机读,30%随机写。
  • --bs=4k 或 --bs=1m:块大小。这个参数影响巨大。小块(如4k, 8k) 主要考验存储的 IOPS 和延迟,适合数据库类应用。大块(如128k, 1m) 主要考验带宽,适合流媒体、大数据分析。测试时,两者都要覆盖。

3.2 并发与队列:制造压力的关键

单线程的测试就像一个人搬砖,测不出仓库的吞吐能力。我们需要多人并发。

  • --numjobs=16:并发线程(或进程)数。可以理解为同时有多少个“工人”在发起I/O请求。增加线程数通常能提升总吞吐量,直到达到存储系统的瓶颈。
  • --iodepth=32:I/O队列深度。这是每个“工人”手上能同时持有的“待办任务”数量。即使只有一个线程,如果队列深度很大,它也能一次性向磁盘提交很多个I/O请求,这对SSD和高端存储阵列尤其重要,能充分压榨其并发处理能力。numjobs 和 iodepth 是相乘的关系,总并发I/O数 = numjobs * iodepth。这是制造压力的核心杠杆。
  • --ioengine=libaio:I/O引擎。我强烈推荐使用 libaio(Linux原生异步I/O引擎)。它允许使用上述的 iodepth 来发起异步I/O,效率最高。如果系统不支持,可以回退到 sync(同步)或 psync(预读同步)。使用 libaio 通常需要安装 libaio-devel 开发包。

3.3 数据与时间控制:让测试更科学

测试不能无休无止,数据量也要合理。

  • --size=10G:测试数据总量。对于随机测试,这个数据量最好远大于你的系统内存(比如内存的2-3倍),以防止操作系统缓存全部数据,导致测试结果虚高。对于顺序测试,可以设大一些。
  • --runtime=300:测试运行时间(秒)。设置一个固定时间(如300秒即5分钟)比单纯跑完 size 更常用,因为它能保证测试时长一致,便于对比。fio会在时间到达后优雅地停止所有线程。
  • --direct=1:务必加上! 这意味着使用直接I/O(O_DIRECT),绕过操作系统的页面缓存(Page Cache)。这是获得真实磁盘性能的生命线。如果不加,数据可能只在内存里倒腾,测出的速度能飙到每秒几个GB,但那完全是假象。
  • --group_reporting:输出汇总报告。当你有多个 numjobs 时,这个选项会把所有线程的结果汇总成一个简洁的报告,看起来非常方便。

3.4 一个参数组合的实战案例

假设我们要模拟一个数据库的典型压力:随机、小块、高并发、混合读写。我们可以这样设计命令:

fio --filename=/mnt/san_volume/test.db \
    --direct=1 \
    --rw=randrw \
    --rwmixread=70 \
    --bs=4k \
    --iodepth=32 \
    --numjobs=8 \
    --size=50G \
    --runtime=600 \
    --ioengine=libaio \
    --group_reporting \
    --name=database_simulate

这条命令的意思是:在 /mnt/san_volume/test.db 文件上,进行10分钟(600秒)的测试。使用4KB小块,70%随机读+30%随机写。总共8个线程,每个线程的队列深度是32,所以系统同时面临着多达256个未完成的I/O请求。数据总量50G,确保超过内存。使用高效的libaio引擎,最后输出汇总报告。

你看,理解了每个参数的意义,组合起来就是一份完整的测试方案。你可以把这个命令保存成一个脚本,方便以后重复使用或修改。

4. 设计你的专业测试场景:从单一到混合

知道了参数怎么用,我们就可以像厨师设计菜单一样,设计一套完整的性能测试场景了。一个全面的评估,不能只测一种模式。我通常会把测试分成几个阶段,从简单到复杂,逐步给存储系统加压,观察其在不同负载下的表现。

4.1 基准测试:四大经典场景

首先,我们进行四大经典的单模式测试,建立性能基线。这就像体检里的身高、体重、血压,是最基础的指标。

场景一:峰值顺序读写带宽(吞吐量)测试 这个测试关心存储的“最大运力”,比如拷贝大文件能有多快。

# 顺序读(大块,高队列深度,测最大读取带宽)
fio --filename=/dev/sdb1 --direct=1 --rw=read --bs=1m --iodepth=32 --numjobs=4 --runtime=120 --ioengine=libaio --group_reporting --name=seq_read_max

# 顺序写(大块,高队列深度,测最大写入带宽)
fio --filename=/dev/sdb1 --direct=1 --rw=write --bs=1m --iodepth=32 --numjobs=4 --runtime=120 --ioengine=libaio --group_reporting --name=seq_write_max

这里用 1m 的大块和较高的队列深度,是为了让磁盘磁头或SSD通道能持续满负荷工作,测出理论带宽上限。

场景二:峰值随机读写IOPS测试 这个测试关心存储的“处理速度”,比如数据库每秒能处理多少笔交易。

# 随机读(小块,高队列深度,测最大读取IOPS)
fio --filename=/dev/sdb1 --direct=1 --rw=randread --bs=4k --iodepth=128 --numjobs=16 --runtime=120 --ioengine=libaio --group_reporting --name=rand_read_iops

# 随机写(小块,高队列深度,测最大写入IOPS)
fio --filename=/dev/sdb1 --direct=1 --rw=randwrite --bs=4k --iodepth=128 --numjobs=16 --runtime=120 --ioengine=libaio --group_reporting --name=rand_write_iops

这里用 4k 小块,并把 iodepth 和 numjobs 调得很高,是为了产生海量的小I/O请求,压出存储系统每秒能处理的最大请求数(IOPS)。对于SSD,这个数字可能非常惊人。

4.2 进阶测试:混合负载与延迟敏感型

真实的业务很少是100%读或100%写,也很少是纯顺序或纯随机。所以我们需要更复杂的混合场景。

场景三:数据库OLTP模拟(随机混合读写) 典型的在线交易处理系统,如MySQL、PostgreSQL,负载是随机小块,且读写混合。

fio --filename=/mnt/db_volume/test.ibd \
    --direct=1 \
    --rw=randrw \
    --rwmixread=70 \
    --bs=8k \
    --iodepth=16 \
    --numjobs=8 \
    --size=100G \
    --runtime=300 \
    --ioengine=libaio \
    --group_reporting \
    --name=oltp_simulate_70read_8k

这里我用了 8k 块(InnoDB页的典型大小),70/30的读写比,队列深度和线程数也设置得比较适中,模拟一个中等压力的数据库。

场景四:低延迟敏感型测试 对于一些对延迟极其敏感的应用(如高频交易、实时日志),我们不仅关心IOPS,更关心每个请求的响应时间。这时需要降低队列深度,甚至降到1。

fio --filename=/dev/nvme0n1 \
    --direct=1 \
    --rw=randread \
    --bs=4k \
    --iodepth=1 \
    --numjobs=1 \
    --runtime=60 \
    --ioengine=libaio \
    --group_reporting \
    --output=latency_test.log \
    --name=low_latency_read

iodepth=1 意味着没有队列,每个I/O请求都是发出去后等完成再发下一个,这样测出的延迟是最纯净的“响应时间”。这个数值对于评估SSD的质量尤其关键,好的NVMe SSD延迟可以低到几十微秒。

4.3 使用配置文件:让测试更规范

当测试场景变多,每次在命令行里敲一长串参数很容易出错。fio支持使用配置文件(.fio 文件),把参数写进去,管理起来方便又清晰。

创建一个文件,比如 seq_read.fio:

[global]
ioengine=libaio
direct=1
thread=1
group_reporting=1
filename=/mnt/test_volume/fio_test
size=100G
runtime=120

[sequential-read]
rw=read
bs=1m
iodepth=32
numjobs=4

然后运行测试只需:

fio seq_read.fio

你可以为每个测试场景创建一个配置文件,组成一个测试套件。还可以在配置文件中使用 [global] 段定义公共参数,在后面的任务段(如 [sequential-read])里覆盖或添加特定参数,非常灵活。这是我管理长期性能监控和回归测试的推荐方式。

5. 读懂fio输出报告:从数据到洞察

测试跑完了,屏幕上刷出一大堆数字,哪些才是关键?别被吓到,我们只需要关注几个核心部分,就能把性能“画像”勾勒出来。

5.1 核心性能指标解读

fio的输出报告虽然详细,但最需要你盯住的是汇总行(Run status group)和延迟分布(clat percentiles)。

首先看汇总行,它给出了整个测试的“成绩单”:

Run status group 0 (all jobs):
   READ: bw=215MiB/s (225MB/s), io=25.2GiB (27.0GB), run=120001-120001msec
  • bw (Bandwidth):带宽/吞吐量。单位通常是 MiB/s (Mebibytes per second) 或 MB/s。顺序测试主要看这个,数字越高,说明传输大文件越快。
  • iops:每秒I/O操作数。在随机读写,尤其是小块(如4k)测试中,这是最重要的指标。它直接反映了存储处理小请求的速度。高IOPS对数据库、虚拟化至关重要。
  • io:测试期间总共完成了多少数据量的I/O。

然后是每个job的详细部分,这里藏着“魔鬼的细节”:

read: IOPS=64.5k, BW=252MiB/s (264MB/s)(29.5GiB/120001msec)
    slat (usec): min=10, max=352, avg=15.32, stdev= 4.72
    clat (usec): min=80, max=14328, avg=495.12, stdev=283.44
     lat (usec): min=110, max=14350, avg=510.66, stdev=284.12
    clat percentiles (usec):
     |  1.00th=[  220],  5.00th=[  276], 10.00th=[  308], 20.00th=[  356],
     | 30.00th=[  396], 40.00th=[  428], 50.00th=[  460], 60.00th=[  492],
     | 70.00th=[  524], 80.00th=[  564], 90.00th=[  628], 95.00th=[  692],
     | 99.00th=[  860], 99.50th=[  956], 99.90th=[ 1360], 99.95th=[ 1648],
     | 99.99th=[ 2640]
  • slat (Submission Latency):提交延迟,指fio把I/O请求提交给内核所花的时间。通常很小。
  • clat (Completion Latency):完成延迟,这是黄金指标! 指从I/O请求提交给内核,到内核通知fio该请求已完成所花的时间。它真实反映了存储设备本身的响应速度。
  • lat (Total Latency):总延迟,约等于 slat + clat。
  • clat percentiles (延迟百分比分布):这个比平均延迟更重要! avg=495.12 是平均延迟,但它可能被少数超慢的请求拉高。而 99.00th=[860] 意味着99%的请求都在860微秒内完成。对于追求稳定性的系统,我们更关心 P99(99分位)甚至 P99.9 的延迟。如果P99延迟很低但P99.9突然飙升,说明存储可能存在“毛刺”,在重负载下偶尔会卡顿,这对敏感业务是致命的。

5.2 系统资源与磁盘状态

报告的最后部分,反映了测试期间系统和磁盘的繁忙程度:

Disk stats (read/write):
  sdb: ios=3301234/12, merge=0/3, ticks=1572001/120, in_queue=1572121, util=99.98%
  • util (Utilization):磁盘利用率。如果这个值持续在90%以上,说明磁盘已经非常繁忙,接近瓶颈。如果此时IOPS或带宽还没达到预期,那可能就是这块盘(或这个LUN)的极限了。
  • ios:磁盘实际处理的I/O请求数。
  • in_queue`:请求在队列中等待的总时间。

5.3 输出到文件与自动化分析

让fio把结果输出到文件是个好习惯,方便事后分析。

fio --filename=/dev/sdb1 ...(其他参数)... --output=my_test_result.log

你还可以加上 --output-format=json 或 --output-format=json+,让fio以JSON格式输出结果。JSON格式非常适合用脚本(比如Python)进行自动化解析、提取关键指标(如平均IOPS、P99延迟)并生成图表或报告。

6. 构建专业评估报告与避坑指南

拿到一堆测试数据后,如何把它们变成一份有说服力的报告?同时,在测试过程中有哪些坑需要提前避开?这是我多年踩坑总结的经验。

6.1 整理你的测试结果

不要只扔给领导一堆日志文件。你需要整理成清晰的表格。我通常会用Excel或Google Sheets做这样一张汇总表:

测试场景块大小读写比例队列深度/线程数平均IOPS平均带宽 (MB/s)平均延迟 (ms)P99延迟 (ms)磁盘利用率
顺序读 (峰值)1M100%读32/4-52012.325.198%
顺序写 (峰值)1M100%写32/4-48013.528.799%
随机读 (峰值)4K100%读128/1685,0003322.45.895%
随机写 (峰值)4K100%写128/1634,0001336.015.2100%
数据库模拟8K70%读/30%写16/812,5009810.222.588%

在报告开头,写明测试环境:服务器型号、CPU、内存、操作系统、存储设备型号、连接方式(如FC, iSCSI)、文件系统及挂载参数等。然后附上测试方法简述和上面的结果汇总表。最后,给出结论与建议,比如:“该存储顺序读写带宽达到XX MB/s,4K随机读IOPS达到XX,满足数据库当前负载要求,但随机写延迟较高,建议在写入密集型场景下观察实际表现。”

6.2 实战避坑指南

这些坑我都踩过,希望你能绕过去。

  1. “幽灵性能”与缓存:--direct=1 参数必须加! 如果不加,Linux会用空闲内存缓存数据,你测的可能只是内存速度,而不是磁盘速度。这是新手最容易犯的错误。
  2. 测试目标选择错误:测试文件系统性能,却指向了裸设备(/dev/sdb)。这测的是驱动层性能,可能比实际文件系统性能高。正确的做法是在挂载好的文件系统里创建一个文件进行测试。
  3. 数据量太小:如果你的测试数据总量(size)比服务器内存还小,那么即使加了 direct=1,在第二轮测试时,数据可能已经被缓存了。确保 size 远大于内存(比如2-3倍)。
  4. 测试时间太短:存储阵列可能有缓存、分层、GC(垃圾回收)等机制。短时间测试可能只测到了缓存性能。对于稳定性评估,建议 runtime 至少设置300秒(5分钟),甚至更长,观察性能是否会出现波动或下降。
  5. 忽略延迟分布:只盯着平均IOPS和平均延迟。结果上线后,业务总在偶尔卡顿。一定要关注 P99、P99.9 延迟,它们代表了长尾延迟,直接影响用户体验。
  6. 在已使用的生产系统上测试:如果必须在生产环境测试,一定要用 --filename 指定一个新创建的、独占的大文件,并确保有足够的备份。切勿直接指向关键数据文件或设备。
  7. 多磁盘测试的干扰:如果你在测试一个RAID组或一个存储卷,确保测试时没有其他重要进程在访问同一套存储,否则结果会相互干扰。

6.3 进阶玩法:写在最后

当你熟悉了基础测试,可以探索一些更高级的玩法。比如,使用 --eta=always 让fio实时显示预估完成时间。用 --status-interval=5 每5秒输出一次中间状态,观察性能是否稳定。对于超长时间测试(比如24小时压力测试),一定要把输出重定向到文件,并考虑使用 nohup 放在后台运行。

fio还有一个强大的功能是支持多个job段在同一个配置文件中运行,这样可以模拟更复杂的多应用混合负载场景。例如,你可以同时模拟一个数据库job(随机8k读写)和一个日志服务job(顺序追加写),看它们相互干扰下的表现。

存储性能测试不是一个一次性的任务,而应该是一个持续的过程。在系统上线前、扩容后、甚至定期巡检时,跑一套标准的fio测试,建立性能基线,你就能敏锐地发现性能衰减或异常。数据不会说谎,当你拿着fio生成的坚实数据去讨论性能问题,你的话语会更有分量。希望这篇指南能帮你把fio这个利器真正用起来,不再为存储性能而焦虑。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐