服务器硬盘读写速度怎么测试?fio工具参数与结果分析

新上线一台数据库服务器,厂商承诺的磁盘 IOPS 是 6000,业务跑起来却各种超时。你跑了个 dd 测出 1.2GB/s,看起来快得离谱,于是更加困惑——为什么纸面数据很好,实际却慢?问题出在测试方法上:dd 测的是单线程大块顺序写,而且很可能测的是操作系统页缓存,跟数据库那种"成千上万个 4KB 随机读"的真实负载毫无关系。要科学地测量服务器硬盘读写速度,业界的标准工具是 fio(Flexible I/O Tester)。本文讲清 fio 的参数怎么配、结果怎么看、以及哪些数字才是判断磁盘好坏的关键。

一、测试前的三条铁律

在用 fio 之前,先确立三条原则,否则测出来的数字没有任何参考价值。

1.1 必须绕过页缓存(direct I/O)

Linux 默认会用空闲内存做页缓存,写操作先落到内存就返回。如果你的测试文件比内存小,或者用默认的 buffered 模式,测到的其实是内存速度,动辄几 GB/s。fio 通过 --direct=1 使用 O_DIRECT,绕过页缓存直写设备。等价的选项是 --buffered=0。

1.2 测试文件要远大于内存

即使开了 direct I/O,如果测试文件太小、且被缓存过,仍可能出现偏差。经验做法是 --size 设为内存容量的 2 倍以上,且不低于 2GB。查看内存: free -g。

1.3 明确你要模拟的业务模型

磁盘性能不是单一数字,而是"块大小 + 读写比例 + 队列深度"三个变量的组合结果。数据库的 OLTP 负载是 4KB/8KB 随机读写、队列深度较高;日志和备份是 1MB 顺序写;视频点播是 1MB 顺序读。测试前先问自己:我要测的是哪一种?

# 安装 fio
yum install -y fio        # RHEL/CentOS/Rocky
apt-get install -y fio    # Debian/Ubuntu

# 测试前先确认目标盘与剩余空间,避免把系统盘写满
lsblk
df -h /data
free -g

# 确认文件系统类型(影响可选的 ioengine)
stat -f -c %T /data

二、fio 核心参数速查

fio 的参数非常多,但日常只需要掌握下面这些。理解它们的含义,比记住命令行模板更重要。

参数作用常用取值与说明
--filename测试目标文件路径或裸设备,如 /data/testfile、/dev/sdb。测裸设备会破坏数据,慎用
--ioengineI/O 引擎libaio(Linux 原生异步,最常用);sync(同步,用 pread/pwrite);psync;libaio 需内核支持
--direct是否绕过缓存1=O_DIRECT 直写设备;0=走页缓存。压测必须设 1
--rw读写模式read/write 顺序;randread/randwrite 随机;randrw 随机混合;rw 顺序混合
--rwmixread混合读占比与 randrw 配合,70 表示 70% 读 30% 写
--bs单次 I/O 块大小随机场景 4k/8k;顺序吞吐场景 1m。可用 4k,8k 逗号分隔做混合分布
--iodepth单任务队列深度同时 inflight 的 I/O 数。1=测纯延迟;32/64=测极限 IOPS
--numjobs并发任务数模拟多线程业务。总并发 ≈ iodepth × numjobs
--size单任务数据量应远大于内存,如 4G、20G
--runtime运行时长(秒)建议 ≥60 秒,太短无法反映稳态性能
--time_based按时间而非数据量结束与 runtime 配合,跑满指定时间
--group_reporting汇总多任务结果多 numjobs 时输出总 IOPS 而非每个 job 单独输出
--name任务名结果输出中的标识,如 randread
--output结果输出到文件配合 --output-format=json 可做自动化比对

三、四组典型场景的标准测试命令

3.1 场景一:4K 随机读(数据库查询型负载)

fio --name=randread --filename=/data/fio.test --ioengine=libaio \
    --direct=1 --rw=randread --bs=4k --iodepth=32 --numjobs=4 \
    --size=4G --runtime=120 --time_based --group_reporting

这组参数模拟的是并发查询时的索引扫描。重点看 IOPS 和读延迟的 P99。

3.2 场景二:4K 随机写(事务提交、redo 日志)

