☰
fio 3.8 存储压测完全指南:从编译到可信 IOPS 与延迟分析
2026/10/13 13:39:02 网站建设 项目流程

简介:FIO 3.8 开源 I/O 性能测试工具源码包,面向存储工程师、运维人员与底层开发者,用于对 SSD、HDD、RAID 等设备进行基准测试与压力验证,帮助定位性能瓶颈、评估硬件选型和系统调优效果。压缩包共 433 个文件、约 872KB,主要包含 164 个 C 头文件、148 个 C 源文件及 58 个 FIO 配置文件/示例作业,另有少量 shell、Python 辅助脚本与构建说明,目录结构清晰,便于查看完整实现与二次开发。目前已有 1643 人学习下载。通过该包可获得官方 3.8 版本全部源代码、configure 与 Makefile 构建体系、多样化的 job 示例和测试脚本,从编译安装到自定义工作负载均可完整复现;无论是评估新购存储设备、调整文件系统参数,还是排查线上存储性能问题,都能借助其丰富的 I/O 模式与详尽统计报告获得可量化依据。

1. fio-3.8.zip 到底能帮你验证什么

很多人把 fio-3.8.zip 解压后随手跑一条命令,看到屏幕上蹦出一排 IOPS 数字就以为拿到了磁盘性能的体检报告。实际上这些数字里可能一半是 page cache 的功劳,另一半是你没搞懂队列深度造成的虚高。fio 是 Linux 存储压测的事实标准,3.8 这个版本语法稳定、依赖少、编译门槛低,特别适合固定成团队内部的基准测试基线。本文只解决三件事:怎么编译出一个自己可解释的 fio,怎么把负载模型写清楚,以及拿到结果后怎么判断哪些数字能信、哪些数字是陷阱。适合刚接手存储性能验证的同学,也适合要搭回归测试脚本的运维。

2. 从 fio-3.8.zip 到第一个可信的 IOPS 数字:编译与最小跑通

2.1 为什么拿 3.8 当基线而不是最新版

如果你去翻 fio 的发布历史,会发现版本迭代非常快,新版本不断加 io_uring、针对 NVMe 的调度优化、新的延迟统计模式。但存储压测最怕的就是基线漂移:同一个负载模型,今天跑 3.7,明天跑 3.9,结果差了百分之十几,你很难判断是磁盘固件变了还是 fio 行为变了。3.8 恰好是维护期里比较稳的一个版本,源码还是传统的configure+Makefile工程,系统上只要装好 libaio 和 zlib 就能编过,编译时间在一分钟左右。它对异步引擎 libaio、同步引擎 psync、posixaio 的支持都已经成熟,jobfile 语法和后来主流版本兼容度也很高,意味着你现在写好的配置,将来升级 fio 时改动成本很小。

关键的一点是,fio 本身只是负载发生器,它不会告诉你“这块盘好不好”,它只负责按你定义的 IO 模型去打压力,再把时延分布和吞吐量记录回来。内核的 I/O 调度、NVMe 多队列、文件系统缓存策略都会影响最终数字,所以把 fio 版本固定住,等于把变量里最可控的一个先摁死。3.8 里没有后来那些花哨特性,反而让你更关注负载模型本身是否合理。

2.2 最小编译安装:依赖、configure 参数与版本验证

在 Debian/Ubuntu 系系统上,先装编译依赖。这一步漏掉 libaio 是初学者最常见的翻车点——fio 照样能编译成功,但ioengine=libaio会被禁用,等你写好 jobfile 跑的时候才报错。

sudo apt-get install -y build-essential libaio-dev zlib1g-dev unzip fio-3.8.zip cd fio-3.8 ./configure --prefix=/usr/local make -j$(nproc) sudo make install which fio fio --version

