fio-3.8完整指南:从解压编译到IOPS与延迟分析实战

发布时间:2026/10/13 5:33:30
fio-3.8完整指南:从解压编译到IOPS与延迟分析实战
简介FIOFlexible I/O Tester3.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 bs1M count1000的缓存测试。这个 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 支持哪些 ioenginefio 的 ioengine 是它和内核打交道的执行引擎。3.8 这个版本默认会编译一系列引擎比如sync、libaio、posixaio。其中libaio是最常用的异步 IO 引擎适合测高性能 NVMe 盘但它是基于 Linux AIO 的系统调用需要内核和用户态库支持。如果没装开发包编译时不会立刻报错但./configure会把 libaio 的检测结果标记为No最后生成的 fio 就不支持ioenginelibaio这会导致你测 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/fioman 手册放到/usr/local/fio-3.8/man。没有 systemd 服务、没有全局配置文件安装完直接把二进制路径加到 PATH 就行。对这种命令行工具这就是最优解。3. 第一次 fio 性能测试最小 Job 文件与参数选型3.1 用一组可控参数测出磁盘的真实 IOPS全局段与目标段fio 的用法不是像dd那样直接跟参数而是通过一个 job 文件描述测试场景。你可以在命令行直接写一整条命令但可读性很差。我一般会把测试参数写进一个文件扩展名用.fio然后让 fio 读取它。先看一个测随机读的最小例子cat randread.fio EOF [global] ioenginelibaio direct1 rwrandread bs4k runtime30 time_based group_reporting1 [RandomRead] size512M iodepth32 numjobs1 EOF执行fio randread.fio理解这个文件的关键在于段的作用。[global]是全局段它下面写的参数会作用到后面所有的 job 段。[RandomRead]是第一个 job 段定义了这个测试的具体 IO 负载。参数说明ioenginelibaio让 fio 通过异步 IO 系统调用下发请求IOPS 上限远高于sync。direct1绕过操作系统页缓存直接读写磁盘。这是测磁盘真实性能的前提后面避坑章节会再强调。rwrandread指定随机读模式。bs4k每个 IO 请求的块大小。4K 是数据库随机读写最常见的块大小也是很多云厂商定义 IOPS 的默认块大小。runtime30测试运行 30 秒。配合time_based强制 30 秒跑满即使size指定的数据量已经写完。group_reporting1把多个 job 的结果聚合成一组报告避免多个 job 段各自输出导致难以阅读。iodepth32每个 job 同时在队列里等待完成的请求数。这个值越高越能压满磁盘队列。对 NVMe 盘32 到 64 是常用起点。numjobs1只启动一个进程。实际压测可以提高到 4 或 8但第一次跑建议先 1 个把基线摸清楚。这里选择size512M而不是size1G是因为随机读不需要覆盖整个盘512M 足够让 SSD 上的映射表产生随机寻址压力又不会因为容量太大在机械盘上消耗过长时间。测试大小对性能结果有影响大 size 会让 SSD 进入稳态小 size 更接近空盘峰值。根据你的目的调整测新品上限用小 size测长期运行后的衰减要跑大 size 甚至全盘。3.2 顺序读、随机写、混合读写三种典型 Workload 的 Job 写法生产环境最常见的场景是顺序读、随机写和混合读写。我在实际压测中会把这三个场景分别写成三个 job 文件而不是在同一个文件里用多个 job 段去轮换因为轮换会互相干扰结果样本不干净。顺序读测带宽cat seqread.fio EOF [global] ioenginelibaio direct1 rwread bs1M runtime30 time_based group_reporting1 [SeqRead] size2G iodepth16 numjobs1 EOFrwread是纯顺序读。bs1M用大块因为顺序读的指标是带宽不是 IOPS。2G 大小能保证顺序读完一个连续区间不会因为盘太小触底反弹。随机写测稳态cat randwrite.fio EOF [global] ioenginelibaio direct1 rwrandwrite bs4k runtime30 time_based group_reporting1 [RandWrite] size1G iodepth64 numjobs4 EOF这里numjobs4同时跑 4 个写线程iodepth64加起来队列深度是 256能更好地压满 SSD 的内部并行通道。随机写最容易暴露 SSD 写放大问题所以如果这个测试跑出来的 IOPS 远低于官方标称值先不要下结论说盘有问题看看是不是缓存策略或者固件垃圾回收在影响。混合读写最接近数据库或者文件服务器压力cat rwmix.fio EOF [global] ioenginelibaio direct1 rwrandrw rwmixread70 bs8k runtime30 time_based group_reporting1 [MixWorkload] size1G iodepth32 numjobs2 EOFrwrandrw表示随机混合读写rwmixread70让读占 70%、写占 30%。bs8k是很多 OLTP 场景的典型块大小。混合读写的结果要留意读和写各自的带宽不能只看聚合数值。写这几个文件时一个容易忽略的点是size对随机写的影响。如果size1Gfio 只会在这 1G 范围内随机寻址性能会明显好于全盘随机写。如果你要模拟已经使用过半的磁盘用size指定那个区域或者用filename直接指定一个/dev分区。3.3 跑稳定 30 秒还是跑满 1GiBruntime 与 size 的取舍runtime和size都控制测试时长但语义完全不同。很多人两个都用却不知道谁优先。fio 判断测试结束的规则是如果指定了time_based则runtime优先即使 size 对应的工作量已经提前完成fio 会重复跑这个 IO 模式直到 runtime 时间耗尽。如果没有time_based则先完成size对应的工作量就结束即使用了runtime也不生效。什么时候用runtime我一般做对比测试都用runtime60这样的固定时长确保每次跑盘的时间一致这样多次测试的 IOPS 平均值有可比性。SSD 预热比较快30 秒足够稳定但为了排除开头的一两次 GC 波动我习惯用 60 秒。什么时候用size当你需要测试一个特定容量范围的性能特征时比如验证一个 4TB 盘在写入前 200GB 后性能衰减多少。这时候不设runtime直接用size200G加rwwritefio 会连续写满 200G模拟真实业务写入量。两个参数混合使用时有一个坑如果你同时写了size100G和runtime60并且没有time_basedfio 会优先执行完 100G 的写操作而这个操作可能远超 60 秒。反过来加了time_based后size100G基本只剩一个逻辑限制根本不会跑完 100G实际只跑runtime。所以先确定你测的是时长还是业务量再决定是否加time_based。这一点是 fio 新用户最常踩的坑之一。4. 读懂 fio 输出的三个关键指标带宽、IOPS 与延迟分布4.1 输出文件的整体结构从起始到结果统计fio 跑完会输出一大段文本除了开头的一堆参数回显真正值得读的是结尾的统计部分。不要滑屏滑过去了。先看一个经典输出片段read: IOPS3170, BW12.4MiB/s (13.0MB/s)(372MiB/30001msec) lat (usec): min148, max28149, avg378.14, stdev563.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_reporting1最终结果会把所有 job 的统计聚合在一起没有每个 job 的独立报告。多 job 场景下不要去掉这个参数否则你要手动把几十个 job 的 IOPS 加起来很容易算错。4.2 关键指标解读bw、iops、clat 百分位带宽写在每行 IO 统计的前半段。比如BW12.4MiB/s (13.0MB/s)前面是二进制带宽后面是十进制带宽。如果你测的是顺序读这里就是连续读取吞吐。测随机读写时带宽意义没有 IOPS 大IOPS 会更直观。IOPS 是每秒完成的 IO 请求数。read: IOPS3170表示这个 job 的随机读平均每秒 3170 次。注意这个数字是平均数fio 在最后还会给出iops百分位吗不会。它只给平均值。所以如果你发现平均值异常需要用--output-formatjson配合工具去抓取更细粒度的采样这会在后面讲。延迟分布是最容易看出问题的部分。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-formatjson --outputrandread.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 用ioenginelibaio时提示 unavailable。原因fio 的 configure 在初始化时通过pkg-config探测 libaio如果没找到就自动降级用sync或posixaio。它在此时不给你 hard fail而是做成一个可选引擎。解决安装缺失的开发包然后重新 configure 并 make clean。Debian 系执行sudo apt-get install libaio-devRHEL 系执行sudo yum install libaio-devel。装完后不要直接 make先make clean再./configure确保重新探测。如果还不行确认pkg-config --libs libaio能输出路径不行就手工安装把/usr/include/libaio.h放到编译器的头文件路径下。这个问题隐蔽因为编译成功不代表引擎齐全只有实际运行时才知道。5.2 IOPS 结果忽高忽低被缓存干扰direct1 为何不能省现象同一块 SSD 连续跑三次随机读第一次 IOPS 十几万第二次跌到三万第三次又回到十几万。原因没有设置direct1时fio 默认走操作系统页缓存。第一次读盘时数据被缓存第二次读的是缓存所以 IOPS 虚高第三次缓存失效又回落。磁盘本身性能没变是测试逻辑把缓存和盘混在一起了。解决在 job 文件的全局段强制写direct1。这个参数让 fio 绕过页缓存直接和磁盘块设备交互。如果网络存储设备需要走缓存另当别论但本地盘压测没有理由不加。加了direct1后如果 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 里指定filenamefio 默认会在当前路径下创建randomread.0.0这种文件。如果size20G它就会生成一个 20G 的文件。测试完成后这些文件不会自动删除除非你加了unlink1。解决添加unlink1参数让 fio 在测试结束后删除测试文件。这会损失文件系统缓存的完整性但一般不影响纯性能测试。更好的做法是指定测试目录比如/data/fio-test/并在测试脚本开头mkdir -p结束后用rm -f清理。最佳实践是 job 文件里同时写directory/data/fio-test和unlink1。千万不要在/根目录下直接跑 fio否则文件残留在系统分区很快挤出可用空间。5.5 多线程压测触发系统参数瓶颈文件数限制与队列长度现象使用numjobs16跑压测时fio 启动报错io_setup failed或cannot allocate memory或者打开文件时报too many open files。原因iodepth32乘以 16 个 job 之后需要的内核 AIO 上下文数量很庞大。内核默认每个用户能申请的 AIO 事件数有限制同时系统 open files 上限也限制每个 job 打开的测试文件句柄。解决先用ulimit -n查看文件描述符上限如果低于 65536用ulimit -n 65536临时提高。AIO 限制可以用fs.aio-max-nr调整需要 root 权限执行sysctl -w fs.aio-max-nr1048576这个命令把内核允许的 AIO 请求总数提升到 100 万。改完后再跑就不会出现 io_setup 失败。这里有个教训压测系统前先检查系统限制不要让压测脚本变成压测环境的元凶。很多线上系统被 fio 压翻车原因是 fio 没有遵守生产环境已有的 IO 调度和资源配额。6. 把 fio 3.8 用得更顺手压测脚本的模板化与结果自动解析技巧当你需要反复对比多块磁盘或者多个版本的固件时一条条手输 fio 命令就很低效。我会用 bash 脚本定义一套可复用的 job 模板把变量通过环境变量传入 fio 的 job 文件。fio 支持 job 文件里的变量替换比如${BS}执行时传入环境变量。BS4k RWMIXREAD70 fio - EOF [global] ioenginelibaio direct1 rwrandrw rwmixread${RWMIXREAD} bs${BS} runtime60 time_based group_reporting1 [Mixed] size1G iodepth32 numjobs2 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 给脚本算。最后说一个自己的习惯任何压测结果我都会在最开始记录被测设备的型号、固件版本、文件系统、挂载参数。否则下一次测试同一块盘可能因为文件系统不同结果差别很大。以前我拿到新盘就直接跑默认随机写测出一堆数字后根本没法定位问题后来才意识到是挂载参数没记录。这个习惯被验证了很多次希望帮到你。本文还有配套的精品资源点击获取