fio --name=randwrite --filename=/data/fio.test --ioengine=libaio \
    --direct=1 --rw=randwrite --bs=4k --iodepth=16 --numjobs=4 \
    --size=4G --runtime=120 --time_based --group_reporting \
    --fsync=1

注意比随机读多加了 --fsync=1:它表示每次写后调用 fsync 刷盘。如果业务是数据库,事务提交必须落盘,那么不加 fsync 测出来的写性能是虚高的——数字很好看,但数据安全性完全不同。想要更贴近 MySQL/PostgreSQL 的 redo 写入模型,还可以用 --fdatasync=1。如果测的是纯粹的硬件能力(与其他机器横向对比),则不要加 fsync。

3.3 场景三:1M 顺序读写(吞吐、备份、大文件)

# 顺序读吞吐
fio --name=seqread --filename=/data/fio.test --ioengine=libaio \
    --direct=1 --rw=read --bs=1m --iodepth=8 --numjobs=1 \
    --size=8G --runtime=60 --time_based --group_reporting

# 顺序写吞吐(把 rw 换成 write,bs 换成 1m)
fio --name=seqwrite --filename=/data/fio.test --ioengine=libaio \
    --direct=1 --rw=write --bs=1m --iodepth=8 --numjobs=1 \
    --size=8G --runtime=60 --time_based --group_reporting

这一组看的是 BW(带宽),单位为 MiB/s。机械盘在这个场景通常能达到 100~200 MiB/s,SATA SSD 约 400~550 MiB/s,NVMe 则可到数 GB/s。具体数值取决于介质与接口,不要跨介质类型硬比。

3.4 场景四:混合随机读写(真实业务模型)

fio --name=mixed --filename=/data/fio.test --ioengine=libaio \
    --direct=1 --rw=randrw --rwmixread=70 --bs=8k \
    --iodepth=32 --numjobs=4 --size=4G --runtime=180 \
    --time_based --group_reporting

输出里会分别给出 read 和 write 两组的 IOPS/BW/lat,对读写分离的业务(如主从库)很有参考价值。

四、结果判读:看懂 fio 输出里的每个数字

fio 跑完会输出一大段文本,核心是三部分。

4.1 IOPS 与 BW

类似 read: IOPS=24.5k, BW=95.8MiB/s (100MB/s)(5748MiB/60001msec)。IOPS 是每秒完成次数,BW 是带宽。两者可以互相换算:BW ≈ IOPS × bs。所以 4K 随机读要 24000 IOPS 才能达到约 94 MiB/s,而 1M 顺序读只需要 95 IOPS。这就是为什么"IOPS 高"和"速度快"不能直接画等号——块大小不同,主导指标就不同。

4.2 延迟三分法:slat、clat、lat

这是最容易看错的地方。fio 把一次 I/O 拆成三段计时:

  • slat(submission latency):提交延迟,从 fio 发起 I/O 到真正把请求交给内核的时间。通常只有几微秒,如果很高说明 CPU 或内核队列已经饱和。
  • clat(completion latency):完成延迟,从请求交给内核到 I/O 真正完成的时间。这才是反映磁盘真实性能的关键指标。
  • lat(total latency):总延迟 = slat + clat(在同步引擎下还包括排队时间)。

看磁盘快慢,主要看 clat 而不是 lat,因为 slat 里混入了 fio 自身的调度开销。

4.3 延迟分布百分位(比平均值重要十倍)

clat percentiles (usec):
 |  1.00th=[  221],  5.00th=[  262], 10.00th=[  293], 20.00th=[  338],
 | 30.00th=[  379], 40.00th=[  420], 50.00th=[  462], 60.00th=[  510],
 | 70.00th=[  570], 80.00th=[  660], 90.00th=[  824], 95.00th=[ 1037],
 | 99.00th=[ 2147], 99.50th=[ 3294], 99.90th=[ 8455], 99.95th=[12649],
 | 99.99th=[23173]

上面是一段真实的延迟分布。读法:50.00th = 462 微秒表示一半的请求在 462μs 内完成;99.00th = 2147 微秒表示 1% 的请求超过了 2.1 毫秒;99.99th = 23173 微秒表示万分之一的请求超过了 23 毫秒。

