第一次在项目里用到SPDK perf,是帮客户验证一批NVMe SSD的极限性能。当时客户拿着fio跑出来的数字问我:“这盘标称500K IOPS,为什么我只能跑到300K?”这个问题很有代表性。fio跑的是内核存储栈下的性能,而盘本身的硬件极限往往要绕过内核才看得到。SPDK perf就是用来做这件事的。
如果你也遇到过“标称性能永远跑不出来”的疑惑,或者正在做SSD选型、固件验证、NVMe-over-Fabric方案压测,这篇文章应该能帮上忙。我会从原理、环境准备、perf参数、数据解读到硬件平台上的各种坑,把SPDK NVMe测试工具perf完整拆一遍。文章偏实战,命令都是我自己用过的,照着操作基本能复现。
1. 为什么测NVMe极限性能,大家非要用SPDK perf
1.1 用户态驱动到底改变了什么
先聊一个很多人忽略的前提:SPDK(Storage Performance Development Kit)并不是一个“测试工具”,而是一套存储开发套件。它最核心的设计,是把传统内核态的存储驱动搬到用户态来做。perf只是SPDK官方提供的一个示例应用,但因为太好用,慢慢成了大家默认的基准测试工具。
传统内核NVMe驱动的IO路径大概是这样的:应用发IO请求,经过VFS、文件系统、块层、内核NVMe驱动,驱动把请求写到设备的提交队列,然后靠中断通知CPU处理完成事件。每一次IO都有系统调用、上下文切换、锁竞争、中断处理的开销。单看每一步都还好,但高并发下这些开销会被无限放大。
SPDK的做法完全不同:应用直接通过用户态驱动操作PCIe设备的MMIO寄存器,把请求提交到NVMe提交队列后,CPU用轮询(polling)的方式不停检查完成队列。没有中断,没有系统调用,没有锁竞争。IO路径上几乎只有“提交请求—轮询完成”这一件事。
用生活化的类比来说:内核驱动模式像去银行大厅排队,办完业务还要等叫号;SPDK模式像是你塞给业务员一张纸条,然后一直站在窗口盯着他,他办好你立刻拿走。省掉了叫号系统的等待时间和被别的业务插队的概率,每笔IO都少了一大截固定开销。
1.2 perf和fio测的不是一回事
很多人第一次接触SPDK perf时会把它和fio做对比,然后纠结“到底该用哪个”。这是理解偏差。fio是通用的IO负载生成器,它走的是Linux内核的IO栈,测的是“操作系统存储栈+硬件”的整体性能。SPDK perf走的用户态驱动,测的是“NVMe设备+用户态驱动”这条最短路径的性能上限。
这两者不是替代关系,而是不同层级的测量工具。我做测试时通常两个都跑:fio的结果用来评估“这盘在这台服务器上实际能用出多少”,SPDK perf的结果用来评估“这块盘本身到底能跑多快”。
两者的差值,恰好就是内核IO栈的额外开销。这个数据对存储方案设计很有参考价值,后面第4章我会专门讲怎么用perf和fio做对比实验。
1.3 这个工具适合谁,不适合谁
SPDK perf的适用场景非常明确,主要是这几类:
- SSD固件开发或验证,需要量化固件在不同队列深度、IO大小下的表现
- 服务器存储选型,想确定某块盘在“最小软件开销”下能提供多少IOPS和带宽
- NVMe-oF方案压测,perf支持RDMA、TCP等传输层,可以直接测NVMe over Fabrics这条链路
- 想深入理解NVMe协议,或者做内核存储栈优化的人
反过来,如果你需要验证文件系统行为、应用层真实延迟,或者跑的是数据库这类复杂负载,SPDK perf就不合适了。它测的是块设备裸性能,不经过文件系统,这和你的生产环境差距很大。工具选型的关键在于“明确知道自己想测哪一层”。
2. 诚实的性能测试从环境准备开始:HugePage、设备接管与CPU隔离
SPDK perf的环境准备比fio麻烦不少,但每一步都有它的道理。我见过太多人在这一步翻车,命令还没跑就报各类错误。其实把环境理顺了,perf本身非常简单。
2.1 HugePage:先给DPDK备好内存池
SPDK从DPDK那继承了一套内存管理机制,IO数据缓冲和DMA映射都从预分配的大页内存池里取。为什么一定要用大页?因为普通4KB页在做DMA映射和IO提交时,TLB(页表缓存)容易被打爆,频繁查页表会把轮询模式的低延迟优势抵消掉。用2MB甚至1GB大页,地址转换开销几乎可以忽略。
配置2MB大页的标准操作如下:
# 预留4096个2MB大页,一共8GB echo 4096 > /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages # 挂载hugetlbfs mkdir -p /mnt/huge mount -t hugetlbfs -o pagesize=2M hugetlbfs /mnt/huge如果你想要更极致的大页,可以在内核启动参数里加hugepagesz=1G hugepages=4,预留4个1GB大页,效果比2MB更好。测试机内存不紧张的话,建议直接上1GB大页。
这里有个实操细节:DPDK跑起来时会检查大页目录的访问权限。如果SPDK是用root跑的,一般没问题;但如果你用普通用户跑,记得把/mnt/huge的权限放开,或者把用户加入相应组,否则会看到“cannot open hugepage”之类的错误。
预留大页失败最常见的原因是内存碎片化,物理内存不够连续的大块区域。释放一些不必要的进程再试,一般能解决。另外,不要指望swap来兜底,大页内存是不能被换出的。
2.2 把NVMe设备从内核驱动“借”过来
默认情况下,Linux内核的nvme驱动会占用NVMe设备,系统盘和普通数据盘都是。SPDK要直接访问PCIe设备的BAR空间,必须先把设备从内核驱动解绑,改绑到vfio-pci或uio驱动上。
SPDK官方提供了脚本,一条命令搞定大部分工作:
cd spdk sudo scripts/setup.sh这个脚本会自动加载vfio-pci驱动,把所有非系统盘的内核驱动设备解绑,重新绑定到vfio-pci。执行完可以用下面的命令确认状态:
sudo scripts/setup.sh status lspci -k | grep -A 2 -i nvme正常情况下你会看到类似“Kernel driver in use: vfio-pci”的输出。此时原来/dev/nvme0n1这样的设备节点会消失,这是正常的,设备已经被“借”走了。
这里有两件事必须提醒:
- 系统盘绝对不能这么操作。如果整台测试机只有一块NVMe系统盘,你需要再加一块独立的测试盘,或者换一台系统装在SATA盘上的机器。
- 用vfio-pci需要有IOMMU支持。BIOS里开启VT-d(Intel平台)或IOMMU(AMD平台),内核启动参数加上
intel_iommu=on iommu=pt。
如果你的老平台BIOS里没有VT-d选项,或者你想绕过IOMMU,可以改用uio_pci_generic驱动。手动绑定方式如下:
# 先确认uio_pci_generic模块已加载 modprobe uio_pci_generic # 用DPDK的绑定脚本手动指定驱动 dpdk-devbind.py -b uio_pci_generic 0000:01:00.0不过uio_pci_generic不支持某些特性,稳定性也不如vfio-pci,能用IOMMU还是优先用IOMMU。这个取舍在后面第5章讲老平台时会再提到。
2.3 用isolcpus给perf留出专用核
SPDK的IO路径是轮询模式,负责轮询的线程必须独占CPU核。如果这个核还被系统调度器拿去做别的事情,线程一被抢占,延迟毛刺就会非常难看,测出来的尾延迟数据完全不可信。
所以我在测试机上都会做CPU隔离,在内核启动参数里加上:
isolcpus=2,3 nohz_full=2,3 rcu_nohz_full=2,3意思是从Linux调度器中隔离出核2和核3,专门留给perf跑。同时加上前面说的intel_iommu=on iommu=pt,完整的内核cmdline类似这样:
GRUB_CMDLINE_LINUX="... intel_iommu=on iommu=pt isolcpus=2,3 nohz_full=2,3 rcu_nohz_full=2,3"改完记得更新grub并重启。
除了核隔离,还有两个影响巨大的设置:CPU频率和PCIe电源管理。轮询线程如果跑在一个忽高忽低的频率上,性能数据会大幅抖动。建议强制性能模式:
cpupower frequency-set -g performancePCIe的ASPM(主动电源管理)能关就关,最简单的方式是内核参数加pcie_aspm=off。这个我在第5章会结合实际的controller reset问题详细讲,它不只是省电那么简单,还会直接影响NVMe链路的稳定性。
还要多说一句NUMA。不同核访问同一个PCIe设备的距离是不一样的,跑perf时最好选择NVMe设备所在NUMA节点上的核。用numactl -H查看节点拓扑,用lspci -vvv确认设备挂在哪个numa node上(一般会显示NUMA node: 0或类似信息)。跨NUMA测试不是不能跑,但性能会打折扣,而且数据不具备和同NUMA测试直接对比的资格。
2.4 跑通最小验证用例
环境准备好后,先跑一个最简命令验证整条链路是否通畅:
sudo app/spdk_perf/spdk_perf -q 16 -s 4096 -w randread -t 10 -c 0x1 -r trtype:PCIe解释一下各参数:-q 16表示队列深度16,-s 4096表示4KB IO大小,-w randread表示4K随机读,-t 10表示跑10秒,-c 0x1表示用核0,-r trtype:PCIe表示测本地PCIe NVMe设备。
跑通后,输出里会看到IOPS、带宽和延迟信息。如果这里报错,优先排查这几个问题:
| 报错现象 | 常见原因 | 处理方式 |
|---|---|---|
| No available hugepage | 大页没预留或权限不足 | 检查nr_hugepages和/mnt/huge挂载 |
| EAL: Error - exiting with code 1 | DPDK大页初始化失败 | 确认大页预留成功,用root运行 |
| Could not access device | 设备没绑定vfio-pci | 重新执行setup.sh,lspci -k检查 |
| No such device | Transport ID写错 | 核对trid格式和设备BDF号 |
第一次跑通之前不要急着调参,先把环境和设备链路确认好。perf这工具本身不复杂,复杂的是让它在正确环境下运行。
3. perf命令逐项拆解:从IOPS曲线到延迟分布,构建测试矩阵
3.1 核心参数吃透,先别急着手抖
SPDK perf的常用参数其实不多,但每个都很关键。不同版本参数略有差异,以你这台机器上spdk_perf -h的输出为准,下面的表是核心参数的通用语义:
| 参数 | 含义 | 示例 |
|---|---|---|
| -q / --io-depth | 每个核的队列深度 | -q 32 |
| -s / --io-size | IO大小,单位字节 | -s 4096 |
| -w / --rw | IO模式:read/write/randread/randwrite/rw/randrw | -w randread |
| -M / --rwmixread | 混合模式中读的百分比 | -M 70 |
| -t / --time | 测试时长,单位秒 | -t 60 |
| -c / --core-mask | CPU核掩码 | -c 0x3 |
| -r / --trid | Transport ID,指定设备 | -r trtype:PCIe |
这里最容易被误解的是-q。SPDK perf里的队列深度是“每个核的队列深度”。如果你用-c 0x3跑两个核,每个核-q 32,那么实际打到设备上的总队列深度是64,而不是32。很多人在设计测试矩阵时没算清这笔账,导致测出来的数据和预期对不上。
-r参数在不同场景下区别很大。测本地NVMe时用-r trtype:PCIe就行。测NVMe-oF时,要指定远端的传输类型和地址,比如RDMA的写法类似:
-r 'trtype:RDMA adrfam:IPv4 traddr:192.168.1.10 trsvcid:4420'如果你的SPDK编译时开了TCP传输,也可以用trtype:TCP跑NVMe/TCP。perf之所以在NVMe-oF测试圈里这么流行,就是因为它这套trid机制对本地和远端设备一视同仁,命令风格完全一致。
3.2 以“队列深度扫描”为核心设计测试矩阵
NVMe协议本身就是一个高并发协议,它允许每个队列有很深的队列深度。现代SSD内部普遍有多通道、多Die并行结构,只有把队列深度拉高,设备才有机会把内部并行度调度起来。这也是为什么perf测试一定要做“队列深度扫描”,而不是只测一个QD下的数字。
我常用的测试矩阵长这样:
| 测试目的 | 固定参数 | 变化参数 |
|---|---|---|
| 4K随机读性能曲线 | -s 4096 -w randread | -q 从1扫到128 |
| 4K随机写性能曲线 | -s 4096 -w randwrite | -q 从1扫到128 |
| 大块顺序读带宽上限 | -q 32 -w read | -s 从64K到1M |
| 大块顺序写带宽上限 | -q 32 -w write | -s 从64K到1M |
| 混合读写比例影响 | -s 4096 -q 32 -w randrw | -M 从70扫到30 |
每个组合的测试时长,我建议至少30秒,稳定场景用60秒。太短的数据抖动很大,尤其是写操作,盘内垃圾回收(GC)的影响会让10秒内的采样数据完全不可复现。
批量跑的时候,写个简单的bash循环就行:
for qd in 1 2 4 8 16 32 64 128; do sudo app/spdk_perf/spdk_perf -q $qd -s 4096 -w randread \ -t 60 -c 0x1 -r trtype:PCIe > result_randread_qd${qd}.txt sleep 5 done轮与轮之间隔几秒,让上一轮的IO彻底排空。如果你连续跑高强度写入,盘温会上升,NAND温度过高时固件会主动限速,数据就会飘。让盘凉一凉再跑下一组,数值会稳定很多。
3.3 混合读写与多核场景怎么测
混合读写是模拟真实业务负载最有用的模式。命令上很简单,比如70%读、30%写:
sudo app/spdk_perf/spdk_perf -q 32 -s 4096 -w randrw -M 70 -t 60 -c 0x1 -r trtype:PCIe但混合读写的测试结果波动通常比纯读纯写大得多。原因在于写路径会触发垃圾回收,而垃圾回收的时机不是均匀分布的。一块刚做完Secure Erase的盘和一块写满数据的脏盘,混合读写性能可能差30%以上。
所以混写测试我建议每个用例至少跑3次,取中位数。如果只看一次的结果,你很有可能被一个忽高忽低的数字误导。
多核场景的测试逻辑和单核不同。你要么固定每核队列深度,用更多核来冲刺极限IOPS;要么固定总队列深度,把线程数从1加到8,观察延迟和IOPS的变化。前者适合看峰值,后者适合看扩展性。
比如想用8个核、每核32队列深度,总共256队列深度去冲随机读极限:
sudo app/spdk_perf/spdk_perf -q 32 -s 4096 -w randread -t 60 -c 0xff -r trtype:PCIe如果你发现从4核加到8核,IOPS几乎没涨,这时候别急着怪SPDK,先检查设备的固件有没有多队列处理瓶颈,再检查是不是跨了NUMA节点访问。
4. 不要只盯着IOPS:数据怎么解读,测试结果怎么才算可信
4.1 延迟-队列深度曲线才是设备品质的照妖镜
刚到手的perf结果,很多人第一眼只看最大IOPS。这个习惯得改。IOPS这个数字太容易被“制造”出来了——调高队列深度,IOPS自然涨,但代价是延迟也在涨。
存储性能分析有一个基本公式:IOPS = 队列深度 / 平均延迟。举个例子,假设平均延迟是200微秒(0.0002秒),队列深度32,那么理论IOPS就是32 / 0.0002 = 160K。当设备内部没有瓶颈时,IOPS会随队列深度线性增长;一旦设备内部资源饱和,延迟会加速上升,IOPS增速放缓甚至停滞。
这个“拐点”就是设备真实能力的边界。所以做队列深度扫描时,不仅要记录每个QD下的IOPS,还要同时记录平均延迟和尾延迟。一块好的企业级SSD在饱和点附近延迟依然稳定;一块消费级SSD可能在队列深度16时平均延迟还行,到32时P99延迟突然暴涨。
SPDK perf不同版本输出的延迟字段不太一样。老版本只给平均延迟,新版本会带P99甚至更细的百分位。如果只有平均延迟,也不要紧,你只要对比每个QD下的平均延迟变化趋势,同样能判断出设备的饱和点。测尾延迟更精确的办法还是用fio的clat_percentiles,这个是另一套玩法了。
4.2 多核扩展性与核数-队列深度的配合
多核测试能回答一个关键问题:这块盘的性能上限是设备固件决定的,还是软件栈决定的?SPDK perf的价值恰恰在于它的软件栈开销极小,如果你用perf测多核扩展性都不好,那问题基本出在设备侧。
操作方式很简单:固定每核队列深度(比如-q 32),把核掩码从0x1(1核)、0x3(2核)、0xf(4核)、0xff(8核)依次变化,记录每个配置下的总IOPS。
不同设备的表现差异很大。有的企业级NVMe SSD在1核时就能跑到700K IOPS,4核时冲到2M以上,扩展性接近线性;有的消费级盘1核跑到300K就到顶了,加核数几乎不涨。这种差异反映的是设备控制器内部多个队列并行处理能力的差距。
还有一个容易踩的坑:如果你在做多核测试时没有固定总队列深度,而是一味加核,总队列深度也在成倍增加,那你实际测的是“队列深度上升”和“核数上升”的混合效应,数据解释起来会很棘手。所以我一般会把两种场景分开测:一种是“总QD固定,通过增加核数分散负担”,另一种是“每核QD固定,通过增加核数提升总QD”,然后分别分析。
4.3 和fio做对照,量化软件栈损耗
用SPDK perf测出设备极限后,下一步我会在同一台机器上跑一遍fio,两组数字一对比,就能算出内核IO栈到底“吃掉”了多少性能。这个数据对做存储方案设计非常有用。
fio的对照实验要注意对齐条件,否则对比没有意义。核心参数要尽量与perf保持一致:
fio --name=randread \ --ioengine=libaio \ --iodepth=32 \ --numjobs=1 \ --rw=randread \ --bs=4k \ --size=20G \ --direct=1 \ --runtime=60 \ --time_based注意几点:--iodepth对应perf里单个核的-q,--numjobs对应perf里的核数,--direct=1绕过page cache,--size要大于设备DRAM缓存,否则读命中了盘内缓存,数据虚高。
举个例子,我测过一块支持1.6M随机读IOPS的企业级盘,SPDK perf单核跑出约380K IOPS,fio用同样的单核配置只能跑到310K左右,差幅接近20%。这20%就是中断处理、内核驱动、块层调度的额外开销。换个配置差幅可能更大,内核版本、IO调度器、CPU中断绑核都会影响这个差值。
做这个对比实验的意义在于:当你向业务方承诺性能指标时,不能只报SPDK perf的数字,而要报fio的数字,因为生产环境跑的是真实IO栈。
5. 硬件平台和热词里那些坑:PCIe插槽带宽、老主板BIOS与stornvme复位
5.1 NVMe盘插到x1槽上的问题
最近老看到有人问“NVMe固态插PCIe x1”能用吗,能不能跑满速。答案很直接:能识别,但性能会被严重限制。
问题出在PCIe通道数量上。一条PCIe 3.0 x1链路,理论单向带宽约985MB/s,而主流NVMe SSD走PCIe 3.0 x4可以到3.5GB/s以上。如果你把盘插在x1槽上,顺序读就被卡死在1GB/s左右,连好一点SATA SSD的优势都体现不出来。
不同PCIe版本的x1和x4带宽大致如下:
| PCIe版本 | x1 单向带宽 | x4 单向带宽 |
|---|---|---|
| PCIe 2.0 | 约500MB/s | 约2GB/s |
| PCIe 3.0 | 约985MB/s | 约3.94GB/s |
| PCIe 4.0 | 约1.97GB/s | 约7.88GB/s |
要判断盘实际跑在什么链路上,不要只看插槽形状,用命令看最准:
lspci -vvv -s 01:00.0 | grep -E "LnkCap|LnkSta"如果输出显示LnkSta: Speed 8GT/s, Width x1,说明链路协商在x1上。换一根x4转接卡,或者换到主板上走CPU直连的x16插槽,问题就解决了。
顺带提醒一句:主板上的PCIe插槽不是所有都直连CPU。消费级主板通常只有前两根x16形状的插槽走CPU直连,其余走芯片组(PCH)。PCH本身通过DMI总线连CPU,带宽有限,所有走PCH的设备共享这路带宽。测NVMe极限性能时,尽量把盘插在直连CPU的插槽上,否则数据会受其他设备干扰。
5.2 老主板的BIOS与SPDK测试之间的关系
“华硕B85M-V Plus NVMe BIOS”这个搜索词我太熟悉了。老主板没有NVMe引导模块,想让NVMe盘做系统盘启动,得刷修改版BIOS或借助第三方引导。但这只是“从NVMe启动系统”的需求,和SPDK测试完全是两码事。
很多玩老平台的人被我这句话解救过:SPDK测试不依赖BIOS认识NVMe协议。SPDK是在操作系统运行起来之后,通过PCIe配置空间和BAR映射直接操作设备的。只要BIOS能初始化PCIe总线,能从其他设备(比如SATA盘)启动Linux,NVMe盘插在PCIe插槽上能被lspci认出来,就能用SPDK perf测试。
所以如果你手头正好有一台B85老机器,完全可以用它当NVMe测试机,省去刷BIOS的风险和折腾。真正需要关注的只有一个点:BIOS里有没有VT-d/IOMMU选项。没有的话,就用前面提到的uio_pci_generic绑定方式,绕开对IOMMU的依赖。
我实际在Haswell平台上跑过SPDK,性能数据和新平台并没有本质差异。老平台的PCIe 3.0链路对NVMe测试来说够用了,只要转接卡别太劣质,数据依然可信。这套玩法成本很低,对预算有限又想入门SPDK的人来说是个不错的路径。
5.3 stornvme.sys controller reset对测试平台的启示
“stornvme.sys + controller reset”是Windows下很典型的一类NVMe问题。stornvme.sys是微软自带的NVMe驱动,当它检测到控制器长时间无响应或IO超时,就会触发控制器复位(Controller Reset)。表现为系统卡顿、事件查看器里出现警告,严重的甚至蓝屏。
从硬件测试的角度看,这个现象背后往往不是“驱动不行”,而是平台链路稳定性出了问题。常见诱因有三个:
- ASPM电源管理导致链路进入低功耗状态后唤醒失败
- 固件在高负载或特定IO模式下出现异常,超时触发复位
- 转接卡接触不良、PCIe插槽信号质量差、供电不足
这对我们做SPDK测试有什么启示?太多了。我自己的习惯是,任何一台测试机跑SPDK perf之前,先做一轮平台加固:
- BIOS里关闭ASPM、C-State、CPU节能(EIST)
- 内核参数加
pcie_aspm=off - 禁用NVMe的APST(自主电源状态转换),内核参数加
nvme_core.default_ps_max_latency_us=0
APST是NVMe协议里的低功耗特性,盘在空闲时会自动进入省电状态,但从省电状态恢复的延迟不稳定,会造成延迟毛刺甚至IO超时。做性能测试时必须禁掉它。用nvme-cli也可以临时关闭:
nvme set-feature /dev/nvme0 -f 0x0c -v 0这些设置在长时间压测中尤其重要。跑一个48小时的稳定性测试,如果中途因为链路唤醒失败掉一次盘,整轮数据就废了。我在实际项目中遇到过一次类似问题,排查到最后就是一条PCIe延长线信号质量差导致的偶发性链路错误,换线后一切正常。
stornvme.sys controller reset这个现象也提醒我们:NVMe设备对平台电气特性的敏感度远超普通SATA设备。做评测时如果系统里同时有Windows和Linux双系统,我建议两边都检查一下ASPM和电源管理设置,避免“Windows下正常、Linux下性能异常”这种让人抓狂的对比问题。
最后再分享一个长期测试的好习惯
perf本身不复杂,复杂的是让每次测试的数据可以被解释、被复现。我现在的习惯是,每个结果文件的命名里都带上完整的环境标签:盘型号、固件版本、PCIe链路宽度和速率、CPU核掩码、队列深度、IO大小、读写模式、测试时长。比如:
SN630-3B2QGXQ7_pcie3_x4_qd32_bs4k_randread_60s.txt这样的命名虽然啰嗦,但三个月后翻出来,你依然能一眼看出这个数字的测试条件。做SSD评测和存储调优的人都明白,一个没有环境信息的性能数字,几乎等于没有测。希望这篇文章能帮你少走一些弯路,把SPDK perf用得更顺手。