
三块新盘的验收报告躺在群里跑分漂亮得很——磁盘性能测试工具 FIO打出来的 4K 随机读 30 万 IOPS顺序读 6GB/s。结果业务上线两周后报表任务从 40 分钟涨到一个半小时慢查询日志一天翻了三倍。把 fio 重新拎出来压一遍才发现那几块盘在混合小块随机读写的场景下直接掉到不足两万 IOPS99 分位延迟从 300 微秒飙到 30 毫秒。同一块盘前后两份报告差了十几倍问题不在硬件在测法。这件事之后我把 fio 的用法彻底捋了一遍。它不是什么高深的工具本质上就是一个按你指定的方式对指定位置反复发起 IO并把结果统计成表格的压测器。真正难的是怎么指定方式——块大小选错了、队列深度给少了、缓存没绕过去、跑的时间不够长出来的数字就完全不能用来做决策。它适合三类人做存储选型和验收的运维、给数据库挑盘的 DBA、以及写后端服务想知道自己 IO 模型会打成什么形状的开发。下面这些内容就是我把这几年踩过的坑和稳定可复现的配置整理出来的完整版本。1. 先弄清 fio 到底在测什么很多人第一次用 fio是照着某篇文章抄一条命令跑完盯着iops后面那个数字看然后截图发群。这种用法十次有八次得出错误结论。原因很简单磁盘性能从来不是一个标量它是一条随负载形状变化的曲面。你得先知道自己在曲面的哪个点上才知道那个数字有没有意义。1.1 四个指标与它们之间的换算关系fio 报告的核心指标就四个剩下的都是衍生品。IOPS每秒完成的 IO 次数。小块随机场景的主角。带宽 BW每秒搬运的数据量。大块顺序场景的主角。延迟单次 IO 从提交到完成的时间fio 会把提交延迟slat、完成延迟clat和总延迟lat分开统计。抖动延迟的分布离散程度。fio 用百分位曲线和 stdev 来表达这一项最容易被忽略但往往是线上体验的真正杀手。IOPS 和带宽之间有一个换算公式随手就能算带宽 IOPS × 块大小。比如 4KiB 块、每秒 5 万次 IO换算下来是50000 × 4KiB 200000KiB ≈ 195MiB/s。这个换算在做容量规划的时候特别有用因为磁盘厂商标称的是 IOPS而业务方跟你提的需求往往是我要每秒 300 兆的写入。一块盘如果顺序写能到 500MiB/s但在 4KiB 随机写下只有 2 万 IOPS那么它的随机写带宽上限其实只有 78MiB/s 左右差了六倍多。厂商宣传页上那个 500 是顺序大块下的数字拿它去套数据库的写入需求大概率要翻车。延迟和队列深度之间也有一个硬关系业内叫利特尔法则IOPS 队列深度 ÷ 平均延迟。反过来说你想跑到某个 IOPS需要的队列深度等于目标 IOPS × 单次延迟。这个公式后面第 4 章会展开算它是你设置iodepth的唯一依据拍脑袋填个 32 或者 128出来的数据基本没有参考价值。提示延迟的单位极易看错。fio 的slat/clat/lat在输出里单位可能是nsec也可能是usec或msec取决于数值大小fio 会自动选一个合适的单位并打印在括号里。看到clat (usec): avg79.34就是 79 微秒看到clat (msec): avg1.2就是 1.2 毫秒两者差了一个数量级做报告时看漏这个括号会闹大笑话。1.2 fio 的作业模型与 job 文件长什么样fio 的运行单位叫job。一个 job 就是一段独立的 IO 流定义它有自己的读写模式、块大小、队列深度、目标文件或设备。你可以在一条命令里定义多个 job也可以写进一个 job 文件里。多个 job 之间默认并行执行也可以靠stonewall参数强行串行。实际工作中我强烈建议用 job 文件而不是一长串命令行参数。原因有两个一是命令行参数超过十几个之后根本没法审阅同事接过你的命令看不出你到底测了什么二是 job 文件可以提交进版本库半年后回看还能复现。job 文件是 ini 风格的分[global]全局段和具名 job 段全局段的参数会被下面所有 job 继承。[global] ioenginelibaio direct1 time_based1 runtime300 ramp_time60 group_reporting1 filename/data/fiotest/testfile size40G [randread-4k] rwrandread bs4k iodepth32 numjobs4 [seqwrite-1m] rwwrite bs1m iodepth8 numjobs2上面这份文件定义了两个 job它们共享全局段的引擎、缓存策略、运行时长等设置只在读写模式、块大小和并发参数上分道扬镳。这样一份文件既能量出随机读的能力也能顺手量出顺序写的上限跑完一次 300 秒两份数据都有了。1.3 六个决定结果走向的核心参数fio 的参数有上百个但真正改变结论的就那么六个。把它们吃透剩下的都是锦上添花。ioengine决定 IO 以什么方式下发。Linux 上做磁盘压测基本只用libaio内核原生异步 IO或者更新的io_uringpsync、sync是同步引擎队列深度永远上不去只适合测单线程同步 IO 的场景mmap走内存映射容易把页缓存的影响混进来一般不用。选错引擎iodepth参数直接失效——用sync引擎填 128 的队列深度实际队列深度还是 1。direct决定是否绕过操作系统的页缓存。测物理介质必须设direct1否则你测的是内存读写速度几 GB/s 的数字毫无意义。这个后面单独说。bs是块大小它直接决定你测的是哪一类负载。4K 对应数据库的页大小8K 到 16K 是常见的文件系统块128K 到 1M 是大数据与备份场景。块大小选错等于跑错了赛道。iodepth是单个 job 的队列深度也就是同时有几个 IO 在路上。它决定了你能否把设备的并行能力压榨出来。消费级盘队列深度给到 32 就饱和了企业级 NVMe 能吃掉 128 甚至 256。numjobs是并发的 job 数量每个 job 是独立进程或线程。总队列深度近似等于numjobs × iodepth。用多 job 模拟的是多线程应用的并发形态比如数据库的连接池。runtime与time_based一起决定跑多久。只写runtime不写time_basedfio 会认为这是一次性跑完指定数据量就结束runtime只起超时保护作用。想跑固定时长两个必须同时写。2. 安装从包管理器到源码编译fio 的安装本身不复杂但版本差异带来的坑不少尤其是在老的服务器上。我遇到过最典型的一次是某台机器上装的是好几年前的版本ioengineio_uring直接报找不到引擎排查了半天才发现是版本太老——那个引擎是后面才加进去的。2.1 用包管理器装快但要确认版本绝大多数主流 Linux 发行版都收录了 fio。Debian 系用aptRedHat 系用dnf或yum# Debian / Ubuntu 系 sudo apt update sudo apt install -y fio # RedHat / CentOS / Rocky 系 sudo dnf install -y fio # 装完确认版本 fio --version包管理器装的好处是省事依赖自动解决缺点是版本跟着发行版走。较新的发行版一般能拿到 3.x 的中后期版本老发行版可能还停在 3.1 甚至 2.x。判断标准很简单fio --enghelp能列出哪些引擎fio --version看主版本号。3.0 以上基本够用但如果你想用io_uring引擎至少得是 3.16 之后的版本。装完顺手确认一下引擎清单这一步能省掉后面很多莫名其妙的报错fio --enghelp输出里如果能看到libaio、io_uring、sync、psync、null这些说明引擎是齐的。如果libaio不在列表里说明编译时没找到异步 IO 的开发库后面所有带队列深度的测试都会退化成同步模式数据直接失真。2.2 源码编译为了新引擎和可控版本需要io_uring、需要更细的统计输出、或者要在没有包仓库的内网机器上部署就得自己编。过程不复杂但依赖要装全# 编译工具与核心依赖 sudo dnf install -y gcc make libaio-devel zlib-devel # Debian 系对应 sudo apt install -y gcc make libaio-dev zlib-dev # 下载解压后进入目录 tar -xzf fio-3.35.tar.gz cd fio-3.35 # 配置。--prefix 指定安装路径避免污染系统目录 ./configure --prefix/usr/local/fio # 编译并安装 make -j$(nproc) sudo make install # 验证 /usr/local/fio/bin/fio --version./configure那一步的输出值得认真看一眼它会把检测到的引擎和功能列出来形如libaio yes io_uring yes posix aio yes zlib yes每一项是yes还是no直接决定了你能用哪些参数。libaio是no那基本等于白编。zlib是no就没法测压缩相关的负载。想要io_uring除了 fio 本身的版本还得确认内核版本够——uname -r至少是 5.1 以上同时对端得装了liburing开发包。注意用--prefix装到非标准路径后旧的fio可能还在PATH里。用which fio和fio --version双重确认你调的是新编的那个而不是系统自带的旧版本。这个坑我在两台机器上各踩过一次两次都是跑完发现结果对不上回头才看到用的是旧二进制。2.3 版本差异会带来哪些实际影响说几个具体会影响到测试结果的差异点方便你判断手里的版本够不够用。较老的版本3.0 之前没有io_uring引擎异步下发只能靠libaio而libaio在一些较新的内核上对某些设备的表现不如io_uring稳定。百分位延迟的自定义列表参数percentile_list在 2.x 后期才有老版本只能用默认的 1/5/10/20……那一串。JSON 输出格式在老版本里字段名和结构都不一样如果你的自动化脚本要解析结果版本不统一会直接报错。还有一个隐蔽的差异ramp_time的行为在早期版本里和time_based的配合有过变化某些版本里写了ramp_time但没写time_based预热阶段的数据仍然会被统计进去。稳妥的做法是永远两个一起写。3. 开测前的准备九成的错误数据出在这一步工具装好了参数也懂了但直接开跑大概率还是错的。测磁盘这件事真正决定数据可信度的不是 fio 的参数而是跑之前的现场准备。我把它拆成四件事选介质、绕缓存、跑稳态、挂监控。3.1 测文件还是测裸设备这是第一个岔路口两条路差别很大。测文件的意思是filename/data/fiotest/testfilefio 会在文件系统上创建一个指定大小的文件在文件范围内发 IO。这种测法包含了文件系统本身的开销——元数据、extent 映射、日志写、块分配策略。如果你的业务是跑在文件系统上的几乎所有应用都是那这个数字更贴近真实体感。测裸设备的意思是filename/dev/nvme0n1fio 直接对块设备扇区下 IO完全跳过文件系统。数字会比文件模式高因为它把文件系统的开销全砍掉了。它适合用来判断硬件的理论天花板或者验证存储厂商给的规格书。我的做法是两个都测先测裸设备拿理论值再测文件拿实际值两者的差距就是文件系统带来的损耗。这个损耗在小块随机写场景下经常有 10% 到 30%是个不能忽略的量。注意filename指向裸设备是直接擦写没有任何二次确认。上一次我在一台测试机上把filename写成了系统盘对应的设备名回车之后系统当场起不来。所以现在我的习惯是测裸设备前先用lsblk把设备名、容量、挂载点连起来看三遍确认这块盘既没有挂载点也没有分区表跟系统盘八竿子打不着。有条件的在跑之前加一个--readonly先做只读探测。3.2 绕过缓存direct1到底绕过了什么direct1让 fio 用O_DIRECT打开文件数据在用户态缓冲区和磁盘之间直接搬运不经过操作系统的页缓存。不设这个参数你测的是内存速度——现代服务器内存带宽动辄几十 GB/s能得出一份漂亮到荒谬的报告。但direct1并不能绕过所有缓存层。磁盘自己的写缓存、RAID 卡的缓存、虚拟化层的缓存、分布式存储的副本写缓存这些它都管不着。所以还经常配一个fsync参数fsync1表示每次 IO 完成后都下发一次 fsync强制落盘。开了它数字会难看很多但那才是真正数据安全落地的速度。[global] direct1 fsync1什么时候该开fsync看你的业务有没有持久化要求。数据库的 WAL 写入、消息队列的持久化消息这类场景测的时候必须开否则测出来的高 IOPS 是假的。反过来如果是缓存类服务、临时文件、视频流缓冲开fsync反而是过度严格测出来的数字会低估实际能力。还有一个常被忽略的缓存形态内存盘。有些机器把/tmp挂成了 tmpfs或者有些人为了图快把测试文件放在/dev/shm下面。这两种情况下direct1也没用因为整个文件就在内存里。判断方法很简单df -h看一下目标目录的文件系统类型tmpfs一律不能用。3.3 预热、填充与跑进稳态磁盘性能不是恒定的。刚通电的 SSDSLC 缓存是空的写什么都飞快跑满缓存之后速度断崖式下跌。机械盘刚访问时的寻道缓存状态和跑了几分钟之后也不一样。所以必须让 fio 先跑一段热身等设备进入稳定状态之后再开始统计。这就是ramp_time的作用。[global] time_based1 runtime600 ramp_time120上面这段的意思是总共跑 600 秒但前 120 秒的数据不进入统计只统计后 480 秒。对于一个 480 秒的统计窗口120 秒的预热是比较稳妥的比例。企业级 SSD 一般 60 秒能进稳态消费级的盘可能要 180 秒以上。文件模式下还有一个隐藏的时间成本fio 第一次运行时需要创建并填满那个测试文件。如果size40G这个填充过程本身就要花几十秒甚至几分钟而且这段时间的 IO 特征和后面完全不一样。更麻烦的是如果你没设time_basedfio 跑完一个完整的 size 就结束了可能填充刚结束测试也结束了你拿到的是最不具代表性的一段。我的做法是先单独跑一次纯填充把文件建好再跑正式测试。填充用一个专门的 jobrwwrite、bs1m反复重写几次把设备的映射表和 GC 状态都推到稳定区。第二次跑正式测试时 fio 会发现文件已存在且大小正确跳过填充直接开测。3.4 测试期间必须挂的监控只看 fio 自己报的数字等于只看病人自己的描述不做体检。至少要挂一个系统层监控做交叉验证。最轻量的是iostatiostat -x 1 5重点看几列r/s和w/s是实际的 IO 次数应该和 fio 报的 IOPS 对得上rkB/s和wkB/s是实际吞吐await是平均等待时间包括排队时间和实际服务时间aqu-sz是平均队列长度能反推实际生效的队列深度%util是设备忙碌比例。交叉验证的价值在于发现测了个寂寞的情况。比如 fio 报 20 万 IOPS但iostat显示w/s只有 3 万说明有大量 IO 被缓存吸收或者被合并了。再比如aqu-sz一直是 1而你明明设了iodepth64说明队列深度根本没生效——大概率是 ioengine 选错了退化成了同步模式。另外测试期间别跑其他重活。同机上的备份任务、日志轮转、监控采集频率过高都会污染数据。我一般会在测试前把非必要的定时任务临时禁掉测试期间只看监控不动手。4. 四类典型场景的参数配方参数不是拍脑袋定的每一个都应该有计算依据。这一章把常见的四类负载拆开从要跑多少 IOPS倒推该给多少队列深度给出可以直接抄的配方。4.1 先把队列深度算出来利特尔法则的实战用法公式前面提过队列深度 目标 IOPS × 单次 IO 的平均延迟。关键在于单次延迟不是猜的得先测。做法是先用一个极小的队列深度iodepth1、numjobs1跑一轮随机读拿到这个设备在无排队状态下的裸延迟。假设测出来 4KiB 随机读的平均clat是 200 微秒也就是 0.0002 秒。现在你想知道跑到 20 万 IOPS 需要多深的队列套公式队列深度 200000 × 0.0002 40所以iodepth设到 40 左右就够。设到 128 不会让 IOPS 变成三倍只会让延迟变长、百分位变差——因为设备处理不过来多余的在队列里排队。反过来也可以验证设备的上限。假设厂商宣称某盘随机读能到 50 万 IOPS而它标称的稳定延迟是 100 微秒那需要的队列深度是500000 × 0.0001 50。如果你把iodepth设到 50 还跑不到 50 万要么是延迟比标称的高要么是有别的瓶颈比如驱动、中断、或者你没开direct。再来看并发数。numjobs和iodepth的组合方式影响的是并行形态而不是单纯的总数。总队列深度约等于numjobs × iodepth。区别在于numjobs1, iodepth64是单线程异步下发 64 个 IO模拟的是一个高效的异步应用numjobs8, iodepth8是 8 个线程各下发 8 个模拟的是线程池形态。数据库这种多连接的应用用后者更贴近。场景建议 iodepth建议 numjobs说明单线程同步应用探测11拿到最干净的裸延迟基线数据库随机读写16 到 324 到 8总队列深度控制在 64 到 256企业级 NVMe 打满64 到 1284总深度 256 到 512接近硬件上限顺序大块写4 到 81 到 2顺序场景对深度要求低4.2 数据库型负载小块随机混合读写数据库的 IO 特征是块小4K 或 8K、随机、读写混合、队列有一定深度。对应到 fio就是rwrandrw加一个读写比例。[global] ioenginelibaio direct1 time_based1 runtime600 ramp_time120 group_reporting1 filename/data/fiotest/dbfile size40G [db-mix] rwrandrw rwmixread70 bs8k iodepth32 numjobs4rwmixread70表示 70% 读 30% 写。这个比例是从典型的 OLTP 场景里来的——查询远多于写入。如果你的业务是写密集的比如日志收集、时序数据入库比例要反过来调rwmixread20甚至10。这里有个细节值得说randrw模式下fio 是每个 IO 独立掷骰子决定读还是写。较新的版本还提供了rwmixpattern参数可以按固定节奏交替比如读 7 个写 3 个循环。要不要用取决于你模拟的场景——真实业务里读写通常是混在一起无规律的掷骰子更接近。另一个高频误用是size设得太小。见过有人size1G去测一块 4TB 的盘跑出来的数字比厂商标称还高。原因很简单1G 的范围太小随机地址在这么小的空间里高度集中SSD 内部的地址映射几乎全命中缓存GC 压力也小。合理的取值是测试文件占设备可用容量的 25% 到 50%。太小失真太大跑不完一个完整循环而且时间成本高。4.3 备份与迁移型负载大块顺序读写备份、数据迁移、大文件拷贝这些场景的 IO 特征是块大、顺序、深度可以很低。测法完全不一样。[global] ioenginelibaio direct1 time_based1 runtime300 ramp_time60 group_reporting1 filename/data/fiotest/seqfile size60G [seq-write] rwwrite bs1m iodepth8 numjobs2顺序场景下有两个陷阱。第一是块大小要和实际业务对齐。备份软件常用的块大小是 128K 到 1M如果你用 4K 去测顺序写得出的带宽数字会明显偏低因为它测的是小块顺序的性能跟实际的大块搬运不是一回事。测之前最好抓一下真实业务的 IO 特征实在抓不到就按 128K、512K、1M 各跑一遍看曲线在哪一段平了。第二是顺序写在 SSD 上其实是伪顺序。文件系统层面看是顺序追加落到闪存颗粒上仍然是按通道、按 die 分散写。所以你会看到顺序写的带宽随着size增大逐步下降到某个稳定值——那才是真实值。这也是为什么ramp_time在顺序写场景下尤其重要。还有一个容易混淆的点rwwrite是从零开始覆盖写rwrw是先读后写rwreadwrite是先读后写交替。测备份恢复速度用write测数据校验用read测迁移工具的综合能力用readwrite。选错模式数字差一倍不奇怪。4.4 延迟敏感型负载低并发下的纯探测有些业务不看 IOPS只看延迟。比如在线交易的数据库提交、实时风控的日志落盘它们的并发度本来就不高但要的是每一次 IO 都快且稳定。这时候用高队列深度去测得出来的数字跟业务体感完全不搭。[global] ioenginelibaio direct1 time_based1 runtime300 ramp_time30 group_reporting1 filename/data/fiotest/latfile size20G rwrandread bs4k iodepth1 numjobs1 percentile_list50:90:99:99.9:99.99iodepth1、numjobs1是纯粹的裸延迟探测。跑出来的clat平均值只是参考真正要看的是百分位曲线。percentile_list参数可以自定义输出哪些百分位默认那串可能不够细。99.9 分位和 99.99 分位的延迟如果比中位数高出一个数量级说明设备存在偶发的长尾——这种情况在高并发下会被放大成慢请求业务侧表现为偶尔卡一下。我一般会跑三组对比iodepth1、iodepth4、iodepth16。三组的中位数延迟应该接近但 99.9 分位会随深度快速上升。哪一档开始失控就说明这个设备的舒适区在那一档以下。这个结论比任何单一数字都更有指导意义。4.5 多盘与多机压测怎么组织单机单盘测完往往还要测整机或整个存储池。这时候有两个选择。多盘并行用 job 文件里多个 job 段每段指向不同的设备[disk1] filename/dev/nvme1n1 rwrandread bs4k iodepth64 numjobs2 [disk2] filename/dev/nvme2n1 rwrandread bs4k iodepth64 numjobs2group_reporting1会把结果按 job 分组汇总。要注意的是多个 job 同时砸盘会互相干扰测出来的是整机聚合能力不是单盘能力。想看单盘数据就一个一个来。多机压测用 fio 的客户端-服务端模式。在压测机上启动服务端fio --server --daemonize/var/run/fio.pid在控制机上指定目标并下发 jobfio --clienttarget-host-1 --clienttarget-host-2 db-mix.fio这个模式在测分布式存储或者共享存储池的时候特别有用因为单个客户端节点可能压不满后端。多客户端同时打才能看清后端聚合能力的真实上限。要注意所有客户端的 fio 版本尽量一致版本不一致时握手会失败。5. 输出怎么读别只盯着 iops 那一行fio 的默认文本输出信息量很大但绝大多数人只看了前两行。真正有用的信息在后面。5.1 逐行拆解一份典型报告下面是一份 4K 随机读的典型输出数字做过处理只保留结构test: (groupid0, jobs4): err 0: pid12345: Mon Jan 1 10:00:00 2024 read: IOPS199k, BW777MiB/s (815MB/s)(20.0GiB/26371msec) slat (nsec): min1200, max88012, avg2345.67, stdev1102.33 clat (usec): min32, max4211, avg79.34, stdev41.22 lat (usec): min34, max4215, avg81.70, stdev41.30 clat percentiles (usec): | 1.00th[ 40], 5.00th[ 45], 10.00th[ 48], 20.00th[ 52], | 50.00th[ 65], 90.00th[ 103], 95.00th[ 122], 99.00th[ 178], | 99.90th[ 372], 99.99th[ 832] bw ( KiB/s): min680000, max820000, per100.00%, avg795000.00 iops : min170000, max205000, avg198750.00 cpu : usr12.34%, sys28.76%, ctx1023456, majf0, minf0 IO depths : 10.1%, 20.1%, 40.1%, 80.1%, 160.2%, 321.3%, 6498.2%, 1280.0% submit : 00.0%, 4100.0%, 80.0%, 160.0%, 320.0%, 640.0%, 640.0% complete : 00.0%, 4100.0%, 80.0%, 160.0%, 320.0%, 640.0%, 640.0% issued rwts: total5242880, 0, 0, 0 short0, 0, 0, 0 dropped0, 0, 0, 0 latency (usec): target0, window0, percentile100.00%, depth64逐块看第一行err 0是错误计数。任何非零值都要立刻停下来查说明有 IO 失败了后面的数字全部不可信。read: IOPS199k, BW777MiB/s是汇总值。注意括号里的20.0GiB/26371msec这是总数据量和总耗时。用这个反算20.0GiB / 26.371s ≈ 776MiB/s和前面报的对得上说明统计没出错。slat/clat/lat三个延迟。slat是提交延迟从 fio 决定发起到真正下发给内核的时间一般是纳秒级太大说明用户态卡住了。clat是完成延迟从下发给内核到 IO 完成这是真正反映设备能力的那一项。lat slat clat。看设备性能只看clat被slat污染的总延迟会掩盖真实问题。clat percentiles是延迟分布第 5.2 节展开。bw和iops的 min/max/avg反映的是运行期间的波动。如果 min 和 max 差了两倍以上说明负载不稳定或者设备在热降速只有 avg 是有误导性的。IO depths这一行极其关键它告诉你实际生效的队列深度分布。上面这份数据里 98.2% 的时间跑在深度 64说明iodepth64确实生效了。如果这一行显示 99% 集中在深度 1那你设的iodepth就是摆设多半是引擎选错了。issued rwts里的short和dropped也必须是 0。short表示实际读写量少于请求量dropped表示因为队列满被丢弃的 IO。两者非零都说明测试环境有问题。cpu那一行里的sys28.76%在压测高 IOPS 设备时经常成为瓶颈。如果 sys 跑到 90% 以上你测的其实是 CPU 的下发能力不是磁盘的能力。这时候要减少numjobs、简化统计项或者换用开销更低的引擎。5.2 百分位延迟比平均值重要得多平均值是延迟统计里最会骗人的指标。一份数据里 99% 的 IO 用了 50 微秒1% 的 IO 用了 50 毫秒平均值算下来是 550 微秒——看起来还能接受但那 1% 的慢请求在业务侧就是偶尔的超时、偶发的卡顿。看百分位的正确姿势是找拐点。上面那份数据50 分位 65 微秒90 分位 103 微秒99 分位 178 微秒99.9 分位 372 微秒。从 50 到 99 分位涨了不到 3 倍这属于健康的长尾。如果 99.9 分位突然跳到 5 毫秒甚至 20 毫秒那就是典型的长尾拖尾通常意味着设备在做 GC、有坏块重映射、或者底层有别的租户在抢资源。判断标准我给一个经验值99.9 分位延迟不超过中位数的 10 倍算健康。超过 20 倍就要查原因超过 50 倍基本可以判定这块盘不适合延迟敏感业务。还有一个小技巧把stdev和中位数对比。上面clat的stdev41.22微秒中位数 65 微秒stdev 是中位数的 63%说明分布比较宽。如果 stdev 比中位数还大那这条曲线的形状就很成问题了平均值的参考价值接近于零。5.3 判断这份数据是否可信的五个自查点跑完一份数据先别急着写报告过一遍下面这几条。自查项异常表现大概率原因数值量级4K 随机读超过 100 万 IOPS走了页缓存或测试文件在内存盘上队列深度IO depths集中在 1ioengine 用了同步引擎或 iodepth 未生效系统开销sysCPU 超过 80%CPU 成为瓶颈测的是下发能力错误计数err/short/dropped非零IO 失败或队列溢出数据作废波动幅度bw 的 min 和 max 差 3 倍以上设备热降速或测试期间有其他负载干扰再补一条同一份配置至少跑三次。三次的 IOPS 差异在 5% 以内算稳定超过 15% 说明环境有干扰需要排查。我见过太多一次定终身的报告第二次跑就复现不出来。6. 常见问题与避坑实录这一章是纯经验都是文档里不会写、但实际会撞上的东西。6.1 现象与原因速查表现象排查方向处理方式报错fio: engine libaio not loadable编译时缺 libaio 开发库装libaio-devel后重新编译或改ioenginepsync应急报错No such file or directoryfilename的父目录不存在先mkdir -p建好目录fio 不会自动创建多级目录跑起来立刻结束只跑了几秒没写time_based跑完 size 就停了加time_based1数字高得离谱走了缓存或文件在 tmpfs 上加direct1并用df -h确认文件系统类型数字低得离谱bs太小、iodepth没生效、盘上还有别的负载查IO depths分布查iostat的aqu-sz同一个配置两次结果差很远没预热或测试期间有干扰加ramp_time停掉同期任务写到一半速度断崖下跌SSD 的 SLC 缓存耗尽进入稳态写入这是真实表现把runtime拉长让统计窗口覆盖稳态段报错wrong number of jobsjob 文件里某个段缺了必填项检查每段是否有rw和bs结果文件解析失败版本不一致导致 JSON 字段名不同统一各机器的 fio 版本6.2 几个我自己踩过的坑第一个坑size的单位理解错了。fio 的size默认单位是字节size40G表示 40GiB。但如果你在多个 job 里都写了filename和size每个 job 会各自创建一个 40G 的文件四个 job 就是 160G。我曾经在一台只有 200G 空间的机器上写了numjobs8、size40G跑到第四个 job 就报磁盘空间不足。要么用$jobnum变量让每个 job 指向不同文件要么让它们共享同一个文件——后者需要所有 job 用同样的filename并且 fio 会做文件锁处理。第二个坑--output和--output-format搞混了。--outputresult.txt是把输出重定向到文件格式还是文本--output-formatjson --outputresult.json才是产出 JSON。两个参数经常被写成一个结果得到一个后缀是 json 但内容是文本的文件后面的 jq 解析全部报错。第三个坑在虚拟机和云盘上测出来的数字虚高。某次在一台云主机上测 4K 随机写短时间跑出了非常漂亮的 IOPS比同规格的物理机还高。后来把runtime从 60 秒拉到 600 秒前 60 秒的高峰消失了——那是突发额度在起作用额度用完之后回落到一个很平庸的水平。所以任何云上或者虚拟化环境的测试runtime至少给到 300 秒以上ramp_time给到 120 秒让突发额度彻底消耗完再统计。第四个坑没注意 RAID 卡缓存策略。带缓存的 RAID 卡如果没开写穿模式测出来的写性能其实是卡上内存的性能比盘快一个数量级。做法是测试前确认卡的写策略测真实落盘性能时切到写穿测业务实际感受时保持回写但要知道数字里包含了缓存。第五个坑忘了看latency那一行。fio 支持用--latency_target做延迟控制比如要求 99.9 分位的延迟不超过 1 毫秒。开了这个参数fio 会动态调整队列深度来满足延迟目标。有一次同事抄了一份带这个参数的配置去测结果 IOPS 数字比预期低很多排查半天才发现是延迟目标卡住了队列深度。看到输出最后一行有latency (usec): target...就说明这个功能是开着的。第六个坑忘了清理测试文件。几十 G 的测试文件跑完不删日积月累把盘占满了然后某天监控报警。现在我的 job 文件里都会带一句注释提醒自己在验收后删掉/data/fiotest/。或者干脆把测试目录单独挂一个小分区跑完直接格式化。最后分享一个小习惯我现在所有正式验收都用同一套 job 文件模板模板顶部固定写着这次测试的目的、设备型号、容量、文件系统、预热时长、跑了几轮。半年后翻出来能立刻复现当时的结论也能看出设备性能有没有随着使用出现衰减。fio 本身只是把尺子真正有价值的是那把尺子上留下的刻度记录。