判读标准:

  • P99.9 与 P50 的比值在 10 倍以内:延迟一致性良好,适合放数据库。
  • P99.9 是 P50 的 20 倍以上:存在严重尾延迟抖动。可能原因包括:云硬盘被其他租户抢占、后端正在做副本重建、SSD 正在做垃圾回收(GC)、RAID 卡电池学习(BBU learn)导致写缓存被临时关闭。
  • P99.99 出现几十毫秒级尖峰:对 OLTP 数据库来说,这意味着每万个查询里就有一个要卡几十毫秒,前端体验就是"偶尔转圈"。

五、交叉验证:fio + iostat + ioping

只靠 fio 一个工具还不够,配合系统级工具能定位更多问题。

# 压测的同时另开一个终端,观察设备级实时指标
iostat -xmt 2
# 重点列:
# r/s w/s      每秒读写请求数(即 IOPS)
# rkB/s wkB/s  每秒读写数据量
# await        平均 I/O 响应时间(含排队),ms
# r_await/w_await 读写分开的平均响应时间
# %util        设备繁忙度,接近 100% 即饱和
# aqu-sz       平均请求队列长度

# ioping 用于快速测纯延迟(类似网络 ping)
ioping -c 20 /data
ioping -D -c 20 /data      # 显示每次请求的延迟

%util 接近 100% 说明设备已到瓶颈;此时如果 aqu-sz 也很大(比如超过 8),说明请求严重积压。机械盘上 await 持续超过 20ms、固态盘持续超过 5ms 通常意味着需要优化或扩容。注意 %util 对 NVMe 这类多队列设备的意义有限(它按单设备统计,无法反映多队列并行度),应以 IOPS 与延迟为准。

六、标准压测流程清单

  1. 确认测试目标:明确业务是随机为主还是顺序为主、块大小、读写比例。
  2. 确认磁盘剩余空间充足,不要在系统盘或生产数据目录上跑写测试。
  3. 确认内存大小,设置 --size 为内存的 2 倍以上。
  4. 先跑 --iodepth=1 --numjobs=1 测基准延迟,再逐步加大 iodepth 找性能拐点。
  5. 按场景跑随机读、随机写、顺序读、顺序写四组,每组 --runtime 不小于 60 秒。
  6. 数据库场景的写测试加 --fsync=1 或 --fdatasync=1,保证结果可比。
  7. 记录 IOPS、BW、clat 的 P50/P99/P99.9,重点看尾延迟而非平均值。
  8. 用 --output=result.json --output-format=json 存档,方便扩容前后横向对比。
  9. 压测期间并行跑 iostat -xmt 2,观察 %util、await、aqu-sz 是否与 fio 结论一致。
  10. 测试完成删除测试文件:rm -f /data/fio.test,释放空间。

小结与避坑建议

测试服务器硬盘读写速度的核心不是"跑个命令",而是用正确的模型测出可比的数字,并从延迟分布里读出稳定性。记住:IOPS 是吞吐视角,延迟是体验视角,两者必须一起看;块大小决定了该看哪一个;direct=1 和足够大的 size 是数据可信的前提。

常见误区:

  • 用 dd 测磁盘性能:dd if=/dev/zero of=test bs=1M count=1000 默认走缓存,且是单线程顺序写,结果既不代表 IOPS 也不代表真实延迟。用 dd ... oflag=direct 至少能绕过缓存,但仍无法模拟并发。
  • 忘了 --direct=1,测出几 GB/s 的内存速度,据此做的容量规划全部错误。
  • 只看平均延迟。业务卡顿由尾延迟决定,P99.9 才是关键指标。
  • 在生产盘的剩余空间上跑大文件测试:把磁盘写满导致业务故障是真实发生过的事故,务必先确认剩余容量。
  • 拿不同块大小的结果横向对比:4K 随机读 5000 IOPS 和 1M 顺序读 5000 IOPS 是完全不同的性能等级。
  • 忽略 CPU 瓶颈:高性能 NVMe 上,单核 CPU 可能先于磁盘被打满。此时 fio 会显示 slat 升高。解法是用 --numjobs 分摊到多核,或使用 io_uring 引擎降低开销。

最后给出一个可量化的验收思路:把压测结果按"目标 IOPS + 目标 P99 延迟"两个条件同时约束。例如"4K 随机写 IOPS ≥ 3000 且 P99 ≤ 5ms",只有同时满足才算达标。只满足 IOPS 而尾延迟超标,数据库依然会卡。


本文为存储与硬盘管理系列,fio 参数以实际安装版本为准,可用 fio --version 查看。