ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

fio磁盘性能压测:参数配置、避坑指南与报告解读

fio磁盘性能压测:参数配置、避坑指南与报告解读

简介:fio-3.8.zip 是 FIO 3.8 版本的源代码压缩包,FIO 是一款灵活的输入输出压力测试工具,常用于评估固态硬盘、机械硬盘、存储阵列及内存文件系统的读写性能。该资源面向存储测试工程师、运维人员和系统调优开发者,可用于吞吐量、每秒读写次数、延迟等多维度基准测试。压缩包共包含四百三十三个文件,总大小约八百七十二千字节,其中包括一百六十四个 C 语言源文件、一百四十八个头文件,以及五十八个 fio 配置样例、七个 shell 脚本、六个 job 作业文件和五个 Python 辅助脚本。读者可自行编译生成可执行程序,并参考示例搭建顺序读写、随机读写、混合负载等测试场景。目前已有 1643 人学习下载。通过阅读源码和配套脚本,能够深入理解 FIO 的配置语法与统计结果,在设备选型、系统调优、性能排查等工作中直接复用这些配置,节省测试环境搭建时间。

1. fio-3.8.zip:一套能直接跑的磁盘性能压测工具箱,别只拿它当“黑匣子”

fio(Flexible I/O Tester)可能是 Linux 生态里最被低估的磁盘性能测试工具,而 fio-3.8.zip 这套资源把编译好的二进制、示例 job 文件和常用脚本打包在了一起,省去了从源码编译、依赖折腾的时间。我第一次接触它是在给一批混合硬盘做上线前验收,当时用 dd 粗测了几轮,数据忽高忽低根本没法形成结论,换成 fio 之后才拿到稳定的 IOPS、时延和带宽基线。如果你是运维、存储工程师,或者正在评估云盘/物理盘性能,fio 几乎是绕不开的标准答案。这篇笔记会带你从 zero 跑出第一份可信的报告,把参数含义、脚本模板和那些藏在 performance 数字背后的坑全部拆开,最后你会发现它不只是个压测工具,更是一套能帮你做容量规划决策的数据来源。

2. 先把 fio 的运行模型讲透:job 文件、ioengine 和 iodepth 决定了一切

2.1 为什么一上来就要理解 job 文件而不只是命令行

fio 最常用的两种调用方式是纯命令行参数和 job 文件,实际压测时我几乎只用 job 文件。原因很简单:命令行参数一旦超过十几个,整个命令不仅难读,而且容易手抖改错参数。job 文件本质上是一个 ini 风格的配置块,一个文件里可以定义多个 job,每个 job 可以跑不同的 I/O 负载,非常方便做对比测试。

一个最简单的 job 文件长这样:

[global] ioengine=libaio direct=1 bs=4k rw=randread size=1G runtime=60 time_based [test1] filename=/dev/sdb

这里[global]表示全局默认配置,[test1]是第一个任务实例。ioengine 指定 I/O 引擎,direct=1 表示绕过 page cache 直接读写磁盘,bs=4k 是单次 I/O 块大小,rw=randread 是随机读模式,size=1G 表示测试总数据量,runtime=60 配合 time_based 表示跑满 60 秒才算完。这个脚本的含义就是:用 libaio 引擎在 /dev/sdb 上连续随机读 60 秒,每读 4K,绕过缓存,直接和盘片对话。

2.2 libaio、sync、io_uring 怎么选:不是越新越好

ioengine 的选择会直接影响压测结果的有效性和真实性。fio 里常见的引擎有 sync、libaio、io_uring、pvsync 等,其中 sync 是朴素的一次一请求,谁发起谁等待,几乎模拟不出真实并发。libaio 是异步 I/O,在 fio 3.8 时代是主流选择,适合模拟数据库这类有很多并发请求的场景。io_uring 是后起之秀,但当时不少内核版本支持还不太好,而且部分老内核直接跑会报错。

我一般在老内核或生产机器上默认用 libaio,只有在需要验证某些新特性时才用 io_uring 做对比。对于 SSD 压测,libaio 已经足够逼近真实负载了。对于机械盘或者 NFS 这类网络存储,则要把iodepth(队列深度)调小,不然测试会失真,这个后面展开讲。

2.3 iodepth 和 numjobs 的组合,折腾了多少人

iodepth 是每个 job 的 I/O 队列深度,numjobs 是并发 job 数量。两者不是一个概念,但经常被混着调。总并发请求数约等于 iodepth × numjobs,但要小心 CPU 和内核锁竞争,开太多线程反而会降低吞吐。

fio --name=randread --ioengine=libaio --iodepth=64 --numjobs=4 \ --bs=4k --direct=1 --rw=randread --size=1G --runtime=60 \ --group_reporting /dev/sdb

