简介:FIO(Flexible I/O Tester)3.8 版本完整源代码压缩包,面向存储性能测试工程师、系统运维与内核/驱动开发者,用于在 SSD、HDD、RAID、NVMe 等场景下评估读写吞吐、IOPS 与延迟,并开展压力测试与瓶颈定位。包内共 433 个文件,压缩后约 872KB;主体为 164 个 C 源文件与 148 个头文件,可直接 make 编译,另含 58 个 fio 配置、6 个 job 作业文件、5 个 Python 辅助脚本及说明文档,便于按需构造自定义负载。目前已有 1648 人学习下载。通过阅读源码与配置范例,可掌握多线程/进程并发、顺序与随机读写、队列深度及缓存策略等参数设置,学会编写 job 文件并解析性能日志;对存储选型、调优验证与故障排查均有直接的实用价值。
1. 拿到 fio-3.8.zip 之后:这不是一个普通压缩包,而是一套存储压测工具的完整源码
如果你是存储工程师或运维,看到fio-3.8.zip这个文件,大概率是想评估一块新磁盘、一台数据库主机的 IO 能力,或者给云盘做一次基线测试。fio 是世界上用得最广的存储性能压测工具,它通过模拟现实 IO 负载来测出设备的顺序带宽、随机读写 IOPS 和延迟分布,而不是简单跑一个dd if=/dev/zero bs=1M count=1000的缓存测试。这个 zip 里装的是 fio 3.8 版本的完整源码包,解压后需要自己编译,但它能跑在几乎所有 Linux 发行版上,也是后来你写自动化压测脚本的基础。这篇笔记就围绕这个具体版本,从解压编译到读懂输出,再到避坑,讲清楚一套可复现的落地路径。
2. 从 zip 包到可执行文件:解压、校验与编译的完整流程
2.1 解压前先校验:zip 包是否完整的两个命令
很多人拿到fio-3.8.zip后第一件事就是直接unzip,这是翻车的开始。网络传输过程中文件可能丢字节,尤其你是从内部文件服务器或对象存储下载的时候。一次损坏的解压会给你莫名其妙的编译错误,比如Makefile: recipe for target 'main.o' failed,但实际原因根本不是代码问题,而是源码文件不完整。
我一般会先跑一个完整性检查:
unzip -t fio-3.8.zip这个命令会逐个文件解压到内存并核对 CRC 校验值,不会落盘。输出末尾会出现类似No errors detected in compressed data of fio-3.8.zip的提示,看到这句话再解压。
unzip -q fio-3.8.zip解压后进入目录确认文件结构:
cd fio-3.8 ls -l | head -20你能看到configure、Makefile、README、fio.1这些文件。configure是一个可执行的 shell 脚本,不是著名的 autoconf 生成物,而是 fio 作者自己维护的一套配置脚本。fio.1是 man 手册页,后面查参数可以man ./fio.1。如果unzip -t有报错,说明包不完整,不要犹豫,重新下载一份。
有一点需要强调:unzip -t只验证 zip 包内部结构,验证不了源码本身的完整性。如果你有发布方提供的 SHA256 校验值,建议再跑一次sha256sum fio-3.8.zip,和你拿到的哈希比对。这不是玄学,是编译大型 C 项目前耗时间最少的保险。
2.2 依赖检查与 configure 参数:决定 fio 支持哪些 ioengine
fio 的 ioengine 是它和内核打交道的执行引擎。3.8 这个版本默认会编译一系列引擎,比如sync、libaio、posixaio。其中libaio是最常用的异步 IO 引擎,适合测高性能 NVMe 盘,但它是基于 Linux AIO 的系统调用,需要内核和用户态库支持。如果没装开发包,编译时不会立刻报错,但./configure会把 libaio 的检测结果标记为No,最后生成的 fio 就不支持ioengine=libaio,这会导致你测 NVMe 时只能用sync,性能数据完全失真。
在编译前,先检查依赖:
which gcc make pkg-config --exists libaio && echo "libaio-dev OK" || echo "libaio-dev MISSING"在 Debian/Ubuntu 系上缺失的话,装一下:
sudo apt-get install -y gcc make libaio-dev zlib1g-devzlib1g-dev是给--enable-compression预留的,如果你只做基本 IO 压测,装不装无所谓,但装上不会错。在 RHEL/CentOS 系上包名略有区别,常见是libaio-devel。
接下来执行配置:
./configure --prefix=/usr/local/fio-3.8--prefix指定安装前缀。我习惯把不同版本装到独立目录,比如/usr/local/fio-3.8,这样多版本共存时不会互相覆盖。configure 完成后,留意输出里的libaio一行,必须是yes。如果显示no,说明你的环境里没有检测到 libaio 头文件,需要回头装依赖,再重新运行 configure。
./configure --help可以列出所有参数。3.8 版本里有几个值得注意的选项:
--enable-libaio:强制启用 libaio,但如果你系统里真的没有 libaio 开发库,这一步会直接报错。--build-static:静态编译,把依赖都打进二进制,适合拷贝到其他同样内核的机器上跑。--disable-json:如果你不需要 JSON 输出,可以关掉,但我不建议关,后面分析结果还要靠 JSON。--with-curl:让 fio 支持 HTTP 引擎做网络压测,一般用不到,别开,否则又多一层依赖。
这些参数不是都要加,默认 configure 对一个干净的 Linux 系统已经足够友好。
2.3 编译安装与版本验证:make 命令背后的可选配置
依赖确认无误后,直接编译:
make -j$(nproc)-j$(nproc)意思是让 Make 用所有 CPU 核心并行编译,能显著缩短时间。fio 源码并不大,在一台 8 核的机器上大概一两分钟就能编完。如果你在编译过程中看到warning,忽略大部分没有关系,但如果出现了error就要停下来查原因。常见的 error 是某些旧版本 glibc 缺少__sync_synchronize等内建函数,这种一般得换到更新的发行版编译,而不是硬编。
二进制编出来之后,先不急着安装,直接运行:
./fio --version理论上应该输出fio-3.8。这里有个小技巧:如果编译时缺了某个模块,比如 libaio 被禁用,--version不会告诉你。你可以再跑一条:
./fio --enghelp | grep libaio如果输出里有libaio,说明这个引擎可用。如果没输出,说明编译配置里没有包含它,不能继续测 NVMe。
确认没问题后再安装:
make install echo 'export PATH="/usr/local/fio-3.8/bin:$PATH"' >> ~/.bashrc source ~/.bashrc fio --version安装到自定义 prefix 后,make install会把二进制放到/usr/local/fio-3.8/bin/fio,man 手册放到/usr/local/fio-3.8/man。没有 systemd 服务、没有全局配置文件,安装完直接把二进制路径加到 PATH 就行。对这种命令行工具,这就是最优解。
3. 第一次 fio 性能测试:最小 Job 文件与参数选型
3.1 用一组可控参数测出磁盘的真实 IOPS:全局段与目标段
fio 的用法不是像dd那样直接跟参数,而是通过一个 job 文件描述测试场景。你可以在命令行直接写一整条命令,但可读性很差。我一般会把测试参数写进一个文件,扩展名用.fio,然后让 fio 读取它。
先看一个测随机读的最小例子:
cat > randread.fio << 'EOF' [global] ioengine=libaio direct=1 rw=randread bs=4k runtime=30 time_based group_reporting=1 [RandomRead] size=512M iodepth=32 numjobs=1 EOF执行:
fio randread.fio理解这个文件的关键在于段的作用。[global]是全局段,它下面写的参数会作用到后面所有的 job 段。[RandomRead]是第一个 job 段,定义了这个测试的具体 IO 负载。
参数说明:
ioengine=libaio让 fio 通过异步 IO 系统调用下发请求,IOPS 上限远高于sync。direct=1绕过操作系统页缓存,直接读写磁盘。这是测磁盘真实性能的前提,后面避坑章节会再强调。rw=randread指定随机读模式。bs=4k每个 IO 请求的块大小。4K 是数据库随机读写最常见的块大小,也是很多云厂商定义 IOPS 的默认块大小。runtime=30测试运行 30 秒。配合time_based,强制 30 秒跑满,即使size指定的数据量已经写完。group_reporting=1把多个 job 的结果聚合成一组报告,避免多个 job 段各自输出导致难以阅读。iodepth=32每个 job 同时在队列里等待完成的请求数。这个值越高,越能压满磁盘队列。对 NVMe 盘,32 到 64 是常用起点。numjobs=1只启动一个进程。实际压测可以提高到 4 或 8,但第一次跑建议先 1 个,把基线摸清楚。
这里选择size=512M而不是size=1G,是因为随机读不需要覆盖整个盘,512M 足够让 SSD 上的映射表产生随机寻址压力,又不会因为容量太大在机械盘上消耗过长时间。测试大小对性能结果有影响,大 size 会让 SSD 进入稳态,小 size 更接近空盘峰值。根据你的目的调整:测新品上限用小 size,测长期运行后的衰减要跑大 size 甚至全盘。
3.2 顺序读、随机写、混合读写:三种典型 Workload 的 Job 写法
生产环境最常见的场景是顺序读、随机写和混合读写。我在实际压测中,会把这三个场景分别写成三个 job 文件,而不是在同一个文件里用多个 job 段去轮换,因为轮换会互相干扰,结果样本不干净。
顺序读测带宽:
cat > seqread.fio << 'EOF' [global] ioengine=libaio direct=1 rw=read bs=1M runtime=30 time_based group_reporting=1 [SeqRead] size=2G iodepth=16 numjobs=1 EOFrw=read是纯顺序读。bs=1M用大块,因为顺序读的指标是带宽,不是 IOPS。2G 大小能保证顺序读完一个连续区间,不会因为盘太小触底反弹。
随机写测稳态:
cat > randwrite.fio << 'EOF' [global] ioengine=libaio direct=1 rw=randwrite bs=4k runtime=30 time_based group_reporting=1 [RandWrite] size=1G iodepth=64 numjobs=4 EOF这里numjobs=4同时跑 4 个写线程,iodepth=64,加起来队列深度是 256,能更好地压满 SSD 的内部并行通道。随机写最容易暴露 SSD 写放大问题,所以如果这个测试跑出来的 IOPS 远低于官方标称值,先不要下结论说盘有问题,看看是不是缓存策略或者固件垃圾回收在影响。
混合读写最接近数据库或者文件服务器压力:
cat > rwmix.fio << 'EOF' [global] ioengine=libaio direct=1 rw=randrw rwmixread=70 bs=8k runtime=30 time_based group_reporting=1 [MixWorkload] size=1G iodepth=32 numjobs=2 EOFrw=randrw表示随机混合读写,rwmixread=70让读占 70%、写占 30%。bs=8k是很多 OLTP 场景的典型块大小。混合读写的结果要留意读和写各自的带宽,不能只看聚合数值。
写这几个文件时,一个容易忽略的点是size对随机写的影响。如果size=1G,fio 只会在这 1G 范围内随机寻址,性能会明显好于全盘随机写。如果你要模拟已经使用过半的磁盘,用size指定那个区域,或者用filename直接指定一个/dev分区。
3.3 跑稳定 30 秒还是跑满 1GiB:runtime 与 size 的取舍
runtime和size都控制测试时长,但语义完全不同。很多人两个都用,却不知道谁优先。fio 判断测试结束的规则是:如果指定了time_based,则runtime优先,即使 size 对应的工作量已经提前完成,fio 会重复跑这个 IO 模式直到 runtime 时间耗尽。如果没有time_based,则先完成size对应的工作量就结束,即使用了runtime也不生效。
什么时候用runtime?我一般做对比测试都用runtime=60这样的固定时长,确保每次跑盘的时间一致,这样多次测试的 IOPS 平均值有可比性。SSD 预热比较快,30 秒足够稳定,但为了排除开头的一两次 GC 波动,我习惯用 60 秒。
什么时候用size?当你需要测试一个特定容量范围的性能特征时,比如验证一个 4TB 盘在写入前 200GB 后性能衰减多少。这时候不设runtime,直接用size=200G加rw=write,fio 会连续写满 200G,模拟真实业务写入量。
两个参数混合使用时有一个坑:如果你同时写了size=100G和runtime=60,并且没有time_based,fio 会优先执行完 100G 的写操作,而这个操作可能远超 60 秒。反过来,加了time_based后,size=100G基本只剩一个逻辑限制,根本不会跑完 100G,实际只跑runtime。所以,先确定你测的是时长还是业务量,再决定是否加time_based。这一点是 fio 新用户最常踩的坑之一。
4. 读懂 fio 输出的三个关键指标:带宽、IOPS 与延迟分布
4.1 输出文件的整体结构:从起始到结果统计
fio 跑完会输出一大段文本,除了开头的一堆参数回显,真正值得读的是结尾的统计部分。不要滑屏滑过去了。先看一个经典输出片段:
read: IOPS=3170, BW=12.4MiB/s (13.0MB/s)(372MiB/30001msec) lat (usec): min=148, max=28149, avg=378.14, stdev=563.21 clat percentiles (usec): | 1.00th=[ 161], 5.00th=[ 182], 10.00th=[ 192], | 20.00th=[ 211], 50.00th=[ 207], 80.00th=[ 701], | 90.00th=[ 948], 95.00th=[ 1193], 99.00th=[ 1917], | 99.90th=[ 4519], 99.99th=[ 9868]这个片段里第一行read:是 IO 模式下的输出,紧随其后的是write:部分,最后还有lat、clat两行。lat是总延迟,包含clat和系统提交时间;clat是完成延迟,即发起 IO 请求到收到完成通知的时间,是判断设备真实响应速度最关键的指标。
输出文件的整体顺序是:全局参数回显 → 每个 job 的状态信息 → 最终聚合结果。如果你用了group_reporting=1,最终结果会把所有 job 的统计聚合在一起,没有每个 job 的独立报告。多 job 场景下不要去掉这个参数,否则你要手动把几十个 job 的 IOPS 加起来,很容易算错。
4.2 关键指标解读:bw、iops、clat 百分位
带宽写在每行 IO 统计的前半段。比如BW=12.4MiB/s (13.0MB/s),前面是二进制带宽,后面是十进制带宽。如果你测的是顺序读,这里就是连续读取吞吐。测随机读写时,带宽意义没有 IOPS 大,IOPS 会更直观。
IOPS 是每秒完成的 IO 请求数。read: IOPS=3170表示这个 job 的随机读平均每秒 3170 次。注意这个数字是平均数,fio 在最后还会给出iops百分位吗?不会。它只给平均值。所以如果你发现平均值异常,需要用--output-format=json配合工具去抓取更细粒度的采样,这会在后面讲。
延迟分布是最容易看出问题的部分。clat percentiles里的usec表示单位是微秒。50.00th=[207]表示 50% 的请求延迟在 207 微秒以内;99.99th=[9868]表示 99.99% 请求延迟在 9868 微秒以内,也就是接近 10 毫秒。看这组数据时我有个习惯:先看 99 百分位和 99.9 百分位,如果 99 百分位远大于平均延迟,说明有长尾延迟出现,这对数据库业务是致命的。
举例,平均延迟 378 微秒看起来不错,但 99.99 百分位接近 10ms,说明偶尔有请求被卡住 10ms。这通常是磁盘内部垃圾回收或者是 NUMA 调度抖动造成的。你如果只盯平均延迟,这个隐患就漏掉了。
4.3 用 fio 3.8 导出 JSON 结果:方便脚本化的参数
fio 3.8 原生支持 JSON 输出,这对后续自动化分析很重要。所有测试完成后,加一个参数就能拿到结构化数据:
fio randread.fio --output-format=json --output=randread.json生成的 JSON 文件里,先找jobs数组,每个 job 元素下有read对象,包含iops、bw_bytes、lat_ns、clat_ns等字段。与直接在终端看文本不同,JSON 里的延迟单位默认是纳秒,除 1000 转为微秒。
这里要提醒一下,fio 3.8 的 JSON 结构和最新版有差异。比如 3.8 里bw字段是bw_bytes,到了 3.19 以后还可能多出bw嵌套对象。如果你有基于 JSON 的解析脚本,升级 fio 版本后一定要重新验证字段名,不要想当然。文本输出里如果看到fio-3.8版本号,用 JSON 就能直接匹配文档上的字段设计,不用猜。
说到 JSON 输出,还有一个场景:批量跑测试时,把所有结果汇总到一个文件里,用jq提取需要的字段。
jq '.jobs[0].read.iops' randread.json这个命令会把第一个 job 的读 IOPS 打印出来。如果有多个 job,通过jq '.jobs | map(.read.iops)'拿到所有值来计算总 IOPS。不要手动去数文本输出,当你有 8 个 job 同时跑,手动求和既慢又容易出错。
5. 五条 fio 3.8 的常见问题排查与避坑记录
5.1 编译时报错找不到 libaio:现象、原因与解决步骤
现象:运行./configure时看到输出里libaio一行是no,编译的时候也没有报错,后来执行 fio 用ioengine=libaio时提示 unavailable。
原因:fio 的 configure 在初始化时通过pkg-config探测 libaio,如果没找到,就自动降级用sync或posixaio。它在此时不给你 hard fail,而是做成一个可选引擎。
解决:安装缺失的开发包,然后重新 configure 并 make clean。Debian 系执行sudo apt-get install libaio-dev,RHEL 系执行sudo yum install libaio-devel。装完后不要直接 make,先make clean再./configure,确保重新探测。如果还不行,确认pkg-config --libs libaio能输出路径,不行就手工安装,把/usr/include/libaio.h放到编译器的头文件路径下。这个问题隐蔽,因为编译成功不代表引擎齐全,只有实际运行时才知道。
5.2 IOPS 结果忽高忽低,被缓存干扰:direct=1 为何不能省
现象:同一块 SSD 连续跑三次随机读,第一次 IOPS 十几万,第二次跌到三万,第三次又回到十几万。
原因:没有设置direct=1时,fio 默认走操作系统页缓存。第一次读盘时数据被缓存,第二次读的是缓存,所以 IOPS 虚高;第三次缓存失效又回落。磁盘本身性能没变,是测试逻辑把缓存和盘混在一起了。
解决:在 job 文件的全局段强制写direct=1。这个参数让 fio 绕过页缓存,直接和磁盘块设备交互。如果网络存储设备需要走缓存,另当别论,但本地盘压测没有理由不加。加了direct=1后,如果 IOPS 依然忽高忽低,再看系统是否有其他进程在同时写盘,用iostat -x 1确认磁盘利用率。
5.3 压测跑了一会儿性能掉崖:SSD 写放大与垃圾回收的影响
现象:随机写测试刚开始 IOPS 很高,运行到十几秒后突然跌到原来的三分之一,再过一会儿又恢复一部分,但恢复不到峰值。
原因:SSD 内部有一个垃圾回收机制,当空闲块不足时,它会在写入路径上做搬移和擦除,产生写放大。fio 持续写满测试区域后,垃圾回收介入,性能自然下跌。这不是 fio 的问题,而是闪存设备的固有行为。机械盘没有这种掉落,因为它的寻道时间固定,不会等到垃圾回收。
解决:用fstrim提前清理测试分区,或者用size参数控制测试范围,确保不要写入到接近分区边界的位置。如果要测真实稳态性能,就接受性能下跌,并且把 runtime 延长到 5 分钟以上,记录最终稳定的 IOPS,而不是只记录开头 30 秒。写压测时还要注意温度,现代 5.0 固态长时间写高温掉速,测试结果里要记录环境温度,否则第二次复现数据对不上。
5.4 测试结束后文件残留占满磁盘:如何清理与避免
现象:跑完 fio 后,df -h发现有大量文件创建在你指定的测试目录里,有些人用默认 filename 导致直接写在当前目录。
原因:你没在 job 里指定filename,fio 默认会在当前路径下创建randomread.0.0这种文件。如果size=20G,它就会生成一个 20G 的文件。测试完成后这些文件不会自动删除,除非你加了unlink=1。
解决:添加unlink=1参数,让 fio 在测试结束后删除测试文件。这会损失文件系统缓存的完整性,但一般不影响纯性能测试。更好的做法是指定测试目录,比如/data/fio-test/,并在测试脚本开头mkdir -p,结束后用rm -f清理。最佳实践是 job 文件里同时写directory=/data/fio-test和unlink=1。千万不要在/根目录下直接跑 fio,否则文件残留在系统分区,很快挤出可用空间。
5.5 多线程压测触发系统参数瓶颈:文件数限制与队列长度
现象:使用numjobs=16跑压测时,fio 启动报错io_setup failed或cannot allocate memory,或者打开文件时报too many open files。
原因:iodepth=32乘以 16 个 job 之后,需要的内核 AIO 上下文数量很庞大。内核默认每个用户能申请的 AIO 事件数有限制,同时系统 open files 上限也限制每个 job 打开的测试文件句柄。
解决:先用ulimit -n查看文件描述符上限,如果低于 65536,用ulimit -n 65536临时提高。AIO 限制可以用fs.aio-max-nr调整,需要 root 权限执行:
sysctl -w fs.aio-max-nr=1048576这个命令把内核允许的 AIO 请求总数提升到 100 万。改完后再跑,就不会出现 io_setup 失败。这里有个教训:压测系统前先检查系统限制,不要让压测脚本变成压测环境的元凶。很多线上系统被 fio 压翻车,原因是 fio 没有遵守生产环境已有的 IO 调度和资源配额。
6. 把 fio 3.8 用得更顺手:压测脚本的模板化与结果自动解析技巧
当你需要反复对比多块磁盘或者多个版本的固件时,一条条手输 fio 命令就很低效。我会用 bash 脚本定义一套可复用的 job 模板,把变量通过环境变量传入 fio 的 job 文件。fio 支持 job 文件里的变量替换,比如${BS},执行时传入环境变量。
BS=4k RWMIXREAD=70 fio - <<'EOF' [global] ioengine=libaio direct=1 rw=randrw rwmixread=${RWMIXREAD} bs=${BS} runtime=60 time_based group_reporting=1 [Mixed] size=1G iodepth=32 numjobs=2 EOF这段脚本利用 here-document 把 job 内容传给 fio 的标准输入,同时通过环境变量BS和RWMIXREAD控制块大小和读写比例。这样你可以在外层循环里跑一组参数,比如for bs in 4k 8k 16k; do ... done,自动生成多组结果。
结果解析我用 JSON 加上一个小循环:
for r in result_*.json; do echo -n "$r " jq -r '[.jobs[0].read.iops, .jobs[0].write.iops] | @tsv' "$r" done每行输出一个历史 JSON 文件的名字和对应的读写 IOPS。把这些数字放到一张表里,就能直观看到块大小对 IOPS 的影响。如果你还要对比延迟,把clat_ns的均值也取出来,多个指标放一起才有意义。
fio 3.8 有一个小限制,它的 JSON 里没有直接输出每个 job 的带宽和延迟百分位相对文本格式那么可读。我一般会在脚本里同时保留文本输出和 JSON 输出,文本给人快速看,JSON 给脚本算。
最后说一个自己的习惯:任何压测结果,我都会在最开始记录被测设备的型号、固件版本、文件系统、挂载参数。否则下一次测试,同一块盘可能因为文件系统不同结果差别很大。以前我拿到新盘就直接跑默认随机写,测出一堆数字后根本没法定位问题,后来才意识到是挂载参数没记录。这个习惯被验证了很多次,希望帮到你。
本文还有配套的精品资源,点击获取