基于华为云 FlexusX 四节点集群的云计算全栈实操(三):云硬盘 IO 压测(fio 全场景读懂 IOPS 与吞吐)
基于华为云 FlexusX 四节点集群的云计算全栈实操(三):云硬盘 IO 压测(fio 全场景读懂 IOPS 与吞吐)
前两篇我们搭好了实验室、给 CPU/内存做了体检。本篇把目光投向最容易「翻车」的 storage:云硬盘的 IOPS 和吞吐到底是多少?为什么数据库和对象存储对云盘的要求天差地别?我们用 fio 跑五个场景,把数字扒开。
0. 引子:云上最容易被低估的瓶颈
很多性能问题,最后都落在磁盘上。应用层调得再好,如果云盘 IOPS 不够、尾延迟爆炸,MySQL 的慢查询、Kafka 的刷盘停顿、容器镜像的拉取都会跟着抖。问题是——云盘的性能是「分场景」的:顺序大块读写看吞吐(MiB/s),随机小块读写看 IOPS。买错类型,钱花三倍体验还差。
《深入浅出云计算》把存储分为「块存储 / 对象存储 / 文件存储」。本篇聚焦最底层的块存储(云硬盘),用 fio 量化它的能力边界,并给出选型与成本权衡。
1. 理论:先分清几个容易混的概念
- IOPS(Input/Output Operations Per Second):每秒能完成多少次 IO。小块(如 4KiB)随机读写时,IOPS 是天花板。
- 吞吐(Throughput,MiB/s):每秒搬多少数据。大块(如 1MiB)顺序读写时,吞吐是天花板。
- 二者的关系:
吞吐 ≈ IOPS × 块大小。4KiB 随机读 8000 IOPS ≈ 31 MiB/s;1MiB 顺序读 120 IOPS ≈ 120 MiB/s。块越大,相同 IOPS 下吞吐越高。 - iodepth:单个进程的「在途 IO 深度」。云盘后端是队列,iodepth 太小喂不饱,太大则排队延迟上升。
- numjobs:并发进程数。多进程 + 适当 iodepth,才能压出云盘标称峰值。
一句话:小块看 IOPS,大块看吞吐;要压满,得靠 iodepth × numjobs 把队列喂饱。
2. 实验环境
四节点规格同上篇(8vCPU/16GiB,Ubuntu 24.04.4 LTS)。本次被测盘为各节点的40G 系统盘/dev/vda(注意:是系统盘,非独立数据盘,性能口径需结合这一点看)。压测在 node1、node3 两个节点采样。
==== 云硬盘信息 ==== NAME SIZE ROTA SCHED vda 40G 1 noneROTA=1是 lsblk 对 virtio 块设备的常规标记(不代表真机械盘),SCHED=none说明使用了无队列调度的 virtio 路径。
3. 实操:fio_bench.sh 五个场景
我用scripts/fio_bench.sh一次性跑完五个场景,--direct=1绕过页缓存、测真实盘,libaio异步引擎,time_based+runtime=20取稳态。关键参数如下:
# 顺序写:1M 大块,iodepth=32fio--name=seqwrite--directory=/root/fiotest--ioengine=libaio--direct=1--rw=write\--bs=1M--iodepth=32--numjobs=1--size=2G--runtime=20--time_based--group_reporting# 顺序读:1M 大块,iodepth=32fio--name=seqread--directory=/root/fiotest--ioengine=libaio--direct=1--rw=read\--bs=1M--iodepth=32--numjobs=1--size=2G--runtime=20--time_based--group_reporting# 随机写:4k 小块,iodepth=64,4 进程fio--name=randwrite--directory=/root/fiotest--ioengine=libaio--direct=1--rw=randwrite\--bs=4k--iodepth=64--numjobs=4--size=512M--runtime=20--time_based--group_reporting# 随机读:4k 小块,iodepth=64,4 进程fio--name=randread--directory=/root/fiotest--ioengine=libaio--direct=1--rw=randread\--bs=4k--iodepth=64--numjobs=4--size=512M--runtime=20--time_based--group_reporting# 混合读写 70/30:读占 70%fio--name=randrw--directory=/root/fiotest--ioengine=libaio--direct=1--rw=randrw--rwmixread=70\--bs=4k--iodepth=64--numjobs=4--size=512M--runtime=20--time_based--group_reporting并行下发:
PYTHONPATH=deps python tools/fanout.py n1,n3'bash /root/fio_bench.sh'4. 真实输出(来自 results/03_fio_bench.txt)
node1 (113.47.6.41)
==== [1] 顺序写 bs=1M iodepth=32 ==== write: IOPS=125, BW=126MiB/s (132MB/s)(2550MiB/20298msec) ==== [2] 顺序读 bs=1M iodepth=32 ==== read: IOPS=117, BW=118MiB/s (123MB/s)(2432MiB/20672msec) ==== [3] 随机写 bs=4k iodepth=64 numjobs=4 ==== write: IOPS=8152, BW=31.8MiB/s (33.4MB/s)(653MiB/20508msec) clat percentiles (usec): | 99.00th=[ 968885] ==== [4] 随机读 bs=4k iodepth=64 numjobs=4 ==== read: IOPS=8094, BW=31.6MiB/s (33.2MB/s)(657MiB/20782msec) clat percentiles (usec): | 99.00th=[ 977273] ==== [5] 混合读写 70/30 bs=4k ==== read: IOPS=5655, BW=22.1MiB/s (23.2MB/s)(459MiB/20787msec) write: IOPS=2437, BW=9750KiB/s (9984kB/s)(198MiB/20787msec)node3 (1.94.220.182)
==== [1] 顺序写 bs=1M iodepth=32 ==== write: IOPS=126, BW=127MiB/s (133MB/s)(2551MiB/20141msec) ==== [2] 顺序读 bs=1M iodepth=32 ==== read: IOPS=120, BW=121MiB/s (126MB/s)(2473MiB/20500msec) ==== [3] 随机写 bs=4k iodepth=64 numjobs=4 ==== write: IOPS=8071, BW=31.5MiB/s (33.1MB/s)(653MiB/20700msec) clat percentiles (usec): | 99.00th=[ 968885] ==== [4] 随机读 bs=4k iodepth=64 numjobs=4 ==== read: IOPS=8089, BW=31.6MiB/s (33.1MB/s)(657MiB/20791msec) clat percentiles (usec): | 99.00th=[ 977273] ==== [5] 混合读写 70/30 bs=4k ==== read: IOPS=5651, BW=22.1MiB/s (23.1MB/s)(459MiB/20788msec) write: IOPS=2437, BW=9748KiB/s (9982kB/s)(198MiB/20788msec)汇总对比
| 场景 | 指标 | node1 | node3 |
|---|---|---|---|
| 顺序写 (1M) | 吞吐 | 126 MiB/s (125 IOPS) | 127 MiB/s (126 IOPS) |
| 顺序读 (1M) | 吞吐 | 118 MiB/s (117 IOPS) | 121 MiB/s (120 IOPS) |
| 随机写 (4k) | IOPS / 吞吐 | 8152 / 31.8 MiB/s | 8071 / 31.5 MiB/s |
| 随机读 (4k) | IOPS / 吞吐 | 8094 / 31.6 MiB/s | 8089 / 31.6 MiB/s |
| 混合 70/30 (4k) | 读 IOPS / 写 IOPS | 5655 / 2437 | 5651 / 2437 |
两节点几乎一致,符合系统盘的同质预期。
5. 深度解读(重点)
5.1 吞吐 vs IOPS:同一块盘的两个面
看这张表最直观的结论:大块顺序场景,吞吐约 120 MiB/s、IOPS 只有 ~120;小块随机场景,IOPS 冲到 ~8000、吞吐却只有 ~31 MiB/s。同一个盘,换了块大小,天花板指标就从「吞吐」切换成「IOPS」。
验证公式吞吐 = IOPS × 块大小:随机读 8094 IOPS × 4KiB ≈ 31.6 MiB/s ✓;顺序读 120 IOPS × 1MiB ≈ 120 MiB/s ✓。两张面孔,本质是一回事——盘后端的「并发能力」与「单请求搬运量」的乘积。
5.2 为什么顺序读略低于顺序写(这里反直觉)
一般企业云盘是「写有缓存/写合并、读要落盘」所以写快于读;本环境顺序写 126 MiB/s、顺序读 118–121 MiB/s,读略低于写。这是 40G 系统盘的限速画像——系统盘的定位就是「装系统和偶尔读」,顺序读带宽被刻意压在比写更低的档位。规格选型时若需要高顺序读(如训练数据集加载),应换高吞吐型/SSD 数据盘。
5.3 iodepth 与 numjobs:为什么大块用 iodepth、小块用 iodepth×numjobs
- 顺序 1M 场景:
numjobs=1, iodepth=32。单个大块请求本身就很「重」,一个进程靠 iodepth=32 把队列喂满就够了,再多加进程收益不大。 - 随机 4k 场景:
numjobs=4, iodepth=64。4KiB 请求极轻,单进程即使 iodepth=64 也可能喂不饱后端(请求处理太快、队列空窗)。用 4 个进程 × 64 深度 = 256 条在途 IO,才把 IOPS 压到 ~8000 的天花板。
经验:大块看单进程 iodepth,小块看 进程数×iodepth。压随机小 IO 时,只调 iodepth 不调 numjobs,很容易测出一个「假低点」然后误判云盘不行。
5.4 尾延迟(clat 99th):被忽略的雷
随机写的clat 99th = 968885 usec(≈ 969 ms),随机读99th = 977273 usec(≈ 977 ms)。这是 99 分位的完成延迟,接近 1 秒。均值很漂亮(4KiB 随机 ~8000 IOPS,平均延迟约 0.8ms),但尾部会蹿到近 1 秒。
这说明:这块 40G 系统盘在压力下的长尾延迟极不稳定。对延迟敏感的在线服务(数据库事务提交、KV 读),p99 可能会很难看。根因通常是共享存储后端在队列拥塞时的调度——这也是「系统盘」不适合扛核心数据库的根本原因之一。
5.5 混合 70/30 的意义
真实业务极少是纯读或纯写。混合 70/30 下:读 5655 IOPS、写 2437 IOPS。注意读 IOPS 从纯随机读的 8094 掉到 5655——因为读写互相争用同一个 IO 队列和盘带宽,叠加写放大效应,整体吞吐被写拖慢。这提醒我们:压测别只跑纯读/纯写,混合比例才接近生产。
6. 数据库 / 日志 / 大文件:云盘选型与成本权衡
基于上面的画像,给三类典型负载的选型建议:
| 负载类型 | 关键指标 | 应选云盘 | 成本权衡 |
|---|---|---|---|
| 数据库(MySQL/PG) | 随机 4k IOPS、低尾延迟 | 高 IOPS SSD 云盘(如超高 IO 型),并用独立数据盘而非系统盘 | 贵,但 IOPS/尾延迟直接决定 QPS 与 p99;系统盘跑 DB 是禁忌。 |
| 日志 / Kafka / 写放大型 | 顺序写吞吐 + 一定 IOPS | 高吞吐型或通用 SSD | 顺序写本盘 ~126 MiB/s 可用;量极大时按吞吐计费更划算。 |
| 大文件 / 镜像 / 训练集 | 顺序读吞吐 | 高吞吐型 + 大块读 | 顺序读 ~120 MiB/s 偏弱,需高吞吐盘或并行多盘条带。 |
成本观点:云盘选型的第一原则,是让「负载的瓶颈指标」匹配「云盘的峰值指标」。随机小 IO 的库别买吞吐盘(花钱买用不上的 MiB/s),大文件服务别买 IOPS 盘(花钱买用不上的 IOPS)。另外,系统盘(本环境的 40G vda)只该装系统——任何有性能要求的存储,都请挂独立数据盘。本次 8000 IOPS 的「好看数字」背后是近 1 秒的尾延迟,绝不适合直接扛核心数据库。
5.6 一个常见误判:拿系统盘当数据盘
本环境 40G vda 是系统盘,它有明确的「装系统」定位:顺序读被压在 ~120 MiB/s、随机 IO 尾延迟近 1 秒。很多新手把数据库 data 目录直接放在系统盘,结果 QPS 上不去、偶尔卡顿查不出原因——根子就在这块盘的画像上。正确做法永远是:系统归系统,数据挂独立云盘,按需选高 IOPS / 高吞吐型,并把 IOPS 和吞吐的「突发+基准」额度算进容量规划。
7. 踩坑与排障
--direct=1一定要开:不开会走页缓存,测出的是内存带宽不是盘速,数字虚高。- 不要在系统盘根分区直接写:脚本用
/root/fiotest临时目录并最终rm -rf,避免污染与占满。 - iodepth 过大导致延迟飙升:本文 64 已偏高,若再加大,IOPS 不涨、clat 反而更差,说明已到后端队列极限。
- 多节点采样差异:本次仅 n1/n3 采样即高度一致;正式选型建议覆盖更多节点取中值,排除偶发邻居干扰。
8. 配套脚本
../scripts/fio_bench.sh:本篇五个场景的完整 fio 脚本,可直接fanout下发。../tools/ssh_run.py、../tools/fanout.py:结果采集与并行下发。
9. 小结
本篇用 fio 把 40G 系统盘的真实成色扒开,三条核心结论:
- 吞吐与 IOPS 是同一盘的两个面:顺序 1M 约 120 MiB/s(~120 IOPS),随机 4k 约 8000 IOPS(~31 MiB/s),公式
吞吐=IOPS×块大小完全吻合; - 压随机小 IO 必须用
numjobs×iodepth:单调 iodepth 会测出假低点;本环境 4 进程×64 深度才压出 ~8000 IOPS; - 尾延迟是隐藏雷:随机 IO 的 99th clat 接近 1 秒,系统盘绝不适合扛核心数据库——按负载瓶颈指标选独立数据盘,才是正解。
IaaS 三篇到此收官(开篇基建、CPU/内存、云硬盘)。下个阶段我们将进入PaaS 层:对象存储、负载均衡、RDS vs 自建,把「计算 + 存储」真正用成服务。