--group_reporting会把多个 job 的结果合并输出,不然你会看到四组独立报告,读起来很费劲。--size=1G是每个 job 的 I/O 规模,四个 job 合计就是 4G,如果磁盘剩余空间不够就会直接失败。参数搭配上,我一般建议 SSD 用 iodepth=32 或 64,配合 numjobs=4~8;而机械盘建议 iodepth=8~16,numjobs=1~2,否则会产生很多排队等待,数字很难看。

3. 自己搭压测脚本:顺序写、随机读、混合读写三个模板直接拿走

3.1 顺序写模板:最容易被 flash 缓存骗到的场景

在做 SSD 评测时,很多人喜欢先跑一轮顺序写,看到 1.5GB/s 的写入带宽就觉得这块盘很快,实际上那可能是写入到了 SSD 的 SLC cache,一旦 cache 写满,速度立刻断崖式下跌。下面这个脚本加入了runtime和time_based,可以让写入持续几分钟,把缓存打穿之后再观察真实写性能。

[global] ioengine=libaio direct=1 bs=1m rw=write size=10G runtime=300 time_based group_reporting [seqwrite] filename=/dev/sdb

执行命令:

fio /path/to/seqwrite.fio

跑完之后除了关注 BW(带宽)平均值,还要看slat、clat和lat的区别。slat 是提交 I/O 到内核的耗时,clat 是从提交到完成的时间,lat 是 slat + clat。一般情况下 clat 是主要关注点。如果看到 clat 的 p99(99% 分位时延)远高于平均时延,就说明写入过程有突发长尾,这在数据库场景下往往是不可接受的。

3.2 随机读模板:模拟 OLTP 数据库 IO 特征

随机读是评估存储系统 QPS 能力最直接的负载。对于模拟 InnoDB 这类数据库引擎的随机读,4K 块大小和 32~64 的队列深度比较接近真实场景。注意size要远大于机器可用内存,否则页缓存会帮你兜住很多请求,测出来的延迟全部失真。

[global] ioengine=libaio direct=1 bs=4k rw=randread size=10G runtime=120 time_based group_reporting [rr] filename=/dev/sdb iodepth=32

这里每个 job 的队列深度是 32,如果单队列无法获得预期 IOPS,可以增加 numjobs。常见的做法是直接把numjobs=4写进[global],然后把 iodepth 降到 8 或 16,让总并发保持不变,但这种方式会增加共享锁竞争,并非线性提升性能。对比测试时,动一个变量就够了,两个变量一起动会搞不清楚结果到底受了谁的影响。

3.3 混合读写模板:读写比例用 rwmixread 控制

生产系统很少是纯读或纯写,更多是读写混合,这时候要用rw=rw配合rwmixread来设置读比例。比如 70% 读、30% 写,这是在线事务类应用比较典型的分布。

[global] ioengine=libaio direct=1 bs=8k rw=rw rwmixread=70 size=10G runtime=120 time_based iodepth=32 numjobs=4 group_reporting [mix] filename=/dev/sdb

跑完以后除了看 IOPS,还建议把write_lat和read_lat分开解读。混合读写时经常出现读延迟正常、写延迟偏高的情况,这不一定说明磁盘写能力差,可能是因为写穿透到了更底层。对于 SSD,混合读写会触发垃圾回收,延迟波动比顺序读写更明显,所以建议跑至少 300 秒,多观察几个时间片的报告,不要只记录 60 秒的数据。

3.4 怎么拿到一份足够可信的基线数据

压测最怕的是没有对比基准。同一块盘,文件系统格式化和裸设备性能差很多;分区起始扇区不对齐,性能也差很多。我的习惯是每次压测前先记录以下信息:测试机器的内核版本、fio 版本、磁盘型号和固件、分区对齐情况、文件系统类型和挂载参数。这些基础信息在出问题排查时特别重要。把上面这些信息写进压测报告的首部,才算是一份拿得出手的基线数据。

4. 避坑指南:我在 fio 压测路上踩过的五个经典坑

4.1 测试没跑完直接报错,原来是被系统 OOM 杀了

现象:fio 跑到一半进程消失,dmesg里看到Out of memory: Killed process。
原因:size设置得太大,而 fio 的缓存和队列缓冲占用了大量内存,任务触发了系统 OOM killer。
解决:减小size到物理内存的 1/2 以下,或者用memory_limited=1限制每个 job 的内存使用。再不行就把numjobs降低,看看总内存占用情况。

4.2 direct=1 没写,结果测出来的全是 page cache 的速度