上面的命令里,libaio-dev是异步 I/O 引擎依赖的库,缺失时 configure 会静默降级;--prefix指定安装目录,如果你不想污染系统环境,可以改成--prefix=$HOME/fio-3.8,然后把$HOME/fio-3.8/bin加进 PATH。make -j$(nproc)用满本机 CPU 核心加速编译,老机器建议-j4避免内存吃紧。最后fio --version输出如果显示 3.8,说明安装成功;如果显示的是系统自带的旧版 fio,执行sudo ln -sf /usr/local/bin/fio /usr/bin/fio或在当前 shell 里调整 PATH 顺序。

安装完成后建议先用一条极简命令冒烟测试。注意,这条命令的输出会成为后续所有 jobfile 参数的参照系,所以参数含义要逐一说清。

fio --name=smoke --ioengine=libaio --direct=1 --bs=128k --rw=read --size=2G \ --runtime=30 --time_based --group_reporting

--name只是任务名,会出现在输出里方便区分;--ioengine=libaio显式指定异步引擎,避免默认引擎因编译选项不同而漂移;--direct=1绕过 page cache,直接面向裸设备或文件系统的块层,这是让数字可信的第一道闸门;--bs=128k是块大小,顺序读场景里 128k 比较符合文件服务器特征;--size=2G限定测试空间,配合--runtime=30和--time_based,保证测试跑满 30 秒而不是读够 2G 就提前结束。--group_reporting把多个 job 的统计聚合输出,跑出来的read: IOPS=和clat percentiles才是你要看的核心指标。

2.3 跑完第一步:输出里哪几行值得盯

冒烟测试结束后,屏幕上会有以read:开头的一段统计。先看第一行的IOPS,再看bw后面的带宽数值,最后翻到clat percentiles看p50、p99这两个分位点。很多初学者只看 IOPS,不看时延分布,但存储性能的真实体验往往由 p99 决定:平均值再漂亮,p99 出现毫秒级毛刺,数据库照样卡顿。

如果你跑出来的 IOPS 高得离谱(比如一块 SATA SSD 顺序读跑出接近内存带宽),先不要高兴,八成是direct没生效或者测试文件还在 page cache 里。这时回看输出前几行的iodepth=、direct=字段,确认和你命令行一致。另外注意Jobs: 1这个字段,如果你的测试后面加了多 job,这里会变成Jobs: 4,意味着所有统计都被乘了并发数,对比时一定要折算成单 job 表现。

3. 把压测写进 jobfile:核心参数与三种常用负载模板

3.1 为什么不用命令行而用 jobfile

命令行适合临时验证,但没法归档。我一般在目录里建一个jobs/文件夹,按业务场景把负载模型存成.ini文件,和代码一样提交到 git。fio 的 jobfile 是 ini 风格,[global]段放公共参数,每个[job名]段是一个独立任务,多个任务段默认并行执行,想串行要在段尾加stonewall。这个并行语义是新手最容易理解错的点:你在 jobfile 里写了两个段,fio 会同时打两种负载,而不是像读代码一样从上到下顺序跑。

[global] ioengine=libaio direct=1 time_based=1 runtime=60 group_reporting=1 [seq-read-128k] rw=read bs=128k size=4G stonewall [rand-read-4k] rw=randread bs=4k size=2G

上面的配置里,[global]段的time_based=1和runtime=60会自动被两个 job 继承,所以不需要在各自段里重复写。stonewall放在第一个段后面,意思是第一个段全部跑完后,第二个段才开始,这样两个负载之间不会互相干扰。如果不加stonewall,两个段会同时跑,输出里会多出两份统计,读起来很累。

3.2 决定测试可信度的八个参数

下面这张表是我配置 fio 负载时每次都要过一遍的清单,全部列完再动手跑。参数之间耦合很紧,改一个就要重新考虑另一个。

