前阵子帮某团队优化一套日志采集系统的存储,测试结果单看峰值很不错,可一上业务就延迟飙升,查了半天,问题出在最基础的IO特征没对齐。这类事在存储运维里太常见了,很多人压测只跑一个fio默认参数,拿到的数字除了发个报告,什么也说明不了。Storage Performance这个领域,真正值钱的不是峰值IOPS,而是搞清楚IO characteristics——你的业务到底在制造什么样的IO模式,存储在这类模式下能跑到什么水平,以及哪些参数是你可以调的。
这篇文章我从实际踩坑的视角,把存储性能测试这事拆开讲明白:IOPS、带宽、延迟之间的关系,随机和顺序读写为什么差别巨大,队列深度怎么影响结果,压测工具怎么选怎么配,实测数据怎么解读,常见的坑怎么排查。适合刚接触存储性能评估的运维、DBA和平台工程师,也适合那些正准备给新系统做存储选型,但不确定该怎么设计测试方案的朋友。
1. 先把业务场景翻译成IO特征指标
1.1 认识三个基本指标:IOPS、带宽、延迟
存储性能评估绕不开三个物理量:IOPS、带宽和延迟。先说IOPS,全称是Input/Output Operations Per Second,每秒能完成的读写操作次数。带宽也叫吞吐量,是每秒能传输的数据量,常用MB/s表示。延迟则是单个IO从发出到完成的时间,常用毫秒或微秒计。
这三者不是独立的,它们的关系就像货运车队。IOPS是每秒能发多少辆车,带宽是所有这些车加起来能运多少吨货,延迟则是每辆车从装货到卸货返回用了多长时间。如果每辆车只装一箱货(4KB小块IO),那吞吐主要靠发车频率,即IOPS;如果每辆车装满满一集装箱(1MB大块IO),吞吐则取决于单次搬运能力,也就是带宽。所以测试时不能只报一个数,必须把块大小一起报出来,否则“IOPS 10万”毫无意义——10万次4KB随机读和10万次1MB顺序读,对存储系统的压力完全不是一个量级。
延迟方面有个容易忽略的点:平均延迟看着正常,不代表系统真的健康。存储控制器内部一旦开始垃圾回收、磨损均衡、快照合并等后台操作,某些IO会被拖到几十甚至上百毫秒,产生“尾部延迟”。这些零星的突发延迟如果落在数据库事务提交路径上,SQL整体耗时会被显著拉长,业务侧感知就是“时不时卡一下”。所以后面我测延迟都会同时记录P50、P99和P99.9,只报平均值等于没说。
1.2 四种IO形态:顺序读、顺序写、随机读、随机写
在压测工具里,读写模式被抽象为四类:顺序读(read)、顺序写(write)、随机读(randread)、随机写(randwrite),再加上它们的组合(rw混合模式)。顺序IO的特点是访问地址连续,底层介质可以预取、可以高效合并;随机IO的地址跳来跳去,对机械盘意味着寻道,对闪存意味着更多的地址映射开销和搬移操作。
真实业务里这些模式都能找到对应场景。顺序写最典型的是日志写入和视频流录入,数据一条条追加到文件末尾;顺序读对应数据仓库的全表扫描、视频回放、冷数据备份恢复;随机读是OLTP(在线事务处理)的主旋律,比如电商系统的订单查询,每次只取几条记录;随机写则出现在数据库的Redo Log之外的更新操作、搜索引擎索引更新、消息队列的持久化落盘等场景。
这四类IO里,随机写通常是最难啃的骨头。拿机械硬盘举例,顺序写可以做到接近200MB/s,随机写4KB小块却可能只有几MB/s,差距几十倍。即使是NVMe固态盘,随机写也会因为闪存必须按块擦除的特性,需要做大量后台搬移,性能受写放大影响明显。所以评估一套存储系统,我会先看它的随机写能力,那是整个系统设计水平的试金石。
1.3 队列深度和并发数决定压力的“形状”
压测时有两个参数经常被混为一谈:iodepth和numjobs。iodepth是单个压测进程提交给存储的IO请求队列深度,意思是这个进程最多同时有多少个IO在飞行中,不需要等前一个完成再发下一个;numjobs则是并发进程数。两者都会增加系统的总并发IO数量,但路径不同,前者考验存储设备的队列处理能力,后者还涉及操作系统调度和多进程/多线程开销。
可以用餐厅来类比:排队点餐的深度相当于队列深度,能同时排队的队伍数量相当于并发数。队列深一点,厨房可以同时备好几份菜,吞吐上去了;但每个人等餐的时间也会增加,延迟自然升高。所以压测时队列深度不是越大越好,得看业务真实能做到多少并发。数据库通常一条SQL请求对应一定数量的并发IO,不会无限增加队列;而备份工具、大数据批量任务则可能把队列拉得很高。
我实际测试时,会固定numjobs=1,单独调整iodepth,这样能把队列深度对IOPS和延迟的影响曲线测出来;然后再固定某个合理iodepth,增加numjobs,观察多并发下的叠加压力。两轮测完,能比较清楚地看出这套存储的饱和点和最佳工作区间。
1.4 平均延迟不够,要看尾部时延P99
一次完整的压测输出里,工具会给出min延迟、平均延迟、p50、p95、p99和max等多个统计值。很多人只看平均延迟,这是最容易踩的坑。平均延迟被大量快速IO平均掉了,一个1000毫秒的超长延迟在1万个1毫秒的IO里只贡献不到0.1毫秒的增量,肉眼完全看不出异常,但业务那边那个慢请求已经拖垮了一个事务。
尾部延迟在存储系统里特别重要,因为上游业务往往有超时设置。比如支付系统要求下单接口P99小于200毫秒,存储层若偶尔出现一次500毫秒的延迟,就会导致少量请求超时失败,用户体验是“偶尔失败一次但查不到规律”。压测时,我会重点观察P99和P99.9在长时间运行时是否稳定,凡是P99曲线出现周期性跳变,大概率是存储内部有后台任务在周期性抢占资源,这点在后面的排查章节会细说。
2. 压测前的方案设计:业务模型决定测试矩阵
2.1 先问三个问题:业务类型、负载模型、目标指标
拿到一套存储设备或一个压测任务,我的习惯是先问三个问题,而不是立刻开跑工具。第一个问题,这套存储要承载什么业务?是数据库重事务负载,还是大数据分析扫描,或者是日志流水写入,不同业务对应的IO形态完全不同。第二个问题,业务的读写比例和块大小大致是多少?这个可以通过观察现有系统的IO统计拿到,比如Linux下的iostat的rrqm/s、wrqm/s、avgqu-sz和平均请求大小,都能给出粗略画像。第三个问题,性能目标是什么?是追求高吞吐,还是低延迟,还是两者兼顾,目标不同,测试标准也不同。
这三个问题背后是同一个逻辑:压测不是跑出一个理论峰值,而是回答“这套存储能不能撑住我的业务”。如果业务是订单交易,我压测时就会用4KB随机读写、读写比例7:3、队列深度控制在8-16;如果业务是日志归档,那测试应该偏顺序写、块大小64KB-256KB、关注持续带宽和长时间稳定性。目标定错了,测出来的数字再好看也没用。
2.2 测试矩阵的设计思路
方案落地时,我习惯先做一张测试矩阵,把不同场景拆成组合,每个组合单独记录结果。矩阵列通常包含:压测场景、块大小、读写模式、读写比例、队列深度、运行时长、关注指标。行则按业务需求划分。
举个例子,一套面向数据库场景的SSD存储,测试矩阵可以这样设计:
| 压测场景 | 块大小 | 读写模式 | 读写比例 | 队列深度 | 运行时长 | 关注指标 |
|---|---|---|---|---|---|---|
| 在线交易IOPS | 4KB | 随机混合 | 70/30 | 16 | 30分钟 | IOPS、P99延迟 |
| 批量导出带宽 | 1MB | 顺序读 | 100/0 | 32 | 30分钟 | 带宽、平均延迟 |
| 日志落盘 | 64KB | 顺序写 | 0/100 | 8 | 30分钟 | 带宽、P99延迟 |
| 峰值冲击 | 4KB | 随机写 | 0/100 | 256 | 10分钟 | IOPS、最大延迟 |
每一种组合都对应一个生产上的问题:“这个场景下单盘能扛多少并发”、“这个场景下两块盘是否线性扩展”或者“这个缓存参数对该场景是否有明显影响”。矩阵做好了,后面测试才有章法,否则就是拿同一组参数反复跑,测完还是一头雾水。
2.3 工具选型和环境准备要点
Linux环境下,fio是我用得最多的压测工具,没有之一。它几乎能模拟所有IO形态,且参数粒度细,能够精确控制队列深度、块大小、ioengine、直接IO模式等。另一个常用工具是vdbench,适合做更复杂的存储虚拟化和多协议场景模拟,但学习成本高一些,日常fio足够。Windows平台则用微软的diskspd,参数风格类似。
环境准备有几个容易被忽略的细节。第一,文件系统要对齐,SSD和RAID卡的strip大小如果和分区起始偏移不对齐,性能会打折扣,通常分区工具默认对齐到1MB即可。第二,测试时用direct=1绕过操作系统页缓存,否则数据先落在内存里,工具测出来的是内存和缓存的速度,不是存储的真实能力。第三,测试文件建议使用单个大文件(如64GB以上),避免触碰到文件系统元数据频繁更新的干扰,也避免存储缓存覆盖全部测试区域导致结果虚高。
2.4 fio配置模板与关键参数解析
下面是我常用的一套fio配置模板,用于4KB随机混合读写测试,压测目标是评估一块SSD在数据库负载下的表现。
[global] ioengine=libaio direct=1 rw=randrw rwmixread=70 bs=4k iodepth=16 numjobs=1 runtime=600 time_based group_reporting filename=/data/fio_test.dat size=64G [test]关键参数逐个说清楚。ioengine=libaio表示使用Linux异步IO接口,这类接口才是真实业务里数据库和高性能应用常用的路径;direct=1绕过页缓存直接对磁盘发IO,确保测的是存储真实能力;rw=randrw表示随机混合读写,rwmixread=70表示读占70%、写占30%;bs=4k是块大小;iodepth=16是队列深度;runtime=600是运行600秒,time_based表示即使提前写完文件也继续跑。
还有个容易被忽略的参数group_reporting,它让多个job的统计结果合并汇总,否则numjobs大于1时结果会散成一堆,读数非常痛苦。size=64G是测试文件大小,建议至少是物理内存的一倍以上,避免文件全部被缓存,也确保测试覆盖足够大的存储空间来体现稳态性能。
3. 完整实测流程:从单点到IO特征画像
3.1 第一步:定基线——单盘随机写实测
真正动手的时候,我一般先跑一轮简单的小块随机写,给整体性能定个基调。比如对一块企业级SATA SSD执行如下命令:
fio --name=randwrite_test \ --ioengine=libaio \ --direct=1 \ --rw=randwrite \ --bs=4k \ --iodepth=32 \ --size=64G \ --time_based \ --runtime=300 \ --group_reporting跑完看结果,重点关注几个字段:IOPS表示每秒IO次数,BW表示带宽(KB/s),clat是完成延迟的统计分布,包括p50、p99、p99.9,max是最大延迟。一个典型结果可能长这样:
write: IOPS=42.1k, BW=164MiB/s clat percentiles (usec): | 1.00th=[ 122], 50.00th=[ 195], 99.00th=[ 417], 99.90th=[ 1413]第一眼我会看IOPS和P99延迟是否在同一个小数量级——如果P99时延是平均时延的几倍甚至十几倍,说明系统尾延迟控制得不好。再看max那一行的最大值,通常比P99.9大不少,这部分通常是偶发的后台操作,可以记录但不能作为主要判断依据。
单盘基线的意义不在于数字高低,而在于给后续所有测试提供一个参照。多盘RAID的性能、控制器缓存开与关的影响、不同队列深度下的表现,都要拿这个基线做对比,才能看出改动到底有没有效果。
3.2 第二步:块大小扫描——找到性能转折点
同一套存储,块大小从4KB扫到1MB,性能表现会在某个尺寸附近出现明显转折。这背后是硬件特性的分界:小块的瓶颈在IOPS上限和地址映射开销,大块的瓶颈在介质带宽和控制器处理能力。
我常用的做法是写一段循环脚本,让fio依次跑bs=4k、8k、16k、32k、64k、128k、256k、512k、1024k,每次固定队列深度,记录IOPS和带宽。然后画一张表:
| 块大小 | IOPS | 带宽(MB/s) | 平均延迟(ms) |
|---|---|---|---|
| 4K | 42000 | 164 | 0.19 |
| 8K | 38000 | 297 | 0.21 |
| 16K | 32000 | 500 | 0.25 |
| 32K | 24000 | 750 | 0.30 |
| 64K | 15000 | 940 | 0.40 |
| 128K | 8500 | 1060 | 0.55 |
注意看64K到128K之间,带宽增长明显放缓甚至持平,说明介质带宽接近上限;而4K到8K,带宽翻倍但IOPS下降,说明IOPS逐渐不再是瓶颈。这条曲线对应用层的意义是:如果你的业务平均请求大小是8K,那带宽和IOPS的平衡点正好在比较理想的位置;如果业务大量产生1MB大块请求,那更高队列深度带来的收益就会很有限。
还有一类重要的转折是随机写下的“写放大”效应。部分固态盘在随机写跨闪存页边界时,会触发内部读改写,实际物理写入量大于逻辑写入量,表现为随机写的带宽远低于同块大小的顺序写。扫描测试时可以加一组顺序写对照,如果两者差距超过3倍,就要特别留意。
3.3 第三步:队列深度阶梯——找到最佳工作点
固定块大小,改变队列深度从1到256,是压测里最有价值的一步。这能看见IOPS和延迟如何随着压力上升而变化,并找到这套存储的饱和点。
举个例子,同一块NVMe盘,4K随机读在不同iodepth下的表现可能如下:
| 队列深度 | IOPS(k) | 平均延迟(us) | P99.9延迟(us) |
|---|---|---|---|
| 1 | 7.8 | 128 | 220 |
| 4 | 25.3 | 158 | 410 |
| 8 | 41.0 | 195 | 730 |
| 32 | 76.8 | 390 | 1800 |
| 128 | 88.2 | 1450 | 6800 |
队列深度从1提到8,IOPS翻了5倍多,延迟只是小幅增加,这是增益区间。到32再到128,IOPS继续涨但幅度很小,延迟却翻了近十倍,这是典型的“收益递减区”。生产环境的使用建议是:如果业务对延迟敏感,把并发控制在8左右;如果追求吞吐且能容忍更高延迟,32是可接受的上限;继续压到128以上,对业务没什么好处,只会把存储打到高延迟区。
这块的经验是,不同厂商不同型号的盘,拐点位置差异很大,不要拿别人的结论套自己的设备,必须自己扫一遍。
3.4 第四步:混合读写模拟——贴近真实负载
纯读和纯写能看清存储单方面的能力上限,但生产环境里读写通常是混在一起的。混合读写对存储控制器的挑战更大,因为读和写会争抢内部资源,也有一些设备会做读写分离优化,实际混合表现和纯测试的结果没有直接关系。
混合测试的关键参数是rwmixread和rwmixwrite,用来指定读写比例。接口型业务偏读,常见70/30;日志型业务偏写,常见20/80。如果业务模型不确定,就做一组梯度:100/0、80/20、60/40、40/60、20/80、0/100,用折线图看性能如何随读写比例变化。
有个容易被忽略的问题:混合读写时,读延迟会受到并发写的影响,出现周期性抖动。这在很多系统上都会发生,比如固态盘垃圾回收期间,写操作占用的带宽明显上升,瞬间拖慢读请求。压测时我会额外记录读请求的P99延迟,而不是只看整体混合结果,否则会把读端的问题掩盖在混合平均值里。
4. 实测中的典型异常与排查思路
4.1 尾延迟周期抬头:后台回收与节流
压测时最让人头疼的现象是延迟曲线周期性升高,二十秒正常,忽然一秒内P99翻十倍,然后又恢复。这种周期性,往往不是网络或接口卡问题,而是存储设备内部的后台任务在“打断”正常服务。
固态盘的垃圾回收是最常见的原因。块必须先擦除才能写入,后台回收进程在空闲时把数据搬来搬去,一旦你的写入压力过大,回收就跟不上,控制器会暂停一部分IO来处理,于是出现周期性延迟尖峰。机械硬盘阵列也类似,一致性校验、磁盘巡检、快照后台同步都会造成抖动。
排查方法分两步。第一步,把压测时长拉长到1小时以上,记录P99的时序曲线,如果锯齿波周期稳定,基本可以认定是后台任务调度;第二步,通过设备的管理接口关掉或错峰安排后台扫描任务,重新测试,如果抖动消失,问题定位就完成了。
4.2 结果忽高忽低:缓存、直写和冷热数据的影响
同样的命令,早上跑一遍和下午跑一遍,IOPS差两三倍,这事我碰过不止一次。多数原因是缓存和冷热数据在作怪。
现代存储设备内部都有DRAM缓存和闪存加速层。测试文件如果小于缓存容量,第一次写完的文件数据可能全部留在缓存里,第二遍跑读就是纯缓存速度,自然“性能爆表”。想要规避,第一是用direct=1绕过操作系统缓存,第二是确保测试文件明显大于设备内部缓存。很多存储厂商会在官网标注缓存大小,测试之前查一下,把文件设到缓存的3倍以上,数据才可信。
还有一个冷数据导致的迷惑现象:测试刚开始速度极快,跑几十秒后断崖式下跌。这是因为初始阶段设备在向闲置闪存块直接写入,不需要任何搬移;持续写了一会儿,闪存可用块减少,后台回收开始介入,性能逐渐回落到稳态。这就是为什么测试至少要跑十几分钟,而不是看头三十秒。
4.3 数字和厂商标称对不上:接口、队列与方法差异
经常有人拿着买盘时的说明书来问我:厂家写的顺序读3500MB/s,为什么我测出来只有2200MB/s?差异通常出在三个地方。
第一是接口链路和驱动。PCIe插槽带宽、NVMe驱动是否开启多队列、BIOS里PCIe链路是否降速跑,都能造成带宽差异。第二是队列深度和并发数。厂商测试往往用多队列、深并发压出最高值,现场单线程浅队列自然达不到。第三是测试工具的校准差异,有的工具默认开启页缓存,结果包含了内存加速,有的工具则统计口径不一样,导致看起来差一截。
现场排查的顺序是:先用lspci确认链路速率,再看系统日志里有没有掉链路和超时事件,接着对比厂商给的测试条件和你的条件,缺什么补什么。说句实在话,接近标称值的80%以上且稳定,这设备在现实环境里已经算正常发挥。
4.4 现场排障速查表
实在没头绪的时候,对照这张表找方向。
| 现象 | 可能原因 | 排查手段 |
|---|---|---|
| IOPS上不去但CPU没跑满 | 队列深度太小、块大小和业务不符 | 逐级加大iodepth观察拐点 |
| 延迟周期性跳变 | 后台回收/巡检/一致性校验 | 拉长测试时间观察P99时序 |
| 随机关卡在极低水平 | 受限于锁或页竞争,单队列能力差 | 多numjobs测试排除单进程瓶颈 |
| 带宽测试远低于标称 | PCIe降速、驱动队列未开启 | 查lspci、驱动参数、固件版本 |
| 多盘无扩展性 | RAID条带过小或热点集中在单盘 | 观察单盘统计,调整条带大小 |
| 测试文件超过缓存后暴跌 | 设备缓存太小或写放大严重 | 加大文件尺寸重测,确认稳态值 |
每次压测结束,我会把这些现象和参数变化整理成一份记录,下一次再遇到类似问题,直接翻记录比对,比临时查手册快得多。
5. 把IO特征用回到生产设计里
5.1 从业务负载描述到存储选型参数
做完一轮IO特征测试,最终目的是指导生产设计。很多选型讨论都在比纸面参数,但真正落到实处的,是把你业务产生的负载描述成一串可度量的特征参数:实际块大小分布是什么、读写比例多少、并发压力高还是低、可容忍的P99延迟是多少毫秒。
举例来说,某生产数据库业务的负载特征是“4KB随机读写为主,读写比7:3,单实例并发IO在20左右,业务要求P99延迟低于5毫秒”。拿着这个描述去对照测试结果,如果某块盘在iodepth=16的随机混合测试里P99是3毫秒,那它留有余量,可用;如果P99已经到8毫秒,那要么换更高规格的设备,要么在架构上做读写分离,把压力降下来。
这里面有个原则:按P99选型,而不是按平均延迟选型。平均延迟再好看,救不了偶尔的慢查询。
5.2 性能到容量的粗略换算
性能测试还能帮你算数量,不只是选型号。假设业务峰值需要2万随机写IOPS,单盘实测稳态随机写5000 IOPS,理论算下来至少4块盘。但我会在此基础上再乘1.3到1.5的冗余系数,因为业务峰值不是持续均匀的,存储设备也有寿命和故障率,留足余量才不至于一有波动整个系统就扛不住。
这个逻辑也适用于容量规划。如果按容量算出配10块盘,但按性能算出至少要16块,那就按16块来配。大多数系统从性能角度配的盘都比容量需求多,反过来情况也有,但相对少见。测试数据这时候就是说服决策层加预算的最有力证据。
5.3 业务侧联动优化才是终局
存储性能不只是存储设备的事,很多时候改业务侧的IO模式,比换设备还见效。压测过程中你会发现,有些看似存储的问题,其实根源在应用的IO模型太糟糕:比如把大量小写操作直接写在每行关键路径上,而不是先合并到缓冲区批量落盘;比如每次都打开关闭文件而不是复用句柄;比如日志直接同步写而没有异步批处理。
实践中最有效的组合是“业务层批量 + 存储层对齐”。应用层把随机小块合并成较连续的中等块(比如把8个4KB小IO合并成32KB),存储层性能往往就能提升一个台阶,因为减少了IOPS压力,同时带宽也能更充分利用。做完这类优化后,再用之前的测试矩阵跑一遍,对比结果,你会看到IO特征曲线整体右移,系统容量和性能余量都变大了。
最后分享一个我常用的习惯:每隔一段时间,就给线上系统做一次IO场景快照测试。不用环境全停,工具限制好文件大小和运行时长,挑低峰期跑,记录下IOPS和延迟曲线的变化。设备是有磨损衰减的,固件升级后行为也会变,平时的基线数据到出问题时就是最重要的对照参考。存储性能这件事,本质上就一句话——不是看它能跑多快,而是看它在你的负载模型下能稳定多久。