做存储验收这些年,我最大的感受是:性能数字漂亮不等于数据没问题。有一次我帮朋友验一批待上线的存储设备,fio跑出来的顺序读带宽、随机写IOPS都相当亮眼,可我把verify=crc32c加进配置文件重跑了一遍,第一批数据就有一块读不回来。从那以后,我做任何设备测试都默认带校验,先证明设备能“忠实地保存数据”,再谈性能。
fio(Flexible I/O Tester)大概是现在最常用的存储压测工具,但很多人只用它跑带宽和IOPS,完全忽略了它内置的数据校验(verify)机制。包括crc32c、crc32、crc16、crc64这些算法在内的校验能力,可以让你在压测的同时确认:写进去的每一个字节,读出来时还对不对得上。这篇文章我会把fio的CRC校验参数、典型配置、结果解读、常见坑,以及怎么把它拓展成一套长期蒸测方案,一次讲清楚。适合正在做存储选型、盘体验收、数据完整性回归测试的朋友参考。
1. 为什么性能测试里必须加一道数据校验
1.1 光看带宽和IOPS会漏掉什么
很多存储测试报告,拿出来的核心指标就三样:顺序读带宽、顺序写带宽、4K随机IOPS。这仨数字代表了设备在理想负载下的传输能力和延迟分布,但它们回答不了一个关键问题:数据是不是真的原样落盘了?
现代存储设备里,静默数据损坏(Silent Data Corruption)是真实存在的事情。控制器固件写错地址、闪存单元漏电导致位翻转、坏块重映射失败、SATA/SAS链路信号干扰,都可能让写操作“成功”完成,但实际落盘的数据已经变了。更麻烦的是,很多设备在读回时也不会主动报错,只是悄悄给你一段错误的数据。
性能测试完全看不到这些。IOPS高只能说明命令处理快,带宽高只能说明数据通路宽,它们都不关心数据的“内容对不对”。只有把数据读出来,跟当初写入的原始内容做一次完整比对,才可能暴露这类问题。
我自己就碰到过一次:某款新盘跑4K随机写,IOPS在同批产品里排第一,结果用fio带上校验之后就露馅了,某段LBA区间持续报CRC mismatch。后面一查是固件地址映射的问题,属于典型的“性能很好但数据会坏”的盘。如果当初只做性能压测,这批盘就稀里糊涂上线了。
1.2 CRC在校验机制里扮演的角色
fio的校验机制,简单说就是:写数据时给每个数据块盖一个“防伪章”,读回来时拆开验证。这个章不只是CRC,而是一个结构化的校验头(header),里面至少包含了:
- 校验值(按你选的算法计算,比如CRC32C)
- 数据块序号和它在文件/设备里的offset信息
- 写入时间戳
- 写入的线程/进程标识
所以fio做校验的时候,能做的事情比“内容一致”更丰富。它还能发现“内容虽然对,但数据放错了位置”的异常——比如block被交换、offset错位,因为校验头里带着位置信息,对不上就会报错。这是单纯拿固定pattern比对做不到的。
CRC本身是什么?你可以把它理解成一种“带权重的指纹”。具体原理是把一段数据看成一个二进制多项式,用预先约定的生成多项式去做模2除法,得到的余数就是CRC码。数据里任何一位发生变化,只要变化后的数据不是生成多项式的整数倍,CRC就能察觉到。CRC32C使用的Castagnoli多项式,在检测较长的突发错误上有很好的表现,这也是它在存储领域被广泛使用的原因之一。
1.3 数据校验适用的场景与边界
fio的CRC校验不是万能药,但覆盖面很广。它至少能在这些场景里派上用场:
- 新设备验收与出厂老化测试:证明设备在满负载写读下没有损坏。
- 批量选型对比:在不同品牌、固件版本之间做数据完整性横向对比。
- 固件或驱动更新后的回归测试:确认升级没有引入写路径的bug。
- 长时间稳定性测试:连续跑几天几夜,看有没有偶发的数据损坏。
但有一个边界要想清楚:fio校验的是“它自己写的数据”,不是你的生产数据。如果你想验证一个普通文件有没有损坏,直接扔给fio去读是不行的,因为它解析不出自己定义的校验头,会直接报错。存量数据的完整性应该交给文件系统的scrub机制,或者用外部哈希工具定期比对。fio更适合在测试阶段做介质验收,这一点后面专门讲。
2. fio校验机制的核心参数:从选算法到定校验节奏
2.1 verify算法选择:crc16、crc32c、crc64之间的取舍
fio支持很多校验算法,最常用的是CRC系列。选择的原则其实很简单:在计算开销可以忽略的前提下,尽量选冲突概率更低、检错能力更强的算法。
| 算法 | 校验宽度 | 典型场景 | 备注 |
|---|---|---|---|
| crc16 | 16 bit | 冒烟测试、极短时间验证 | 冲突概率相对高,不适合大规模数据 |
| crc32 | 32 bit | 常规文件校验 | 兼容性好,通用性强 |
| crc32c | 32 bit | 存储设备验收 | Castagnoli多项式,检突发错误能力强 |
| crc32c-intel | 32 bit | 高吞吐压力测试 | 依赖CPU的SSE4.2指令,老平台不可用 |
| crc64 | 64 bit | 长时间蒸测、超大样本 | 冲突概率极低,校验头略大 |
实际测试里,绝大多数场景直接用verify=crc32c就够了。它在x86平台还有硬件加速版本crc32c-intel,几乎不增加什么CPU开销。只有当测试规模巨大,比如连续写几十个TB、跑很多轮,想进一步降低极端情况下的漏检概率,再考虑crc64。
这里多说一句,CRC计算的开销相对IO本身来说非常小。你压测一块NVMe固态盘,瓶颈几乎永远在设备端,而不是在CPU算CRC上。所以别为了“省性能”去选crc16,省下来的那点CPU时间毫无意义,漏检的代价却可能很大。
2.2 verify、do_verify与verify_backlog的理解
这三个参数经常被混在一起说,但职责完全不同,我拆开讲清楚:
verify:指定在写入时采用哪种校验元数据。写IO时fio会往数据块里塞校验头,并按照选定的算法计算CRC。它管的是“写入时怎么盖章”。do_verify:控制写job在写完数据后,是否自动把已经写过的区域再读一遍做校验。它管的是“写完是否马上回头验”。verify_backlog:控制后台校验的节奏。默认情况下,校验会等到整个写job全部结束才做。设置verify_backlog=2048之后,fio会一边写一边维护一个“已写未验”队列,每当写入的block数达到设定阈值,就触发一次读回校验。
为什么verify_backlog重要?假设被测盘有10TB空间,不设backlog,fio会先一口气写完10TB,然后再从头读一遍。万一数据在第1TB就坏了,你要等10TB全部写完才开始校验,然后才发现问题。这在小规模测试里无所谓,在长时间蒸测里就是灾难。开启backlog,校验和写入交错进行,通常能在写满10TB的早期就暴露出问题区域,节省大量时间。
2.3 块大小、verify_interval与校验头布局
默认情况下,fio在每一个blocksize单位里放一个校验头。如果你的bs=128k,那么这128KB只有一个校验头。一旦这一整块里有任何一个位翻转,fio能发现“这块数据坏了”,但它无法告诉你坏在128KB里的哪个4KB页——对文件系统、RAID来说,定位粒度还是很重要的。
这时就该用verify_interval=4096。设置之后,fio会在每个4KB边界上额外放置校验头,读回时按4KB逐段验证。代价也明显:校验头变多,写的元数据开销变大,测试带宽会略降。
我的个人习惯是,文件系统级别的测试,verify_interval设置成4096;裸设备全盘耐久性测试,先用大的blocksize和比较大的interval做一遍粗筛,发现问题后再针对可疑区域用小块精细复测。没必要一开始就把校验粒度拉细,那样会把很多时间耗在元数据开销上。
另外,iodepth开得很大时,IO完成顺序会乱掉,但这不影响校验的准确性。因为校验头里带着offset信息,fio读到一块数据后,是靠header里的位置信息来配对“这块应该属于哪里”的。如果真的对不上位置,fio会报bad header,而不是简单的CRC mismatch,别把这两种错误混为一谈。
3. 一份能直接改用的校验测试配置与执行流程
3.1 针对文件系统和裸设备的job模板
下面是一份我常用的文件系统校验配置,重点关注verify、do_verify、verify_interval、verify_backlog这几个参数的配合。
[global] ioengine=libaio direct=1 bs=128k rw=write size=100g runtime=1200 time_based verify=crc32c do_verify=1 verify_fatal=1 verify_interval=4096 verify_backlog=2048 verify_backlog_batch=16 group_reporting=1 numjobs=1 [write_verify] filename=/mnt/testdata/verify_file.bin stonewall如果你是直接测裸设备,把filename改成设备路径,比如:
[global] ioengine=libaio direct=1 bs=128k rw=write filename=/dev/sdb size=100% verify=crc32c do_verify=1 verify_fatal=1 verify_interval=4096 verify_backlog=2048 [raw_verify] stonewall注意:裸设备测试会覆盖目标设备上所有数据,操作前一定要确认这个设备可以安全写。我自己一般会在设备名后面加个_test标记,并且先用blkid、lsblk反复核对,避免手滑写到系统盘上。
3.2 完整的两阶段测试流程
光一个job文件跑完,还不算完整的数据校验。我通常把整个流程拆成两个阶段:
阶段A:写数据 + 在线校验。使用上面的配置文件跑一遍。do_verify=1配合verify_backlog,让fio一边写一边校验刚才写入的数据。这一步结束后,相当于每一个块至少被写过一遍,也被验证过一遍。
阶段B:静态纯读校验。把rw改成read,把do_verify继续保持为1,其余参数(blocksize、verify_interval、filename、size)保持不变,重新跑一遍。这一阶段不再写入新数据,而是把刚才写过的区域全部重新读一遍,逐一解析校验头并重新计算CRC。
B阶段我会跑不止一轮。第一轮用顺序读,确保所有区域被完整扫过;第二轮用rw=randread加固定随机种子,把寻道和读干扰也覆盖进去。遇到读干扰(read disturb)比较敏感的设备,往往是这一轮把隐藏问题引出来的。
3.3 测试规模与压测矩阵怎么铺
不是所有验收都必须一上来写满整个盘。根据工作经验,我建议按下面的矩阵来铺测试:
- 小块随机写校验:
bs=4k,rw=randwrite,iodepth=16/32,重点测试小粒度随机写路径上的数据一致性,这也是最容易暴露固件映射问题的方式。 - 大块顺序写校验:
bs=1m,rw=write,iodepth=8/16,模拟典型的大文件搬运场景。 - 混合读写校验:
rw=randrw,rwmixread=70,模拟日常业务负载,校验的同时观察读写混合下的延迟变化。 - 离线纯读校验:上面小节提到的B阶段,顺序读一遍加随机读一遍。
每轮之间最好在job配置里加上stonewall,让各个job之间形成同步,避免上一个job还没结束,下一个job就开跑,把测试区域搞乱。规模上,可以先跑设备容量的10%~20%,确认没有错误后再逐步扩大。一次就全盘写满,万一遇到问题,排查范围会非常大。
4. 校验结果怎么读:日志、错误与数据损坏定位
4.1 正常结果与CRC mismatch的输出格式
校验通过时,fio的日志输出会明确写着校验错误数为0。如果用--output-format=json,结果里会有对应的统计字段,方便脚本化处理。
一旦校验失败,fio的输出大概长这样,我用示意格式展示:
fio: verify failed at file /mnt/testdata/verify_file.bin offset 1048576, length 4096 fio: got 0a 1b 2c 3d 4e 5f ... fio: expected 12 34 56 78 90 12 ...这里的offset告诉你哪块数据对不上,length是校验粒度,got是这次实际读回来的字节,expected是当初记录在校验头里的期望字节。如果报的是bad header,那就不是CRC不匹配的问题,而是fio根本没能从数据里解析出合法校验头——通常意味着这块区域当初并不是用同样配置的fio写入的。
4.2 从文件offset到物理位置
搞清楚offset之后,下一步就是把逻辑位置换算成物理位置,方便后续复测。
如果是裸设备测试,offset本身就是设备上的LBA位置。比如bs=4096校验报错在offset 1048576,那对应的LBA就是1048576 / 512 = 2048。可以这样手动定位那一个扇区的内容:
dd if=/dev/sdb of=/dev/null bs=4096 count=1 skip=256这里的skip=256是用块大小换算出来的:1048576 / 4096 = 256。
但如果被测区域在文件系统里,事情就要复杂一些。文件偏移和物理LBA之间隔着文件系统的分配逻辑,你不能直接用文件里的offset去换算设备扇区。正确的做法是先用filefrag这类工具查出文件占用的物理extent,再映射到设备上的位置。也正因为这个映射问题,我在做介质颗粒度的问题定位时,干脆优先在裸设备上测,省去文件系统这一层干扰。
4.3 区分误报与真实损坏
收到CRC mismatch,第一反应不应该是“盘坏了”。多年的测试经验告诉我,误报的比例其实不低。常见误报原因我已经整理成表:
| 现象 | 常见根因 | 对策 |
|---|---|---|
| 偶发mismatch,重跑又好了 | 多个job并发写同一区域,互相覆盖 | job之间隔离区域,或用stonewall同步 |
| 大面积bad header | blocksize、verify设置和写入时不一致 | 保持读写阶段参数完全一致 |
| 校验通过但掉电后丢数据 | 设备写缓存未落盘 | 加掉电后复校环节 |
| 校验失败且位置随机 | 测试进程之外有其他程序在写该盘 | 测试期间卸载分区,独占设备 |
真正的介质损坏,通常有这些特征:同一offset反复校验稳定报错,并且写入阶段刚写完时校验是好的,过一段时间再读开始报错,或者从写入阶段就一直稳定报错。这种情况下,基本可以判定是物理设备问题,应当隔离区域复测确认后走售后流程。
反过来,如果是“这次报下次不报”,优先去查测试干扰,而不是急着换盘。我之前遇到过很多次,最后发现是后台有个定时任务也在往同一块区域写数据,导致校验头被覆盖。
5. 实测中容易踩坑的几个环节
5.1 页面缓存和direct=1的取舍
这恐怕是新手最容易踩的坑。fio默认写数据会经过操作系统的页面缓存,如果你不开direct=1,校验时读回来的可能不是设备里的数据,而是内存缓存里的副本。等于你校验的是内存,不是盘。
开启direct=1后,fio使用O_DIRECT方式绕过页面缓存,直接和设备打交道。这一步做完,校验结果才有意义。但有一个副作用:O_DIRECT要求IO大小和偏移对齐到设备的扇区单位,在部分文件系统上,小bs(比如512字节)会直接失败。
我的做法是:正常情况下,做数据校验的测试一律开direct=1。只有当你专门想测文件系统缓存路径的行为时,才考虑关闭direct,并明确知道自己测的是什么。
5.2 随机写、块大小与随机种子的连带问题
使用rw=randwrite配合verify时,写阶段和读阶段的随机种子不需要一致,因为fio是靠数据块里的校验头来识别位置的,不依赖随机序列重放。但下面几个参数必须保持一致:
bs:决定了校验头的排布密度和位置。verify_interval:决定了块内校验头的间隔。offset和size:决定了校验的范围。
最容易犯的错误,是写入阶段用了bs=128k,读阶段图省事改成了bs=4k。结果就是fio在4K粒度上去找原来的128K校验头,满屏报bad header。这不是设备坏了,是配置不连续。校验阶段必须严格沿用写入阶段的块大小和校验区间。
还有一个隐蔽问题:如果你只写了文件的前100GB,但后续的纯读校验没有设置size=100g,fio会把后面没写过的地方也读一遍。没写过的区域都是旧数据或垃圾数据,fio自然解析不出合法头,会大面积报错。这类错误会淹没真正的问题,浪费时间定位。
5.3 verify_backlog与iodepth的配合
在线校验的代价是整体吞吐下降。这是正常的——设备既要处理写请求,又要穿插读请求做校验,工作量天然增加了。但iodepth和verify_backlog的搭配如果不合理,会出现校验线程永远追不上写线程的尴尬局面。
我常用的经验值是:iodepth=16/32配verify_backlog=2048,verify_backlog_batch设在16左右。如果发现job迟迟跑不满,或者读IO占比异常高,先检查是不是backlog设得太小,导致频繁触发校验;再检查是不是iodepth开得过大,造成大量IO同时在队列里排队。这两者的比例,需要根据设备和负载类型微调,没有固定的万能值。
另外,多线程测试里每个线程独立维护自己的校验进度。用numjobs=8跑的时候,错误日志会带上线程ID,定位时先看是哪个job报的,再去查它对应的测试区域,别从全局错误数上瞎猜。
5.4 阵列卡/RAID透写与一致性问题
测企业设备时,阵列卡的write-back缓存是个大麻烦。很多阵列卡默认开write-back,写数据先落缓存,后台再慢慢回写到盘片。这时候你做在线校验,读回来的数据可能还是缓存里的版本,校验自然通过。但一旦断电,缓存没来得及回写,数据就丢了。
应对办法有三个层次:
- 在阵列卡管理界面把测试卷改成write-through,牺牲一部分性能,换取“真实落盘”的保证。
- 不做任何特殊配置,但测试结束后必须做一次掉电复校。
- 如果阵列卡支持透写模式,优先透写,再配合fio的普通校验。
如果你在虚拟化环境里做测试,还要注意宿主机和虚机之间的缓存层级。fio跑在虚机里,看到的是虚拟磁盘接口,不代表真实的物理介质状态。这种情况下,更合理的做法是把fio直接跑在宿主机的裸设备或者控管系统层面,减少中间层干扰。
6. 更严苛的验证方案:多轮蒸测与掉电恢复
6.1 多轮不同pattern的交叉验证
跑一遍写读校验只能证明“某一时刻这一块区域是好的”。存储介质的问题往往是渐进式的,有的闪存单元一开始没问题,写了几次之后才出现编程干扰;有的磁介质是读多了才出现retry。所以更可靠的验证方式是多轮交叉测试。
最常见的方法是跑多轮,每轮换一种数据pattern。建议的顺序:
- 第一轮全
0x00填充 - 第二轮全
0xff填充 - 第三轮
0xaa、0x55交替 - 第四轮用外部生成的文件作为pattern源
全0和全1对闪存介质尤其有效,容易暴露编程干扰和坏块影响;固定交替pattern则能覆盖相邻单元之间的信号耦合问题。不同pattern都跑过一遍,比单用随机数据跑一百遍更有说服力。
fio支持通过verify_pattern=来指定写入的数据pattern,比如verify_pattern=0xff就是全0xff,也可以指定外部pattern文件。每轮跑完后,记录这一轮的错误数,不能只看最后结果。有的错误一旦被后续覆盖写入,表面上又变“正常”了,其实是数据在反复被重写,掩盖了介质的不稳定。
6.2 掉电恢复与verify_state_save/load
长时间蒸测最怕中途断电。几十个小时的测试,跑了二十小时突然断电,从头再来一遍,非常折磨人。fio有一个专门应对这个场景的机制:校验状态保存和加载。
在job配置里加上这几个参数:
verify_state_save=1 verify_state_load=1 verify_state_path=/path/to/state_dirfio会把当前写到哪、校验到哪的状态信息保存到verify_state_path指定的目录。中途断电或进程被杀之后,重新启动fio并保持job名称、文件路径一致,它就能从上次保存的进度继续,而不是从头再来。
这里有一个关键经验:状态文件必须放到被测设备以外的路径。如果状态文件本身放在被测盘上,而你要校验的数据又覆盖了状态文件所在区域,那状态就废了。我一般放在系统盘或者独立的小分区上,路径单独规划好。
掉电复校的真正价值,不只是续跑省时间。很多设备的问题在平稳运行时不出现,只有经过突然断电、缓存未落盘、元数据不完整这类恶劣状态,才会暴露。所以做完正常蒸测后,我会再做一次“写满 → fio正常结束 → 直接断电重启 → 纯读校验”的流程,这比跑一百遍正常读写更能说明问题。
6.3 fio校验的边界与替代工具
最后把边界说透:fio的verify机制只适用于它自己定义的带校验头的数据格式。你让它去读一个普通的数据库文件、一个视频文件,它解析不出合法头,只会报错。所以fio适合做“介质验收”,不适合做“存量数据巡检”。
存量数据的完整性验证,应该靠另外一套工具组合:
- 文件系统自带的校验机制:比如btrfs的scrub、zfs的scrub,能扫描所有元数据和数据块,并给出校验错误报告;
- 外部哈希比对:对文件算一次SHA-256,定期重算比对;
- 磁盘SMART自检:查看设备当前的错误计数、重映射扇区数等指标。
我自己做设备验收的固定组合是:先用fio带CRC32C校验做压力测试,确认介质本身可靠;然后交付后依靠文件系统scrub做日常巡检。这两者是配合关系,不是替代关系。
做存储测试这几年,我最大的体会就是:先证明数据是对的,再谈性能有多快。顺序一旦反了,漂亮的IOPS数字很容易带偏你的选型判断。如果你手头正好有一批设备要做验收,我建议先从bs=4k的随机写加CRC32C校验开始,再跑一轮全盘顺序读校验。这两个能稳定通过,再去铺性能测试矩阵,心里会踏实很多。