参数作用常见取值影响与注意
bs块大小4k / 8k / 128k / 1M决定 IO 模型是随机小包还是顺序大包,数据库典型 4k-16k
iodepth每个 job 的在途 IO 数1 / 16 / 32 / 64异步引擎专用,同步引擎设了也无效
numjobs并发 job 数1 / 4 / 8真实队列深度 = numjobs × iodepth,这是最大的误用点
direct是否绕过 page cache0 / 1测裸盘性能必须为 1,否则结果包含内存缓存收益
rw读写模式read / write / randread / randwrite / rw / randrw顺序与随机差异巨大,混测时配合rwmixread
rwmixread混合读写里读的占比70 / 50 / 30只对 rw 和 randrw 生效
runtime+time_based测试时长与强制计时60 / 300没有 time_based,任务提前完成会直接结束
ramp_time正式计时的预热时间10 / 30给磁盘和内核缓存一个稳定期,避免起始阶段波动

numjobs和iodepth的关系值得单独强调。很多人以为iodepth=64就是 64 个在途 IO,但如果同时设了numjobs=8,实际提交深度是 512。对 NVMe SSD 来说深度 512 可能仍有余力,但对机械盘或虚拟化存储,这个深度会把时延直接打爆。理解成“进程数 × 每进程队列深度”才是真实压力。

另一个容易忽略的是direct和sync的区别。direct=1绕过操作系统缓存;sync=1是每次 IO 完成后要求数据落盘。两者不冲突,但测裸设备性能时开direct=1就够,不必加sync;测数据库 fsync 性能时才需要sync=1配合小规模随机写。

3.3 三个可以直接抄的负载模板

下面三个模板覆盖了绝大多数业务场景。第一个是顺序大块读,适合文件服务器、流媒体、备份恢复场景:

[seq-read-128k] rw=read bs=128k size=8G iodepth=32 numjobs=1 runtime=60 time_based=1 group_reporting=1

顺序读的调优重点是iodepth。深度太低(比如 1)无法发挥 NVMe 多队列能力,太高则产生排队延迟,通常 16 到 32 之间试一轮,看哪个深度下 IOPS 和 p99 的平衡最好。size=8G在这里只是限定文件区域,真正控制时长的是runtime=60。

第二个是数据库典型的 4K 随机读:

[randread-4k] rw=randread bs=4k size=2G iodepth=32 numjobs=4 runtime=60 time_based=1 group_reporting=1

注意真实队列深度是4 × 32 = 128。如果机器内存有限,4 个 job 各预分配 2G 文件,总占用 8G 空间,跑之前确认磁盘剩余空间。这个负载下如果 IOPS 达到磁盘标称值的 90% 以上,基本可以判断盘没问题,剩下要调的是应用层的连接池和语句模式。

第三个是 OLTP 类混合读写,读占七成:

[oltp-mix] rw=randrw rwmixread=70 bs=16k size=2G iodepth=32 numjobs=2 runtime=120 time_based=1 group_reporting=1

rwmixread=70表示 70% 读、30% 写。混合负载下你不能只看总 IOPS,要关注读和写各自是不是都达标。fio 输出会分别列出read:和write:段,有的盘读性能很强,一旦掺入写就剧烈波动,这正是randrw场景要暴露的问题。

4. 别急着下结论:fio 结果里的 IOPS、时延与可信度验证

4.1 输出字段拆解:slat、clat、lat 分别代表什么

跑到这一步,你已经能稳定产出数字了,但 fio 默认输出里藏着一组被忽略的指标。每次 IO 从 fio 视角看有三个阶段:提交(submission)、完成(completion)、总耗时。对应输出里的slat、clat、lat三行。

slat是 fio 把 IO 提交给操作系统的耗时,数值通常在微秒级,如果slat异常高,说明 CPU 或者内核路径上出现了瓶颈;clat是从提交到完成的时间,这才是磁盘和内核块层真正消耗的时长,裸盘测试主要看它;lat是两者之和,应用层感知的延迟要看lat。当你看到一个测试lat很高但clat正常时,问题大概率出现在 fio 所在机器的 CPU 调度上,而不是磁盘本身。

判断数字有没有参考价值,我的习惯是先看三处:error字段是否为 0,Jobs数量是否和配置一致,clat percentiles的 p99 与 avg 差距是否在一个数量级内。如果 p99 是 avg 的十倍以上,说明负载里出现了严重的排队毛刺,这种盘跑数据库会直接表现为周期性的慢查询。