现象:同一块盘测了两次,顺序读性能差异好几倍,第一次特别快。
原因:你没加direct=1,数据先落到页缓存,第二次读直接从缓存返回,根本没碰到盘。
解决:凡是压测物理盘,direct=1几乎必须写。如果确实想测文件系统缓存能力,那就明确把缓存作为被测对象,而不是给盘下结论。

4.3 iodepth 调大后 IOPS 不升反降

现象:iodepth 从 16 加到 64,随机读 IOPS 反而从 9 万多掉到 6 万多。
原因:队列太深导致 I/O 在设备端排队时间变长,甚至触发 SSD 内部调度异常,时延上升而吞吐下降。
解决:从低到高逐档测试:iodepth=1、4、16、32、64,记录每档的 IOPS 和延迟,找到拐点。压测不是参数越大越猛,而是要找到设备的最优工作点。

4.4 runtime 太短,根本打不穿 SLC cache

现象:SSD 顺序写速度显示 2GB/s,持续几分钟后掉到 500MB/s。
原因:短时测试时数据基本都写入到了 SLC 模拟缓存区,真实 TLC/QLC 写入速度并没有被暴露。
解决:顺序写压测 runtime 至少 600 秒,或者把size设为磁盘容量的 30% 以上。用fio --status-interval=10观察每 10 秒的带宽变化,能看到明显的断崖点。

4.5 多块盘测试用了同一个 filename 变量

现象:写了四块盘的压测 job,结果四份报告数据几乎一样,运行时还有 IO 错误。
原因:job 文件里没有用${filename}参数,四个 job 全写在同一个盘上,相当于四倍负载打在同一块盘。
解决:给每个 job 单独指定filename=/dev/sdb、/dev/sdc、/dev/sdd、/dev/sde,或者用numjobs搭配filename_format自动生成。

5. 看懂 fio 的报告并把它变成决策依据:从 IOPS 数值到真实业务评估

5.1 三个必须读透的指标:IOPS、带宽和时延分位数

fio 输出的报告是文本模式的表格化数据,每次测试完会显示 read 和 write 两个维度的统计。不要只盯着 IOPS,时延分位数才是性能调优的关键。拿随机读的报告举例:

read: IOPS=16220, BW=63363KB/s (64.0MB/s)(3802MB/61401msec) read: latencies: mean=788.19us, p50=720, p90=1100, p99=2500, p99.9=5800

前面那行好懂,IOPS 是每秒请求数,BW 是带宽。后面那行是延迟分位数,意思是 50% 的请求在 720us 内完成、90% 在 1100us 内、99% 在 2500us 内、99.9% 在 5800us 内。生产环境看 p99.9 甚至 p99.99,因为最慢的几个请求往往就是数据库锁等待和写入放大问题的根源。

5.2 拿 fio 结果反推业务容量规划

经常遇到的问题是“我这台服务器能扛多少并发用户”,其实 fio 不能直接回答这个问题,但 fio 数据可以给你反推依据。如果一页数据 8KB,每次用户点击平均触发 10 次随机读,每个请求需要 1ms,那单用户产生的 IOPS 需求大约是 10 次/秒。你的盘 p99 时延是 2ms,只要并发超过某个阈值,时延就会指数上涨。通过 fio 压测得到的峰值 IOPS 和对应的延迟分位,再结合业务模型,才能计算合理并发数。这是一个成本低、见效快的容量评估方法。

5.3 如何把 fio 结果转成 JSON 自动化收集

fio 原生支持--output-format=json,这个功能在自动化压测平台和 CI 环境里特别有用。输出 JSON 之后可以用 jq 或者 Python 解析关键指标。

fio --name=test --ioengine=libaio --direct=1 --bs=4k \ --rw=randread --iodepth=32 --size=1G --runtime=30 \ --output-format=json > result.json

之后用 Python 拉取关键字段:

import json with open('result.json') as f: data = json.load(f) jobs = data['jobs'][0] read = jobs['read'] print(f"IOPS: {read['iops']}") print(f"p50: {read['clat_ns']['percentile']['50.000000'] / 1000} us") print(f"p99: {read['clat_ns']['percentile']['99.000000'] / 1000} us")

clat_ns 里的数值单位是纳秒,除以 1000 转成微秒。这种提取方式适合跑完一批机器后批量生成报告。如果想把多台机器的结果汇总对比,我一般会把这些字段写入 CSV,再让绘图工具生成趋势图,这样可以直观看到不同批次机器之间的性能差异,还敢让供应商签性能承诺吗?有了这几行数据做参照,至少你能知道哪些盘值得签,哪些盘需要换货。

从那以后,我每次压测前都会强制自己在 job 文件里写明description=日期_场景_盘符,并把 JSON 输出固定存到/var/lib/fio-results/,方便后续追溯。毕竟压测这种事情,记录比结论更值钱。希望这套流程能帮到你。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进