有没有遇到过这种情况:一台 Linux 服务器用得好好的,突然某个服务变慢了,排查一圈发现 CPU 和内存都正常,最后定位到磁盘。或者你刚买了一块 NVMe 固态,想在装机后看看真实的读写能力是不是和包装盒上标的一致。再或者你准备上线一套数据库,想评估底层存储扛不扛得住业务负载。这些场景都需要一个基本功:Linux 下测试磁盘读写能力。干这行时间久了,你会发现网上关于磁盘测试的教程很多,但大多是复制粘贴一两条dd命令就草草收场。真正到了生产环境,或者要做性能对比选型的时候,那几条命令完全不够用。这篇文章我从实际运维和测试的角度,把磁盘读写测试这件事从头到尾梳理一遍:先搞清楚要测什么,再说工具怎么选,接着讲影响测试结果的底层机制,最后给出一套能直接复现的完整操作。不管你是运维、后端开发,还是自己折腾 NAS 和家庭服务器,照着这篇文章的思路去测,结果基本不会走偏。1. 先搞清楚:你到底要给磁盘测什么很多人一上来就敲命令,结果测出来的数据四六不靠。我先泼一盆冷水:磁盘读写能力不是单一数字,它是一组不同维度指标的合集。你要先想清楚自己的场景,再去选合适的测试方法。1.1 磁盘读写能力是一块试金石磁盘读写能力说白了就是存储系统能跑多快、扛多少并发。日常业务中它直接决定了数据库查询快不快、日志能不能及时落盘、文件服务传输稳不稳。这块“试金石”主要拆成三个维度:第一个维度是顺序读写带宽,单位通常是 MB/s。它衡量的是磁盘一次性处理大块连续数据的能力,典型场景是视频监控录像、数据库备份恢复、大数据批处理、文件拷贝。机械硬盘顺序读能到 150MB/s 到 200MB/s 已经算不错,SATA 固态一般在 500MB/s 左右,NVMe 固态动辄 3000MB/s 以上。第二个维度是随机读写 IOPS,单位是次/秒。它衡量的是磁盘处理小数据块随机访问的能力,典型场景是数据库 OLTP 业务、虚拟机磁盘、消息队列。IOPS 才是我们日常体感最关键的数字,服务器卡顿十有八九是随机 IOPS 撑不住。第三个维度是延迟,单位是毫秒或微秒。它衡量的是单个 IO 请求从发出到完成的时间,又细分为平均延迟和分位延迟(比如 p99,意思是 99% 的请求都在这个值以内)。数据库慢查询、应用超时直接和延迟挂钩。很多人在 4K 随机写的场景还在用dd测带宽,那结果当然没有参考价值。所以第一步不是问“怎么测”,而是问“我到底要测哪个维度”。1.2 选对工具比跑满参数更重要明确了测试维度,接下来就是工具选型。我的实际经验是:Linux 下测试磁盘读写,工具听过最多的是三类:dd、fio、hdparm,再加上一个稍微冷门的bonnie++,各自的定位完全不同。dd是最基础的文件拷贝工具,很多教程拿它测速,但它的设计初衷根本就不是性能测试工具,用起来有很多隐藏的坑。fio是专门为 IO 负载测试设计的工具,功能强大到夸张,几乎可以说它是磁盘性能测试的事实标准。hdparm适合快速查看磁盘参数和简单的缓存读取测试,能力有限。bonnie++偏文件系统层面的测试,更旧一些。我的建议是:正式对比和验收用fio,快速摸底用dd也不是不行,但心里要清楚它测的是什么。下面详细拆解每个工具,尤其是为什么fio能成为我的主力工具。2. 工具选型解析:为什么最终我选择了 fio关于工具这一块,我踩过的坑特别多,值得单独展开说说。很多人都是因为看了网上随手抄的dd命令,误把缓存速度当成了磁盘速度,导致后续决策跟着出错。2.1 dd 不是不能用,而是容易误读结果先看一条网上最常见的测试命令:dd if=/dev/zero of=./testfile bs=1G count=1 oflag=direct拆开看:if=/dev/zero表示数据源是零字节流,of=./testfile写到当前目录的测试文件,bs=1G是块大小 1G,count=1只写一块。这条命令测的是顺序写带宽。很多人跑完看到 500MB/s 甚至更高,就觉得磁盘好快,实际上这个结果要打折扣。关键的坑在oflag=direct和省略它的差别。如果不加oflag=direct,数据先写进操作系统的 page cache(页缓存),dd命令可能瞬间就返回了,测出来的是内存写入速度而不是磁盘写入速度。加了oflag=direct绕过缓存直写磁盘,结果才接近真实。可即使这样,dd依然只能测非常粗浅的“理论最大写带宽”,它没法控制队列深度,没法模拟随机小 IO,也没法给出 p99 延迟。拿它测 NVMe 这种高并发设备,结果往往偏低,因为dd默认只发单队列深度的请求,根本喂不饱现代固态。所以我的用法是:dd只用来做快速摸底,比如怀疑磁盘挂了、判断是不是有明显的硬件故障,跑一条几十秒的测试看个大概就行。真要严谨对比,绝对不用dd。2.2 fio 为什么是事实标准我第一次用fio的感觉是:这才是正经的性能测试工具。fio能自定义 IO 引擎、块大小、队列深度、读写比例、运行时长、并发进程数,输出结果里有带宽、IOPS、延迟的各项统计,几乎覆盖了性能测试想要的全部维度。先看一个最典型的fio测顺序读命令,后面第四部分我会完整展开:fio -filename=/data/testfile -direct=1 -iodepth=32 -rw=read \ -ioengine=libaio -bs=1m -size=2G -numjobs=1 \ -runtime=60 -group_reporting -name=seq_read这里每个参数都有它的业务含义:-direct=1绕过 page cache 直写磁盘,测真实磁盘能力;-iodepth=32让每进程同时挂 32 个 IO 请求,模拟高并发;-rw=read表示纯顺序读;-bs=1m用 1MB 大块,适合测吞吐。fio设计上就是按“工作负载”去模拟真实业务,它不会像dd那样只发单请求,而是可以模拟数据库那种 4KB 小随机 IO、视频流那种 1MB 大顺序 IO。生态上,fio也是各大存储厂商做性能宣传时最常用的工具,很多云厂商公布的磁盘性能指标都是基于fio跑出来的,大家用同一个基准才能横向对比。fio本身的性能消耗不小,跑测试时它自身也会吃 CPU,但相比它提供的可信度,这个代价完全可以接受。2.3 hdparm 和 bonnie++ 适用边界hdparm适合快速查看磁盘参数,比如缓存大小、是否支持高级电源管理,它还带一个简单的-t测速功能。但它的测速读的是缓存和磁盘的组合速度,对现代 NVMe 的支持也有限,我不是很喜欢把它当性能测试工具用。bonnie++是上世纪留下来的文件系统测试工具,测的是文件创建、删除、字符读写、块读写等,更偏向文件系统层综合表现,现在用得少。比起fio,bonnie++对并发和随机负载的模拟能力差了一截。直接说结论:不管你测 SSD 还是机械盘,测文件系统还是裸设备,主力工具就锁死fio就对了。接下来这部分是整篇文章最核心的干货:影响磁盘测试结果的机制。3. 读懂底层机制:影响磁盘测试结果的几个关键点用错参数测出假数据,通常是没搞懂操作系统 IO 路径上的几个关卡。把下面这几个机制弄明白,你不仅能看懂fio每个参数在干嘛,还能自己判断线上业务的 IO 瓶颈到底在哪。3.1 写缓冲(Page Cache)是最大的“作假”来源Linux 内核默认会把读过的文件数据、写过但还没落盘的数据放在内存里,这部分叫 page cache 或者页缓存。进程写文件时,数据通常先进入 page cache,然后由内核稍后在后台刷入磁盘。这套机制的好处是写入速度快、减少磁盘磨损、提升读写性能,但它的副作用是:如果测试工具不主动绕过,你测到的速度是“内存写速度”而不是“磁盘写速度”。生活化类比:你在一家便利店买东西结账,收银员先收钱,但核账和存钱是后面才做的事。收银员收钱的速度快,但真正把钱存进保险柜的速度不一定快。dd没加direct时,测的是收银员收钱的速度;加了direct,才是保险柜存钱的速度。在fio里对应两个重要参数:-direct=1表示绕过 page cache,让 IO 直接下发到块设备层;-fsync=1表示每个写请求完成后强制调用 fsync,把数据真正刷到磁盘上。如果这两个都没设置,fio测顺序写的带宽可以高出真实值几倍,因为数据全在内存里打转。另一个和缓存相关的变量是测试文件大小。如果测试文件远远小于机器内存,即使加了direct=1,也会遇到这样一种情况:测试文件的一部分其实还留在内存的 cache 里,读的时候直接被 cache 命中,导致读带宽异常高。一般经验是测试文件大小至少要是物理内存的两倍,确保冷缓存场景下走真正的磁盘读。3.2 DirectIO、fsync/fdatasync 和队列深度的微妙关系绕过缓存以后,IO 就进入块设备层,这时候又有一个概念叫队列深度(Queue Depth),对应fio参数里的-iodepth。队列深度可以理解为同一时间有多少个 IO 请求在路上排队,它直接影响设备能不能把并发能力跑满。生活化类比:一个餐厅出菜口,后厨做好菜放到窗口,服务员来取。如果服务员一次只取一道菜(队列深度为 1),后厨再快也出不了太多菜;如果服务员一次取几十道菜(队列深度 32),后厨满负荷运转,吞吐自然上去了。NVMe 固态内部有非常深的硬件队列,能同时处理数百个 IO 请求,所以iodepth对 NVMe 的影响格外明显,对单盘机械硬盘则没那么大,因为机械硬盘本质上是单磁头,物理上就不支持高度并发。再说fsync刷盘。有些业务(比如数据库的事务日志)要求每次写入都必须确认落盘,这时候测的顺序写性能会远低于普通写入性能,因为每次写都要等物理写入完成。fio里可以用下面这两个参数模拟:-fsync=1:每个 IO 完成后刷一次盘,模拟最严苛的持久化要求。-fdatasync=1:只刷数据不刷元数据,比 fsync 略轻一点。我的经验是:测传统关系型数据库落盘能力时,一定要加上-fsync=1或者-fdatasync=1,否则跑出来的数字和真实业务差距能到好几倍。很多人说“我机器顺序写有 1 个 G”,结果数据库一跑就卡,往往是没意识到数据库日志是频繁 fsync 的,两者的模型完全不同。还有ioengine参数。libaio是 Linux 原生的异步 IO 引擎,请求提交后不阻塞进程等待,适合高并发场景;sync引擎则是同步提交,一次就发一个请求,更像是传统程序的写入方式。跑高并发 IOPS 测试时,libaio是唯一能测出设备真实并发上限的方式。3.3 块大小和读写模式怎么配合块大小(-bs)直接决定一次 IO 携带的数据量,它跟测试目标强相关。测顺序大吞吐用大块(512KB、1MB),因为视频流、备份恢复这类业务本来就是把大块数据连续搬移。测数据库随机读写用小块(4K、8K),因为数据库页大小通常是 8KB 或 16KB,而索引和日志等小 IO 往往只有几 KB。读写模式(-rw)覆盖四类基础负载:模式含义对应业务场景read纯顺序读视频点播、文件下载write纯顺序写日志收集、备份写入randwrite纯随机写数据库事务日志、缓存落盘randread纯随机读数据库查询、虚拟机磁盘读取rw 和 randrw混合读写综合业务(可指定读写的比例)实际测磁盘时,我通常会把这几种模式都跑一遍,因为单纯跑一个顺序写是不能反映综合能力的,可很多人偏偏只跑一条dd写命令就当测试完了,这是我特别想纠正的做法。4. 实操:用 fio 跑一套完整的读写性能测试理论讲清楚了,真正动手一点都不难。我在下面给出一套可以直接“抄作业”的完整流程,包含环境确认、命令参数解释和输出解读,每一步都标注了背后的原因。4.1 环境准备:先确认系统状态再动手跑测试之前先确认几件事。第一,用free -h看物理内存总量,测试文件大小要往内存两倍以上设,比如物理内存 16G,测试文件至少 32G。第二,用df -h确认你要测的磁盘挂载点在哪,剩余空间是否充足。第三,如果机器负载本身已经很高,比如 CPU 跑满、线上业务正在高峰期,最好先换个时间,否则测试结果会跟业务 IO 混在一起,还容易影响线上稳定性。测试目录的选择也有讲究。如果你要测整块数据盘的性能,就在这个盘的挂载点下建一个专用测试目录,比如/data/iotest,不要在系统盘/根目录下测。系统盘通常还承担着操作系统读写,它的 IO 路径、分区策略和数据盘不完全一样,测出来的数字不能代表数据盘。生产环境建议在单独的分区或独立目录里测试,测试文件用完直接删除,不要污染业务数据区。需要特别提醒的一点:fio测试会真打真枪地产生大量写 IO,对磁盘健康有轻微影响。SSD 有写入寿命限制,这倒不是大问题,跑几十秒测试的写入量远小于日常使用,但如果拿一块重要数据盘反复跑高强度随机写,还是会有不必要的磨损。测试尽量挑独立测试盘,或者测完就删文件,不要把测试文件留在正式业务分区里过夜。4.2 顺序读写测试:验证最大带宽先测顺序读。完整命令长这样:fio -filename=/data/iotest/testfile \ -direct=1 \ -iodepth=32 \ -thread \ -rw=read \ -ioengine=libaio \ -bs=1m \ -size=4G \ -numjobs=1 \ -runtime=60 \ -group_reporting \ -name=seq_read逐条解释我的选型依据:-filename=/data/iotest/testfile:指定测试文件路径,fio会在这个路径上创建文件并做 IO。-direct=1:绕过 page cache,直通块设备,这保证了测的是磁盘真实能力而不是内存速度。-iodepth=32:队列深度 32,对现代 SSD 来说是个比较合理的起点。NVMe 可以试 64 甚至 128,机械盘用 8 到 16 就差不多了。-thread:使用线程模式代替进程模式,减少上下文切换开销,数字更干净。-rw=read:纯顺序读。-ioengine=libaio:异步 IO 引擎,这是fio在高并发下最常用的引擎。-bs=1m:块大小 1MB。为什么不用小块的 4K 来测带宽?因为小块 IO 的瓶颈在寻址和 IOPS 上,顺序带宽测不出来,用大块才能把设备带的满带宽逼出来。-size=4G:测试文件总大小 4G。先看内存大小调整,最好大于内存两倍,免得测试过程被 page cache 干扰。-numjobs=1:单进程。多进程会叠加并发,适合模拟高并发场景,但单进程跑更能反映单盘基础性能。-runtime=60:运行 60 秒。时间太短结果波动很大,机械盘还要考虑寻道和缓存冷热,60 秒起步比较稳妥。-group_reporting:当有多个 job 跑的时候,汇总显示所有 job 的总和。-name=seq_read:任务名称,输出结果里能看到,方便区分日志。顺序写把-rw=read改成-rw=write就行,其他参数不用动:fio -filename=/data/iotest/testfile \ -direct=1 -iodepth=32 -thread \ -rw=write -ioengine=libaio -bs=1m -size=4G \ -numjobs=1 -runtime=60 -group_reporting -name=seq_write实测下来,一台普通的 NVMe 固态,顺序读能跑到 2500MB/s 以上,顺序写一般也有 1500MB/s 到 2000MB/s。SATA 固态顺序读约 500MB/s,机械盘顺序读写大约在 150MB/s 到 200MB/s。如果明显低于这个区间,基本可以怀疑:线缆/接口速率问题、固件策略问题,或者你的磁盘本身老化了。4.3 随机读写与混合负载:模拟真实业务随机 IO 是数据库和虚拟化场景最关键的性能指标。最典型的是 4K 随机读和 4K 随机写。4K 随机读命令:fio -filename=/data/iotest/testfile \ -direct=1 -iodepth=32 -thread \ -rw=randread -ioengine=libaio -bs=4k \ -size=4G -numjobs=1 -runtime=60 \ -group_reporting -name=rand_read_4k-bs=4k是关键,为什么选 4K?因为它最贴近数据库页和文件系统小 IO 的特征。如果换成-bs=64k,测出来的 IOPS 会低很多,但这不代表设备变差了,只是“块变小了、IO 次数变多了”的自然结果而已。所以横向对比不同磁盘时,必须保证块大小、队列深度、并发数完全一致,否则对比毫无意义。4K 随机写类似,把-rw=randread换成-rw=randwrite:fio -filename=/data/iotest/testfile \ -direct=1 -iodepth=32 -thread \ -rw=randwrite -ioengine=libaio -bs=4k \ -size=4G -numjobs=1 -runtime=60 \ -group_reporting -name=rand_write_4k混合读写用-rw=randrw,并可以用-rwmixread=70控制读写比例,含义是 70% 读、30% 写。这在模拟文件服务器、邮件服务器这类读多写少的业务时很有用。fio -filename=/data/iotest/testfile \ -direct=1 -iodepth=32 -thread \ -rw=randrw -rwmixread=70 -ioengine=libaio -bs=4k \ -size=4G -numjobs=1 -runtime=60 \ -group_reporting -name=mix70_30还有个细节:iodepth对随机读写的 IOPS 影响特别大。同一块 NVMe,iodepth=1时随机读 IOPS 可能只有几千,iodepth=32时能到几万甚至几十万。设备本身没变,变的是你能不能同时把请求喂给它。所以我跑随机 IOPS 测试时,习惯把iodepth从 1 到 64 各测一遍,记录一条曲线,这比单点数据更有说服力。4.4 如何看懂 fio 输出结果fio跑完会输出一大段结果,新手看到容易晕,但其实关键信息就那么几行。我拿一次随机读的典型输出举例,重点看这几个字段:read: IOPS=152k, BW=593MiB/s (622MB/s)(35.6GiB/61439msec) slat (usec): min=2, max=578, avg=14.04, stdev=10.39 clat (usec): min=54, max=2168, avg=195.32, stdev=85.14 lat (usec): min=73, max=2372, avg=209.48, stdev=88.23 clat percentiles (usec): | 1.00th=[ 123], 5.00th=[ 137], 10.00th=[ 145], | 50.00th=[ 177], 95.00th=[ 300], 99.00th=[ 482], | 99.50th=[ 578], 99.90th=[ 793], 99.99th=[ 1044]第一行先说总成绩:IOPS=152k 意思是 15.2 万次随机读/秒,BW=593MiB/s 是带宽。第二行slat是提交延迟,指 IO 从 fio 进程提交到内核的时间,在 libaio 引擎下通常极小。第三行clat是完成延迟,指 IO 从提交到完成的时间,这才是我们最关心的设备延迟。第四行lat是总延迟,约等于 slat + clat。最后那个百分位表更关键:50.00th 是中位数延迟,99.00th 是 p99,99.90th 是 p999,这决定了极端情况下你的服务会不会超时。我判断一块盘能不能支撑数据库业务,不会只看平均值,而是重点看 99 分位和 99.9 分位。比如平均延迟 200 微秒不错,但如果 p99 到了 2 毫秒,对某些要求苛刻的实时服务来说已经偏高了。另一个容易忽略的点:fio结果里有时会出现msec单位,不是usec,看单位要仔细,搞混了数字就差了一千倍。5. 实测翻车记录:常见问题与排查技巧跑磁盘测试看起来是个流水线操作,但实际上翻车的概率不小。我把这么多年踩过的坑汇总成一份排查清单,每条都是真实遇到过的。5.1 缓存和机型负载造成的假数据问题一:测试文件太小,全部命中 page cache。表现是每次跑都破纪录,速度高得离谱,换大文件立刻现原形。解决方式前面提过:测试文件至少为物理内存两倍。如果内存太大没法按两倍设,比如服务器内存 512G,谁也不会为了测试去建 1TB 文件,一个折中办法是把测试文件设成 64G 或 128G,同时把-direct=1打开,效果也基本可信。问题二:测试机器不干净,后台有其他业务 IO 在跑。表现是顺序读跑出了锯齿一样的曲线,一会儿快一会儿慢。我用过最笨但有效的办法:测试前用iotop和iostat确认当前磁盘的 IO 活动,有条件的直接在业务低峰期跑,测试期间不要碰这台机器。问题三:把dd未加direct的缓存写入当成真实速度。我已经强调多次,这里再记录一个我亲眼见过的案例:有人拿dd测一块 SATA 固态,写速度 1.2GB/s,兴高采烈地以为自己买到了高级盘,实际上他测的是跨 NUMA 的内存写带宽。只要加上oflag=direct,速度立刻掉回 450MB/s 左右,这才是实打实的磁盘写速度。5.2 磁盘状态的隐藏干扰测试 SSD 时有一个机械盘没有的现象:测试结果会随着测试时间变长逐渐下降。原因主要两个:SSD 的垃圾回收策略、过热降速。现代 SSD 内部有缓存和映射表,测试刚开始时数据还能写到 SLC 模拟缓存里,跑一段时间缓存耗尽,速度就掉到 TLC 真实写入速度。另外,高性能 NVMe 长时间高负载会发热,主控一旦检测到高温就自动降速。所以测 SSD 的顺序写,我建议runtime至少设 120 秒,观察前 60 秒和后 60 秒的曲线有没有明显跳水。如果前段 2500MB/s、后段 700MB/s,那不是这批盘不行,而是它的持续写入性能和缓存策略决定的,你要根据业务来判断这算不算缺陷。对于机械盘,我踩过另一个坑:测试时间太短,只跑 10 秒。机械盘的外圈寻道时间比内圈快,如果测试数据恰好落在外圈,前 10 秒数字会漂亮得误导人,一拉长就露馅了。测机械盘至少跑 60 秒以上,有条件的最好跑 300 秒,把全盘范围都覆盖到。与此同时,机械盘的随机读写别指望iodepth=32能吃满,它的物理结构决定了并发上去以后反而会因为寻道冲突拖慢速度,用iodepth=8或16比较贴近真实。5.3 环境差异导致的不可对比这是最容易被忽略的一类问题:同一个型号的磁盘,在不一样的机器和系统上跑出截然不同的数字,然后有人就下结论说某某机器有问题,其实未必。我自己就遇到过:同一批盘,一台机器用 ext4 文件系统、默认挂载参数,另一台用了 xfs 且开了nobarrier,结果随机写 IOPS 差了将近 20%。这个差距不代表盘有优劣,而是文件系统日志策略和挂载参数带歪了。所以要横向对比硬件时,必须统一软件环境:同样的内核版本、同样的文件系统、同样的挂载参数、同样的测试工具版本。我在记录测试结果时,会顺手把uname -a、mount | grep /data、fio --version都存到一个结果文件里,这样后续复盘才有据可查。另外云服务器的磁盘测试有个特殊之处:很多云盘的性能表现跟“突发额度”有关,也就是短时间冲高后就会被限流。本地fio能跑出很好看的数据,但长时间跑你会发现带宽悄悄降到某个固定数值。这时候你应该结合云平台的说明文档去理解,而不是骂硬件。测试云盘时加长运行时间是必要的,比如-runtime=300,看持续性能而不是峰值性能。5.4 IOPS 和带宽的“欺骗性”测出高 IOPS 或者高带宽后,还要冷静判断。有一种常见的自欺欺人:追求一个漂亮的峰值指标,却忘了业务需要的是低延迟。比如在队列深度 256 下,4K 随机读 IOPS 能做到 50 万,但同时刻延迟已经飙到几十毫秒。数据库业务根本不会在这个深度的极端负载下运行,这个 50 万 IOPS 对你有意义吗?没有。正确的做法是按业务的真实并发去测。数据库核心业务一般不会用iodepth=256这么深的队列,更多是 8 到 32;视频点播就用大块顺序读,根本不关心 4K IOPS。先定负载模型,再定参数,最后才看数字。否则你就在和一个根本不存在的场景较劲。6. 一点个人经验:测试结果记录的姿势测磁盘不是跑完看一眼就结束的事,尤其是选型对比和性能调优时,一次测试的数据如果没记录好,等于白测。我习惯给每个测试结果建一个固定格式的记录,包含这些要素:测试时间、测试机型号、磁盘型号和固件版本内核版本(uname -r)文件系统及挂载参数(mount输出)测试工具版本(fio --version)本次测试的所有fio参数输出结果的原始文本有了这些记录,无论后面是要向团队汇报、向供应商追责,还是对比多块盘选型,都能拿得出证据,而不是“我记得大概是这个数”。最后分享一个实用小技巧:fio的测试文件可以自己指定路径,测完以后用一条rm删除就行。我习惯把测试文件放在一个专门的目录下,比如/data/iotest/,而不是直接放在根目录或者其他业务目录。这样测试文件有固定归宿,删除时不会误伤其他数据,也不会在测试完成后留下一个十几 G 的垃圾文件占空间。尤其在生产服务器上,这是我对所有线上操作的基本要求:可回退、可清理、不污染。
有没有遇到过这种情况:一台 Linux 服务器用得好好的,突然某个服务变慢了,排查一圈发现 CPU 和内存都正常,最后定位到磁盘。或者你刚买了一块 NVMe 固态,想在装机后看看真实的读写能力是不是和包装盒上标的一致。再或者你准备上线一套数据库,想评估底层存储扛不扛得住业务负载。这些场景都需要一个基本功:Linux 下测试磁盘读写能力。干这行时间久了,你会发现网上关于磁盘测试的教程很多,但大多是复制粘贴一两条dd命令就草草收场。真正到了生产环境,或者要做性能对比选型的时候,那几条命令完全不够用。这篇文章我从实际运维和测试的角度,把磁盘读写测试这件事从头到尾梳理一遍:先搞清楚要测什么,再说工具怎么选,接着讲影响测试结果的底层机制,最后给出一套能直接复现的完整操作。不管你是运维、后端开发,还是自己折腾 NAS 和家庭服务器,照着这篇文章的思路去测,结果基本不会走偏。1. 先搞清楚:你到底要给磁盘测什么很多人一上来就敲命令,结果测出来的数据四六不靠。我先泼一盆冷水:磁盘读写能力不是单一数字,它是一组不同维度指标的合集。你要先想清楚自己的场景,再去选合适的测试方法。1.1 磁盘读写能力是一块试金石磁盘读写能力说白了就是存储系统能跑多快、扛多少并发。日常业务中它直接决定了数据库查询快不快、日志能不能及时落盘、文件服务传输稳不稳。这块“试金石”主要拆成三个维度:第一个维度是顺序读写带宽,单位通常是 MB/s。它衡量的是磁盘一次性处理大块连续数据的能力,典型场景是视频监控录像、数据库备份恢复、大数据批处理、文件拷贝。机械硬盘顺序读能到 150MB/s 到 200MB/s 已经算不错,SATA 固态一般在 500MB/s 左右,NVMe 固态动辄 3000MB/s 以上。第二个维度是随机读写 IOPS,单位是次/秒。它衡量的是磁盘处理小数据块随机访问的能力,典型场景是数据库 OLTP 业务、虚拟机磁盘、消息队列。IOPS 才是我们日常体感最关键的数字,服务器卡顿十有八九是随机 IOPS 撑不住。第三个维度是延迟,单位是毫秒或微秒。它衡量的是单个 IO 请求从发出到完成的时间,又细分为平均延迟和分位延迟(比如 p99,意思是 99% 的请求都在这个值以内)。数据库慢查询、应用超时直接和延迟挂钩。很多人在 4K 随机写的场景还在用dd测带宽,那结果当然没有参考价值。所以第一步不是问“怎么测”,而是问“我到底要测哪个维度”。1.2 选对工具比跑满参数更重要明确了测试维度,接下来就是工具选型。我的实际经验是:Linux 下测试磁盘读写,工具听过最多的是三类:dd、fio、hdparm,再加上一个稍微冷门的bonnie++,各自的定位完全不同。dd是最基础的文件拷贝工具,很多教程拿它测速,但它的设计初衷根本就不是性能测试工具,用起来有很多隐藏的坑。fio是专门为 IO 负载测试设计的工具,功能强大到夸张,几乎可以说它是磁盘性能测试的事实标准。hdparm适合快速查看磁盘参数和简单的缓存读取测试,能力有限。bonnie++偏文件系统层面的测试,更旧一些。我的建议是:正式对比和验收用fio,快速摸底用dd也不是不行,但心里要清楚它测的是什么。下面详细拆解每个工具,尤其是为什么fio能成为我的主力工具。2. 工具选型解析:为什么最终我选择了 fio关于工具这一块,我踩过的坑特别多,值得单独展开说说。很多人都是因为看了网上随手抄的dd命令,误把缓存速度当成了磁盘速度,导致后续决策跟着出错。2.1 dd 不是不能用,而是容易误读结果先看一条网上最常见的测试命令:dd if=/dev/zero of=./testfile bs=1G count=1 oflag=direct拆开看:if=/dev/zero表示数据源是零字节流,of=./testfile写到当前目录的测试文件,bs=1G是块大小 1G,count=1只写一块。这条命令测的是顺序写带宽。很多人跑完看到 500MB/s 甚至更高,就觉得磁盘好快,实际上这个结果要打折扣。关键的坑在oflag=direct和省略它的差别。如果不加oflag=direct,数据先写进操作系统的 page cache(页缓存),dd命令可能瞬间就返回了,测出来的是内存写入速度而不是磁盘写入速度。加了oflag=direct绕过缓存直写磁盘,结果才接近真实。可即使这样,dd依然只能测非常粗浅的“理论最大写带宽”,它没法控制队列深度,没法模拟随机小 IO,也没法给出 p99 延迟。拿它测 NVMe 这种高并发设备,结果往往偏低,因为dd默认只发单队列深度的请求,根本喂不饱现代固态。所以我的用法是:dd只用来做快速摸底,比如怀疑磁盘挂了、判断是不是有明显的硬件故障,跑一条几十秒的测试看个大概就行。真要严谨对比,绝对不用dd。2.2 fio 为什么是事实标准我第一次用fio的感觉是:这才是正经的性能测试工具。fio能自定义 IO 引擎、块大小、队列深度、读写比例、运行时长、并发进程数,输出结果里有带宽、IOPS、延迟的各项统计,几乎覆盖了性能测试想要的全部维度。先看一个最典型的fio测顺序读命令,后面第四部分我会完整展开:fio -filename=/data/testfile -direct=1 -iodepth=32 -rw=read \ -ioengine=libaio -bs=1m -size=2G -numjobs=1 \ -runtime=60 -group_reporting -name=seq_read这里每个参数都有它的业务含义:-direct=1绕过 page cache 直写磁盘,测真实磁盘能力;-iodepth=32让每进程同时挂 32 个 IO 请求,模拟高并发;-rw=read表示纯顺序读;-bs=1m用 1MB 大块,适合测吞吐。fio设计上就是按“工作负载”去模拟真实业务,它不会像dd那样只发单请求,而是可以模拟数据库那种 4KB 小随机 IO、视频流那种 1MB 大顺序 IO。生态上,fio也是各大存储厂商做性能宣传时最常用的工具,很多云厂商公布的磁盘性能指标都是基于fio跑出来的,大家用同一个基准才能横向对比。fio本身的性能消耗不小,跑测试时它自身也会吃 CPU,但相比它提供的可信度,这个代价完全可以接受。2.3 hdparm 和 bonnie++ 适用边界hdparm适合快速查看磁盘参数,比如缓存大小、是否支持高级电源管理,它还带一个简单的-t测速功能。但它的测速读的是缓存和磁盘的组合速度,对现代 NVMe 的支持也有限,我不是很喜欢把它当性能测试工具用。bonnie++是上世纪留下来的文件系统测试工具,测的是文件创建、删除、字符读写、块读写等,更偏向文件系统层综合表现,现在用得少。比起fio,bonnie++对并发和随机负载的模拟能力差了一截。直接说结论:不管你测 SSD 还是机械盘,测文件系统还是裸设备,主力工具就锁死fio就对了。接下来这部分是整篇文章最核心的干货:影响磁盘测试结果的机制。3. 读懂底层机制:影响磁盘测试结果的几个关键点用错参数测出假数据,通常是没搞懂操作系统 IO 路径上的几个关卡。把下面这几个机制弄明白,你不仅能看懂fio每个参数在干嘛,还能自己判断线上业务的 IO 瓶颈到底在哪。3.1 写缓冲(Page Cache)是最大的“作假”来源Linux 内核默认会把读过的文件数据、写过但还没落盘的数据放在内存里,这部分叫 page cache 或者页缓存。进程写文件时,数据通常先进入 page cache,然后由内核稍后在后台刷入磁盘。这套机制的好处是写入速度快、减少磁盘磨损、提升读写性能,但它的副作用是:如果测试工具不主动绕过,你测到的速度是“内存写速度”而不是“磁盘写速度”。生活化类比:你在一家便利店买东西结账,收银员先收钱,但核账和存钱是后面才做的事。收银员收钱的速度快,但真正把钱存进保险柜的速度不一定快。dd没加direct时,测的是收银员收钱的速度;加了direct,才是保险柜存钱的速度。在fio里对应两个重要参数:-direct=1表示绕过 page cache,让 IO 直接下发到块设备层;-fsync=1表示每个写请求完成后强制调用 fsync,把数据真正刷到磁盘上。如果这两个都没设置,fio测顺序写的带宽可以高出真实值几倍,因为数据全在内存里打转。另一个和缓存相关的变量是测试文件大小。如果测试文件远远小于机器内存,即使加了direct=1,也会遇到这样一种情况:测试文件的一部分其实还留在内存的 cache 里,读的时候直接被 cache 命中,导致读带宽异常高。一般经验是测试文件大小至少要是物理内存的两倍,确保冷缓存场景下走真正的磁盘读。3.2 DirectIO、fsync/fdatasync 和队列深度的微妙关系绕过缓存以后,IO 就进入块设备层,这时候又有一个概念叫队列深度(Queue Depth),对应fio参数里的-iodepth。队列深度可以理解为同一时间有多少个 IO 请求在路上排队,它直接影响设备能不能把并发能力跑满。生活化类比:一个餐厅出菜口,后厨做好菜放到窗口,服务员来取。如果服务员一次只取一道菜(队列深度为 1),后厨再快也出不了太多菜;如果服务员一次取几十道菜(队列深度 32),后厨满负荷运转,吞吐自然上去了。NVMe 固态内部有非常深的硬件队列,能同时处理数百个 IO 请求,所以iodepth对 NVMe 的影响格外明显,对单盘机械硬盘则没那么大,因为机械硬盘本质上是单磁头,物理上就不支持高度并发。再说fsync刷盘。有些业务(比如数据库的事务日志)要求每次写入都必须确认落盘,这时候测的顺序写性能会远低于普通写入性能,因为每次写都要等物理写入完成。fio里可以用下面这两个参数模拟:-fsync=1:每个 IO 完成后刷一次盘,模拟最严苛的持久化要求。-fdatasync=1:只刷数据不刷元数据,比 fsync 略轻一点。我的经验是:测传统关系型数据库落盘能力时,一定要加上-fsync=1或者-fdatasync=1,否则跑出来的数字和真实业务差距能到好几倍。很多人说“我机器顺序写有 1 个 G”,结果数据库一跑就卡,往往是没意识到数据库日志是频繁 fsync 的,两者的模型完全不同。还有ioengine参数。libaio是 Linux 原生的异步 IO 引擎,请求提交后不阻塞进程等待,适合高并发场景;sync引擎则是同步提交,一次就发一个请求,更像是传统程序的写入方式。跑高并发 IOPS 测试时,libaio是唯一能测出设备真实并发上限的方式。3.3 块大小和读写模式怎么配合块大小(-bs)直接决定一次 IO 携带的数据量,它跟测试目标强相关。测顺序大吞吐用大块(512KB、1MB),因为视频流、备份恢复这类业务本来就是把大块数据连续搬移。测数据库随机读写用小块(4K、8K),因为数据库页大小通常是 8KB 或 16KB,而索引和日志等小 IO 往往只有几 KB。读写模式(-rw)覆盖四类基础负载:模式含义对应业务场景read纯顺序读视频点播、文件下载write纯顺序写日志收集、备份写入randwrite纯随机写数据库事务日志、缓存落盘randread纯随机读数据库查询、虚拟机磁盘读取rw 和 randrw混合读写综合业务(可指定读写的比例)实际测磁盘时,我通常会把这几种模式都跑一遍,因为单纯跑一个顺序写是不能反映综合能力的,可很多人偏偏只跑一条dd写命令就当测试完了,这是我特别想纠正的做法。4. 实操:用 fio 跑一套完整的读写性能测试理论讲清楚了,真正动手一点都不难。我在下面给出一套可以直接“抄作业”的完整流程,包含环境确认、命令参数解释和输出解读,每一步都标注了背后的原因。4.1 环境准备:先确认系统状态再动手跑测试之前先确认几件事。第一,用free -h看物理内存总量,测试文件大小要往内存两倍以上设,比如物理内存 16G,测试文件至少 32G。第二,用df -h确认你要测的磁盘挂载点在哪,剩余空间是否充足。第三,如果机器负载本身已经很高,比如 CPU 跑满、线上业务正在高峰期,最好先换个时间,否则测试结果会跟业务 IO 混在一起,还容易影响线上稳定性。测试目录的选择也有讲究。如果你要测整块数据盘的性能,就在这个盘的挂载点下建一个专用测试目录,比如/data/iotest,不要在系统盘/根目录下测。系统盘通常还承担着操作系统读写,它的 IO 路径、分区策略和数据盘不完全一样,测出来的数字不能代表数据盘。生产环境建议在单独的分区或独立目录里测试,测试文件用完直接删除,不要污染业务数据区。需要特别提醒的一点:fio测试会真打真枪地产生大量写 IO,对磁盘健康有轻微影响。SSD 有写入寿命限制,这倒不是大问题,跑几十秒测试的写入量远小于日常使用,但如果拿一块重要数据盘反复跑高强度随机写,还是会有不必要的磨损。测试尽量挑独立测试盘,或者测完就删文件,不要把测试文件留在正式业务分区里过夜。4.2 顺序读写测试:验证最大带宽先测顺序读。完整命令长这样:fio -filename=/data/iotest/testfile \ -direct=1 \ -iodepth=32 \ -thread \ -rw=read \ -ioengine=libaio \ -bs=1m \ -size=4G \ -numjobs=1 \ -runtime=60 \ -group_reporting \ -name=seq_read逐条解释我的选型依据:-filename=/data/iotest/testfile:指定测试文件路径,fio会在这个路径上创建文件并做 IO。-direct=1:绕过 page cache,直通块设备,这保证了测的是磁盘真实能力而不是内存速度。-iodepth=32:队列深度 32,对现代 SSD 来说是个比较合理的起点。NVMe 可以试 64 甚至 128,机械盘用 8 到 16 就差不多了。-thread:使用线程模式代替进程模式,减少上下文切换开销,数字更干净。-rw=read:纯顺序读。-ioengine=libaio:异步 IO 引擎,这是fio在高并发下最常用的引擎。-bs=1m:块大小 1MB。为什么不用小块的 4K 来测带宽?因为小块 IO 的瓶颈在寻址和 IOPS 上,顺序带宽测不出来,用大块才能把设备带的满带宽逼出来。-size=4G:测试文件总大小 4G。先看内存大小调整,最好大于内存两倍,免得测试过程被 page cache 干扰。-numjobs=1:单进程。多进程会叠加并发,适合模拟高并发场景,但单进程跑更能反映单盘基础性能。-runtime=60:运行 60 秒。时间太短结果波动很大,机械盘还要考虑寻道和缓存冷热,60 秒起步比较稳妥。-group_reporting:当有多个 job 跑的时候,汇总显示所有 job 的总和。-name=seq_read:任务名称,输出结果里能看到,方便区分日志。顺序写把-rw=read改成-rw=write就行,其他参数不用动:fio -filename=/data/iotest/testfile \ -direct=1 -iodepth=32 -thread \ -rw=write -ioengine=libaio -bs=1m -size=4G \ -numjobs=1 -runtime=60 -group_reporting -name=seq_write实测下来,一台普通的 NVMe 固态,顺序读能跑到 2500MB/s 以上,顺序写一般也有 1500MB/s 到 2000MB/s。SATA 固态顺序读约 500MB/s,机械盘顺序读写大约在 150MB/s 到 200MB/s。如果明显低于这个区间,基本可以怀疑:线缆/接口速率问题、固件策略问题,或者你的磁盘本身老化了。4.3 随机读写与混合负载:模拟真实业务随机 IO 是数据库和虚拟化场景最关键的性能指标。最典型的是 4K 随机读和 4K 随机写。4K 随机读命令:fio -filename=/data/iotest/testfile \ -direct=1 -iodepth=32 -thread \ -rw=randread -ioengine=libaio -bs=4k \ -size=4G -numjobs=1 -runtime=60 \ -group_reporting -name=rand_read_4k-bs=4k是关键,为什么选 4K?因为它最贴近数据库页和文件系统小 IO 的特征。如果换成-bs=64k,测出来的 IOPS 会低很多,但这不代表设备变差了,只是“块变小了、IO 次数变多了”的自然结果而已。所以横向对比不同磁盘时,必须保证块大小、队列深度、并发数完全一致,否则对比毫无意义。4K 随机写类似,把-rw=randread换成-rw=randwrite:fio -filename=/data/iotest/testfile \ -direct=1 -iodepth=32 -thread \ -rw=randwrite -ioengine=libaio -bs=4k \ -size=4G -numjobs=1 -runtime=60 \ -group_reporting -name=rand_write_4k混合读写用-rw=randrw,并可以用-rwmixread=70控制读写比例,含义是 70% 读、30% 写。这在模拟文件服务器、邮件服务器这类读多写少的业务时很有用。fio -filename=/data/iotest/testfile \ -direct=1 -iodepth=32 -thread \ -rw=randrw -rwmixread=70 -ioengine=libaio -bs=4k \ -size=4G -numjobs=1 -runtime=60 \ -group_reporting -name=mix70_30还有个细节:iodepth对随机读写的 IOPS 影响特别大。同一块 NVMe,iodepth=1时随机读 IOPS 可能只有几千,iodepth=32时能到几万甚至几十万。设备本身没变,变的是你能不能同时把请求喂给它。所以我跑随机 IOPS 测试时,习惯把iodepth从 1 到 64 各测一遍,记录一条曲线,这比单点数据更有说服力。4.4 如何看懂 fio 输出结果fio跑完会输出一大段结果,新手看到容易晕,但其实关键信息就那么几行。我拿一次随机读的典型输出举例,重点看这几个字段:read: IOPS=152k, BW=593MiB/s (622MB/s)(35.6GiB/61439msec) slat (usec): min=2, max=578, avg=14.04, stdev=10.39 clat (usec): min=54, max=2168, avg=195.32, stdev=85.14 lat (usec): min=73, max=2372, avg=209.48, stdev=88.23 clat percentiles (usec): | 1.00th=[ 123], 5.00th=[ 137], 10.00th=[ 145], | 50.00th=[ 177], 95.00th=[ 300], 99.00th=[ 482], | 99.50th=[ 578], 99.90th=[ 793], 99.99th=[ 1044]第一行先说总成绩:IOPS=152k 意思是 15.2 万次随机读/秒,BW=593MiB/s 是带宽。第二行slat是提交延迟,指 IO 从 fio 进程提交到内核的时间,在 libaio 引擎下通常极小。第三行clat是完成延迟,指 IO 从提交到完成的时间,这才是我们最关心的设备延迟。第四行lat是总延迟,约等于 slat + clat。最后那个百分位表更关键:50.00th 是中位数延迟,99.00th 是 p99,99.90th 是 p999,这决定了极端情况下你的服务会不会超时。我判断一块盘能不能支撑数据库业务,不会只看平均值,而是重点看 99 分位和 99.9 分位。比如平均延迟 200 微秒不错,但如果 p99 到了 2 毫秒,对某些要求苛刻的实时服务来说已经偏高了。另一个容易忽略的点:fio结果里有时会出现msec单位,不是usec,看单位要仔细,搞混了数字就差了一千倍。5. 实测翻车记录:常见问题与排查技巧跑磁盘测试看起来是个流水线操作,但实际上翻车的概率不小。我把这么多年踩过的坑汇总成一份排查清单,每条都是真实遇到过的。5.1 缓存和机型负载造成的假数据问题一:测试文件太小,全部命中 page cache。表现是每次跑都破纪录,速度高得离谱,换大文件立刻现原形。解决方式前面提过:测试文件至少为物理内存两倍。如果内存太大没法按两倍设,比如服务器内存 512G,谁也不会为了测试去建 1TB 文件,一个折中办法是把测试文件设成 64G 或 128G,同时把-direct=1打开,效果也基本可信。问题二:测试机器不干净,后台有其他业务 IO 在跑。表现是顺序读跑出了锯齿一样的曲线,一会儿快一会儿慢。我用过最笨但有效的办法:测试前用iotop和iostat确认当前磁盘的 IO 活动,有条件的直接在业务低峰期跑,测试期间不要碰这台机器。问题三:把dd未加direct的缓存写入当成真实速度。我已经强调多次,这里再记录一个我亲眼见过的案例:有人拿dd测一块 SATA 固态,写速度 1.2GB/s,兴高采烈地以为自己买到了高级盘,实际上他测的是跨 NUMA 的内存写带宽。只要加上oflag=direct,速度立刻掉回 450MB/s 左右,这才是实打实的磁盘写速度。5.2 磁盘状态的隐藏干扰测试 SSD 时有一个机械盘没有的现象:测试结果会随着测试时间变长逐渐下降。原因主要两个:SSD 的垃圾回收策略、过热降速。现代 SSD 内部有缓存和映射表,测试刚开始时数据还能写到 SLC 模拟缓存里,跑一段时间缓存耗尽,速度就掉到 TLC 真实写入速度。另外,高性能 NVMe 长时间高负载会发热,主控一旦检测到高温就自动降速。所以测 SSD 的顺序写,我建议runtime至少设 120 秒,观察前 60 秒和后 60 秒的曲线有没有明显跳水。如果前段 2500MB/s、后段 700MB/s,那不是这批盘不行,而是它的持续写入性能和缓存策略决定的,你要根据业务来判断这算不算缺陷。对于机械盘,我踩过另一个坑:测试时间太短,只跑 10 秒。机械盘的外圈寻道时间比内圈快,如果测试数据恰好落在外圈,前 10 秒数字会漂亮得误导人,一拉长就露馅了。测机械盘至少跑 60 秒以上,有条件的最好跑 300 秒,把全盘范围都覆盖到。与此同时,机械盘的随机读写别指望iodepth=32能吃满,它的物理结构决定了并发上去以后反而会因为寻道冲突拖慢速度,用iodepth=8或16比较贴近真实。5.3 环境差异导致的不可对比这是最容易被忽略的一类问题:同一个型号的磁盘,在不一样的机器和系统上跑出截然不同的数字,然后有人就下结论说某某机器有问题,其实未必。我自己就遇到过:同一批盘,一台机器用 ext4 文件系统、默认挂载参数,另一台用了 xfs 且开了nobarrier,结果随机写 IOPS 差了将近 20%。这个差距不代表盘有优劣,而是文件系统日志策略和挂载参数带歪了。所以要横向对比硬件时,必须统一软件环境:同样的内核版本、同样的文件系统、同样的挂载参数、同样的测试工具版本。我在记录测试结果时,会顺手把uname -a、mount | grep /data、fio --version都存到一个结果文件里,这样后续复盘才有据可查。另外云服务器的磁盘测试有个特殊之处:很多云盘的性能表现跟“突发额度”有关,也就是短时间冲高后就会被限流。本地fio能跑出很好看的数据,但长时间跑你会发现带宽悄悄降到某个固定数值。这时候你应该结合云平台的说明文档去理解,而不是骂硬件。测试云盘时加长运行时间是必要的,比如-runtime=300,看持续性能而不是峰值性能。5.4 IOPS 和带宽的“欺骗性”测出高 IOPS 或者高带宽后,还要冷静判断。有一种常见的自欺欺人:追求一个漂亮的峰值指标,却忘了业务需要的是低延迟。比如在队列深度 256 下,4K 随机读 IOPS 能做到 50 万,但同时刻延迟已经飙到几十毫秒。数据库业务根本不会在这个深度的极端负载下运行,这个 50 万 IOPS 对你有意义吗?没有。正确的做法是按业务的真实并发去测。数据库核心业务一般不会用iodepth=256这么深的队列,更多是 8 到 32;视频点播就用大块顺序读,根本不关心 4K IOPS。先定负载模型,再定参数,最后才看数字。否则你就在和一个根本不存在的场景较劲。6. 一点个人经验:测试结果记录的姿势测磁盘不是跑完看一眼就结束的事,尤其是选型对比和性能调优时,一次测试的数据如果没记录好,等于白测。我习惯给每个测试结果建一个固定格式的记录,包含这些要素:测试时间、测试机型号、磁盘型号和固件版本内核版本(uname -r)文件系统及挂载参数(mount输出)测试工具版本(fio --version)本次测试的所有fio参数输出结果的原始文本有了这些记录,无论后面是要向团队汇报、向供应商追责,还是对比多块盘选型,都能拿得出证据,而不是“我记得大概是这个数”。最后分享一个实用小技巧:fio的测试文件可以自己指定路径,测完以后用一条rm删除就行。我习惯把测试文件放在一个专门的目录下,比如/data/iotest/,而不是直接放在根目录或者其他业务目录。这样测试文件有固定归宿,删除时不会误伤其他数据,也不会在测试完成后留下一个十几 G 的垃圾文件占空间。尤其在生产服务器上,这是我对所有线上操作的基本要求:可回退、可清理、不污染。