4.2 用 JSON 输出做机器解析

人类可读的文本输出偶尔会被刷新覆盖,不方便归档。fio 从 3.8 开始已经把 JSON 输出做得比较完整,我建议所有正经测试都用 JSON 落盘,文本输出只用来快速扫一眼。

fio oltp-mix.fio --output-format=json --output=/tmp/oltp-result.json jq '.jobs[0].read.iops' /tmp/oltp-result.json jq '.jobs[0].write.iops' /tmp/oltp-result.json jq '.jobs[0].read.clat_ns.percentile' /tmp/oltp-result.json

解释一下上面的命令:--output-format=json把结果写成结构化数据,--output指定保存路径。用jq提取字段时,.jobs[0]表示第一个 job,混合读写场景里.read和.write是并列的两个对象;.read.iops是读 IOPS,.read.clat_ns.percentile是一个对象,里面键是"50.000000"、"99.000000"这类分位值。如果你的 fio 编译时选择了微秒精度,字段名可能是clat_us,先执行jq '.jobs[0].read | keys'看一眼再提取,不同构建版本的字段差异是正常的。

4.3 数据一致性验证:测试前后怎么做基准对照

一次测试不能说明问题,三次测试的稳定性才有意义。我自己固定跑三遍,取中位数存档,不取平均值,因为平均值会被某次的磁盘内部 GC 或后台刷盘拉偏。测试前统一做两件事:清空 page cache,并确认没有其他 IO 任务在跑。

sync echo 3 | sudo tee /proc/sys/vm/drop_caches

sync先把脏页刷到磁盘,echo 3告诉内核丢弃 page cache 里的干净页和目录项。这是为了尽量抹平缓存对读测试的干扰,虽然direct=1已经绕过了缓存,但文件系统的元数据缓存仍然会影响首次读取的延迟分布。另外注意,如果你测试的是整块裸设备(比如/dev/nvme0n1),drop_caches 的作用会小一些,因为绕过文件系统后页缓存参与度更低。

前后对照时,除了 iops 和 p99,还要记下测试时的内核版本、磁盘固件版本、文件系统类型。同一块盘在 ext4 和 xfs 上的 fio 结果可能有明显差异,这些信息应该写进 jobfile 头部的注释里,方便后人复现时先判断环境是否一致。

5. fio 3.8 避坑清单:现象、原因、解决

5.1 测试秒结束:size 与 runtime 的关系没搞清

现象:命令返回 0,但runtime=60没生效,整个测试不到一秒跑完。原因:size指定的数据量太小,任务提前完成,而time_based没有设置,fio 按“传完指定量就退出”的语义直接结束。解决:要么去掉size改用纯时间控制(配合time_based),要么两者同时设定。顺序读 2G 文件在一块 NVMe 上可能只需要几秒,如果不强制计时,测出来的时延分布毫无统计意义。我会在[global]里固定写runtime=60和time_based=1,所有 job 继承之后就不会再犯。

5.2 数字高得离谱:direct=0 时你测的是内存

现象:SATA SSD 跑 4K 随机读,IOPS 超过 30 万,接近内存带宽。原因:direct默认是 0,读操作直接命中 page cache,fio 测的是内存复制而不是磁盘。解决:命令行或 jobfile 里显式设置direct=1,并在跑完后检查输出中direct=字段确认生效。还有一个隐蔽场景:文件刚被写过一遍,随后再读同样区域,direct=1虽然绕过页缓存,但文件系统预读或者硬件缓存仍可能带来干扰。要彻底避掉就用--ioengine=libaio+direct=1+ 文件大小远大于内存的 1/4,这样即使有缓存,命中比例也可忽略。

5.3 libaio 的队列深度不是 iodepth

现象:设置了iodepth=64,但iops对应的在途请求数只有 8,吞吐上不去。原因:iodepth是每个 job 的队列深度,而实际提交深度还受numjobs和iodepth_batch影响;更重要的是 libaio 引擎下,单次io_submit系统调用能提交的请求数有限,队列深度并不等于 fio 一次全提交。解决:先确认numjobs,总深度等于numjobs × iodepth;然后打开--debug=io观察提交日志,如果单批提交数总小于 32,调高iodepth_batch到和iodepth相等再测。这个问题容易让人误判为磁盘性能差,实际上是压力根本没打满。

5.4 随机写测试把盘写废或空间撑满

现象:randwrite跑完后/分区满了,或者 SSD 寿命在短时间内明显损耗。原因:size设置过大,fio 会为你预分配一个与size等大的文件,随机写模式下频繁覆盖会让 SSD 做写放大,磨损加速。解决:写测试优先指向专用测试文件,例如--filename=/tmp/fio-test.dat,并设置size=2G这类小空间;跑完立即删除。对 NVMe 或企业级 SSD,如果必须做持久性验证,用norandommap=1搭配random_distribution=zoned让写入集中在指定区域,避免全盘磨损。生产业务盘上不要跑随机写压测,要跑先和团队确认维护窗口。

5.5 同一命令两次结果差 20%:后台任务与绑核缺失

现象:连续跑同一个 jobfile,iops 和 p99 波动明显,有时差异超过 20%。原因:后台有 crond、日志滚动、其他容器的 IO 干扰;CPU 频率波动也会影响slat。解决:测试前用htop确认没有重负载任务;在 jobfile 里用cpus_allowed=0-3把 fio 绑到固定 CPU 核心;再把ramp_time=10加上,给调度器和缓存一个预热期。我通常跑三轮,每轮之间间隔 10 秒,取中位数作为基线。如果三轮里最大值和最小值还是差很多,先怀疑测试机不是空闲状态,而不是盘的问题。

6. 把单次压测变成回归基线:脚本化提取 IOPS 与 p99

6.1 一个可以直接用到团队里的回归脚本

等到 jobfile 确定下来,下一步是把它固化到脚本里,让每次测试都产生同样的指标集。下面这个脚本循环跑三轮随机读,把每次的 IOPS 和 p99 打出来,最后取中位数。

#!/bin/bash JOBFILE=randread-4k.fio OUTDIR=./results mkdir -p "$OUTDIR" for i in 1 2 3; do fio "$JOBFILE" --output-format=json --output="$OUTDIR/run-$i.json" iops=$(jq '.jobs[0].read.iops' "$OUTDIR/run-$i.json") p99=$(jq '.jobs[0].read.clat_ns.percentile["99.000000"]' "$OUTDIR/run-$i.json") error=$(jq '.jobs[0].error' "$OUTDIR/run-$i.json") echo "run $i: iops=$iops p99_ns=$p99 error=$error" done

这个脚本里run-$i.json保留每一轮原始数据,方便事后重新解析。p99_ns的单位是纳秒,除以 1000 才是微秒,阅读结果时别被数字大小误导;如果解析出来是null,说明你的 fio 编译输出用的是clat_us,把clat_ns换成clat_us即可。error字段只要不为 0,说明这轮测试本身失败了,这轮结果直接作废,不要并入中位数计算。跑完脚本后,我习惯把三元组的 json 文件和一个 markdown 表格一起提交到 git,记录日期、内核版本和盘型号,下次固件升级或调参后重跑,直接看趋势就知道改动是正向还是负向。

6.2 我现在的习惯

做存储性能验证这几年,我越来越相信一件事:fio 测的是负载下的行为,不是硬盘标称值。标称值只是上限,真实系统里队列深度、缓存策略和业务并发共同决定体验。我现在每次测试前都会在 jobfile 头部注释里写清楚目标场景、预期指标和用法,测试时不看实时输出,直接落 JSON,跑完统一解析。后端的教训是踩了不少坑才换来的,比如最开始从文本输出里抠数字,某个版本换行格式一变脚本就废了;后来全部迁到 JSON,至少格式稳定,社区用户也多,踩坑时搜索结果丰富。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询