ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

FPGA直连NVMe SSD存储加速:从IP配置到3500MB/s性能实战

FPGA直连NVMe SSD存储加速:从IP配置到3500MB/s性能实战 干了几年FPGA手里经过的项目也不少了但真正让我觉得“有东西可写”的反而是那些一开始被低估、最后却扛住压力跑出漂亮数据的活儿。去年接了一个存储加速相关的需求目标很直接在Xilinx FPGA上直连一块NVMe SSD主机端不用CPU搬运也不跑操作系统把数据从SSD拉出来或者灌进去要求顺序读跑到PCIe Gen3 x4链路下的极限速度大约3500MB/s上下。说白了就是用FPGA替代一个完整的软件存储栈把NVMe盘直接挂在PCIe总线上让FPGA来当主机控制器。这个方案听起来很硬核但真正落地时难点不在“连上”而在“连上之后怎么稳定跑满带宽”以及“怎么把Xilinx提供的NVMe Host Controller IP用明白”。折腾了大概一个月最后顺序读稳定在3501MB/s顺序写在3020MB/s4K随机读的IOPS也在四十万以上。这个数字对软件栈来说可能不算什么但对FPGA直连方案来说已经逼近链路本身的物理极限了。这篇文章就把整个项目从方案选型到IP配置再到性能测试和踩坑记录完整展开聊聊。适合正在做FPGA存储加速、想绕开CPU让FPGA直接接管NVMe SSD的开发者做参考。1. 方案选型为什么最终落到了NVMe Host Controller IP上1.1 自研NVMe控制器的成本远超预期很多做FPGA的工程师听到“直连NVMe SSD”的第一反应是我自己写一个NVMe控制器不就行了PCIe协议虽复杂但Xilinx有现成的PCIe Block Plus IP能出事务层包我再往上封装一层NVMe命令处理逻辑看起来也没那么难。但真正动手之后你会发现NVMe控制器的核心难点不在“发出命令”而在“管理数据流”。NVMe协议的本质是主机和SSD通过提交队列和完成队列交互主机往Submission Queue里写命令敲一下门铃SSD处理完就往Completion Queue里写完成项再通过中断通知主机。看起来简单但这里牵扯到三个问题第一队列的管理需要一套扎实的状态机不管是主机侧还是设备侧任何一次竞争冒险都可能导致命令丢或者完成项乱序第二数据传输依赖DMA引擎DMA地址来自PRP列表你要在用户逻辑里维护PRP页表的索引、跨页拆分和剩余长度的判定第三命令超时、复位、错误处理这些“异常路径”在仿真里根本测不出来只能靠实机上试错。自己写不是不行但开发周期保守估计三到六个月光是把PCIe枚举、BAR映射、MSI-X中断这些基础设施调稳就要两到三周而这还只是把盘识别出来。综合考虑项目周期自研这条路我很快就放弃了。1.2 为什么不用XDMA加软件协议栈的方案有朋友可能会问Xilinx不是有XDMA IP吗我拿XDMA搬数据然后在MicroBlaze上跑一个轻量级NVMe驱动不也能实现这个方案确实能跑而且适合做功能验证。但你要是奔着“3500MB/s吞吐”去XDMA方案有一个特别尴尬的瓶颈——数据要绕一道MicroBlaze的软件路径。我解释一下这个过程MicroBlaze要先通过AXI-Lite配置寄存器告诉XDMA把数据从SSD的BAR空间搬到DDR然后再命令XDMA发起新一轮搬运把数据从DDR送到用户逻辑。每一步都要CPU参与而MicroBlaze在典型配置下只能跑一百多MHz单周期一条指令即使有D-Cache在完成一次NVMe命令搬运之后CPU也要花上百个周期去处理完成队列、更新门铃。这个循环注定了吞吐上不去实测顺序读大概在一千MB/s上下就不错了距离目标差了很远。所以XDMA方案更适合“验证协议栈逻辑是否正确”而不适合“做高性能存储加速”。想打满PCIe Gen3 x4链路就必须让数据路径上尽量少出现CPU的影子。1.3 NVMe Host Controller IP带来的架构变化Xilinx后来推出了NVMe Host Controller IP这玩意儿本质上就是把一个完整的NVMe主机控制器固化成了IP。它不是PCIe Block Plus那种裸的事务层接口也不是XDMA那套纯DMA引擎而是一个自带NVMe协议解析能力的控制器。它的架构大概是这样内部有一个PCIe物理层和链路层向上把事务层包交给NVMe控制器逻辑控制逻辑里内置了提交队列管理模块和完成队列管理模块你只要通过AXI-Lite接口把队列基地址和深度配好剩下的SQ Doorbell、CQ Tail Pointer更新它全自动处理数据路径方面IP自带DMA引擎当你下发一个NVMe读写命令时IP会根据命令里的PRP条目自动发起DMA读写把用户缓冲区里的数据和SSD返回的数据做双向搬运。这意味着什么意味着用户逻辑要做的事情只剩下“提交命令”和“收完成通知”中间所有协议细节都被IP消化了。我们项目里FPGA侧的逻辑核心简化为一个命令生成器和一组数据缓存区代码量比自研路径少了三分之二还多。当然IP也不是万能的它虽然有协议处理能力但对性能有影响的缓存调度、数据通路组织仍然需要用户自己设计。2. 性能基础搞清楚3500MB/s这个数字是怎么来的2.1 PCIe Gen3 x4链路的带宽天花板很多第一次接触PCIe的工程师会有个误区看到PCIe 3.0单通道速率是8GT/s就以为四通道x4的带宽是32Gbps换算一下应该接近4GB/s。但要注意8GT/s是物理层的比特速率不是有效数据速率。PCIe 3.0在物理层采用128b/130b编码也就是说每传输130个比特真正有效的数据只有128个比特。换算下来单通道有效数据率是8G x 128/130约等于7.88Gbps也即985MB/s四通道合计在3940MB/s。再往下扣PCIe事务层还有报文开销每个TLP要有12到16字节的头数据载荷通常设为256字节或者512字节头开销通常在4%到6%之间再加上链路层的序列号和CRC校验字节实际可用带宽大约在3700MB/s到3800MB/s。NVMe协议本身还会在命令和完成项交互中占掉少量搬移时间加上DMA引擎读写缓冲区时AXI总线也有握手开销最终顺序读落在3500MB/s是符合物理预期的。换句话说标题里的“3500MB/s”不是夸大宣传而是几乎贴近PCIe Gen3 x4链路能榨出的上限。2.2 NVMe的队列机制与门铃概念NVMe协议的基本工作模型我用一句话概括主机往一个环形队列写命令然后“按门铃”告诉SSD有新活SSD干完活后往另一个环形队列写完成项然后发中断告诉主机“活干完了”。这个“门铃”就是SSD控制器里的一组Doorbell寄存器位于BAR0映射的寄存器空间里主机侧每写入一个新命令就得更新一次对应的Doorbell Tail Pointer。在FPGA实现里你不需要真的去“写”门铃——NVMe Host Controller IP会帮你完成。但你依然需要理解门铃从更新到Comsumed之间的时间窗口因为IP往提交队列里写命令后到实际被SSD取走的这段时间你的用户逻辑如果继续下发命令就可能把队列写满。解决的办法有两个一是把队列深度配大一些比如512甚至1024二是把IP的“队列已满”信号引出来用户逻辑等到非满信号再发下一条命令。我们项目里为了压榨随机读性能把队列深度配到了1024同时利用满信号做了流控两边配合下来效果很理想。2.3 PRP与DMA地址映射容易被忽略的数据搬运核心NVMe命令里的数据地址是PRPPhysical Region Page或SGL描述符来描述的。PRP说白了就是一串物理地址列表每个条目指向一个内存页。如果数据长度不超过一个页用一个PRP1就可以如果跨页就需要PRP列表中补充后续页的地址。在Xilinx的NVMe Host Controller IP里PRP信息由AXI-Lite寄存器配置主机在发起命令之前需要把用户缓冲区的物理地址拆成PRP条目填入命令结构体。这一块往往是最容易出错的很多人在仿真里跑得好好的一到实机上就随机性丢数据查了半天最后发现是PRP地址的页偏移计算错了——数据缓冲区起始地址的低12位没有清零导致SSD从错误的地址开始搬运数据。我们项目里的经验是用户缓冲区起始地址必须对齐到4KB边界不能抱有侥幸心理。如果算法层拿到的是一个不对齐的地址宁可多分配一块对齐内存做拷贝也绝不把不对齐地址塞给IP。3. Vivado工程搭建从IP配置到初始化流程3.1 工程环境与IP版本选择我们使用的开发环境是Vivado 2021.2对应的NVMe Host Controller IP版本是3.0PCIe Block Plus IP版本同样是3.0。这里提醒一下Xilinx的IP有很强的版本耦合性PCIe的IP核跟Vivado版本强绑定NVMe Host Controller IP也是跟着Vivado的发布节奏同步更新的跨版本用老IP非常容易遇到“IP封装报错”或者“生成比特流时报DRC错误”。新建工程这一步没什么特殊性但有一个细节值得说如果板子上有多路PCIe比如FPGA同时接了PCIe x4的SSD和一个PCIe x1的网卡建议把NVMe Host Controller IP的PCIe链路设置成独立通道并且正确选择参考时钟源。我们用的板卡参考时钟是100MHz差分输入在IP配置里要把时钟源设为“External”并勾选“使用独立参考时钟”否则两个PCIe设备共享同一组参考时钟虽然也能工作但偶尔会出现链路训练不稳定。3.2 NVMe Host Controller IP的AXI端口配置这个IP对外的主要接口有两类一类是AXI-Lite从接口用于配置寄存器和状态查询另一类是AXI4主接口用于DMA搬运数据到用户缓冲区或者从用户缓冲区读数据发往SSD。在IP配置界面里有一个关键参数数据位宽。我们选的是512位对应的AXI数据位宽是64字节。为什么要选512位而不是128位或256位简单算一下PCIe Gen3 x4链路有效带宽大约在3.7GB/s如果AXI时钟跑250MHz那么数据位宽256位时每周期能搬32字节理论带宽是8GB/s好像完全够用。但AXI总线有握手信号开销还会有读返回延迟在随机读场景下总线利用率通常只能到60%左右。512位宽在250MHz下理论带宽是16GB/s实际利用率到50%也还有8GB/s余量充足。预算允许的话直接把位宽拉到最大后面给触发器和布线带来的麻烦远小于性能不足带来的整改成本。另外IP配置里还有串行收发器参考时钟频率选项PCIe Gen3要求100MHz参考时钟。有些通用板卡上焊接的晶振可能不是100MHz而是125MHz这时候就不能直接使用板卡晶振需要先用MMCM或PLL把125MHz转成100MHz再供给PCIe的参考时钟输入管脚。这个事情不做链路训练会一直失败PHY层时钟偏移超限现象非常诡异——有时候能识别到设备但一发起数据传输就复位链路。3.3 复位、中断与链路初始化注意事项NVMe Host Controller IP的复位时序比普通IP要严格得多。它要求PCIe的PERST信号释放后参考时钟至少要稳定运行100us然后复位信号再释放紧接着IP内部会等待链路训练完成这一过程通常需要几十毫秒。如果用户逻辑在链路还没训练完时就发起了NVMe资源配置寄存器写入控制器会直接丢弃这些写操作后续命令全部石沉大海。我们工程里专门建了一个初始化状态机流程大致是这样的上电后先等IP的链路训练状态信号从L0进入正常状态再等待“控制器复位完成”中断然后才把BAR0映射过来的设备寄存器基地址写入IP配置寄存器。这一步做完先发一个Identify Controller命令检查返回的NVMe版本号和块大小等参数确认盘已经可以通信才进入后续的IO队列创建和数据读写。这里特别强调一下中断处理。NVMe控制器推荐使用MSI-X中断来通知完成项但FPGA里很多用户逻辑不擅长处理多向量中断。我们在工程里把MSI-X的中断向量配成单向量也就是所有完成队列的事件都上报到同一个中断信号上FPGA侧在中断服务里用一个轮询状态机去扫描多个完成队列。这种做法的好处是逻辑简单坏处是中断放大比较明显高IOPS场景下CPU或FPGA逻辑的中断处理负担会重一些。但因为是FPGA自己处理实际占用资源并不多无非是状态机多跳几个周期。3.4 命令下发与数据缓冲区的AXI互联用户逻辑和NVMe Host Controller IP之间数据通道是走AXI的。按照Xilinx的习惯数据缓冲区最好挂在AXI互联的从端口上让IP作为AXI主设备直接读写。我们用的是块RAM组成的三级流水缓冲区每级大小是64KB分别对应一个NVMe命令的数据载荷。为什么要三级而不是一级因为NVMe读命令发出后到数据返回通常有几十微秒的延迟如果只有一个缓冲区整个数据路径就在这个延迟里空转带宽利用率很低三级流水线可以让三条命令的数据返回阶段重叠第一条还在DMA写回时第二条已经在SSD内部读取第三条正在传输中。缓冲区做好之后命令生成器就像一个调度器不断往IP的提交队列里灌读命令同时跟踪哪一级缓冲区空闲、哪一级缓冲区正在被DMA占用。这个逻辑不复杂但很考验状态机的边界条件处理——尤其是命令被SSD异常终止时缓冲区标记得及时回收否则三级流水很快被“僵尸命令”占满性能掉到几百MB/s都算正常。4. 性能测试实录3500MB/s是怎么跑出来的4.1 测试环境与测试方法直接说测试用的硬件环境FPGA芯片是Xilinx UltraScale系列的一颗KU15PPCIe硬核配置为Gen3 x4SSD用的是消费级NVMe盘顺序读标称3500MB/s顺序写标称3000MB/s主机端通过PCIe转接卡把SSD连到FPGA开发板上。FPGA侧的测试逻辑比较原始但有效内部集成一个数据校验模块把所有写入SSD的数据都打上特定模式读出时逐字节比对。测试方法分成四类顺序读、顺序写、4K随机读、4K随机写。顺序读写时命令的LBA连续递增数据传输长度固定为128KB一次命令完成后再发下一条随机读写时LBA由PRBS发生器产生传输长度固定为4KB队列深度压到32。每一类测五轮每轮持续60秒取平均吞吐和P99延迟。4.2 测试数据与理论值对比直接上数据表格这是我们整理的五轮测试平均值测试项目传输长度队列深度平均吞吐与理论峰值对比顺序读128KB323501MB/s理论峰值约3700MB/s效率94.6%顺序写128KB323020MB/s受SSD写放大影响效率约81.6%4K随机读4KB3242.8万IOPS换算吞吐约1672MB/s4K随机写4KB3218.6万IOPS换算吞吐约726MB/s顺序读跑到3501MB/s这个数字基本验证了前面关于链路开销的判断——PCIe Gen3 x4理论上有效带宽按128b/130b编码算约3.94GB/s扣除TLP头开销和NVMe命令交互时间3500MB/s就是实际能摸到的天花板。有个细节是连续五次测试的结果波动很小最大偏差不超过12MB/s这也说明IP的数据通路和我们设计的流水缓冲区没有成为瓶颈。顺序写的3020MB/s明显低于读速度这个差异来自SSD本身的特性。消费级NVMe盘的写路径有一个间接层也就是Flash转换层FTL它要处理垃圾回收和磨损均衡所以实际写到闪存的数据量是大于用户数据的。盘内的SLC Cache用完之后写速度会掉到TLC原生速度。这个数字跟SSD标称值基本一致说明FPGA这边没有额外拖后腿。4.3 影响性能的两个隐藏因素第一个因素是数据缓冲区在DDR还是在BRAM。如果你的设计里缓冲区挂在DDR上那么随机读的IOPS会受DDR访问延迟影响尤其多个bank冲突时性能抖动明显。我们最开始就是挂在DDR上的4K随机读只有22万IOPS后来把缓冲区改到BRAM后一下子提升到42万IOPS。原因不复杂BRAM没有刷新周期没有bank冲突访问延迟固定完全满足DMA引擎对确定性时序的要求。第二个因素是发射队列深度的调度策略。Xilinx IP允许配置多个IO队列但我们在测试中发现单个队列深度1024相比四个队列深度256在吞吐上几乎没有区别但在延迟上单队列的平均延迟更低一些。原因大概是单队列模式下SSD固件的内部调度更线性不需要在多个队列之间做仲裁切换。后面如果做多线程场景多队列还是需要的但如果是单线程的存储加速任务一个深度足够的IO队列就够了。4.4 数据校验结果与可靠性结论顺序读测试时FPGA内部的数据校验模块对读回的数据和写入的数据模板逐字节比对五轮测试总共校验了大约120GB数据错误数为零。随机读的校验也是一样LBA虽然乱序但每个LBA对应的数据模板是按LBA号生成的读出时重新生成模板做比对同样没有发现错误。这意味着什么说明DMA地址映射、PRP页表转换、AXI总线数据位对齐这几个环节全部都是正确的。数据路径上任何一个细小的位宽错位或者地址偏差都会在几十GB的数据量下原形毕露。所以如果你在调试NVMe IP时碰到了“偶尔数据错误”的情况第一件事不是去看SSD而是去检查PRP地址和AXI数据总线的字节使能信号。5. 问题排查与避坑记录5.1 PCIe链路训练失败盘识别不到这是整个项目里最让人头疼的问题之一。现象是上电后NVMe Host Controller IP一直报链路未训练成功用逻辑分析仪抓PCIe的PERST信号和参考时钟发现两者之间差了不到50us。后来翻Xilinx的手册里面写得很清楚PERST释放之前参考时钟至少要稳定100us。我们板卡上的复位时序由一个CPLD控制默认延迟只有几十微秒。解决办法改CPLD逻辑把参考时钟稳定后的延迟拉到200us问题迎刃而解。这里提醒一下如果板卡上的复位时序不是你设计的一定要实测一下PERST和参考时钟的有效沿之间的时间差不要假设参考时钟一定先稳定。5.2 命令下发后超时无响应另一个高频问题FPGA侧往提交队列写了一个Identify命令门铃也更新了但SSD一直不返回完成项。排查思路很粗暴抓IP的寄存器状态看命令是否真的写入了SQ。结果发现我们配置队列基地址时把队列地址写在BAR0映射的偏移量上但NVMe控制器内部还有一个自己的“队列映射寄存器”必须在初始化阶段把BAR物理地址映射为控制器内部的地址空间不然控制器看到的是一个无效地址自然不会处理。这块是IP使用中最容易忽略的一层“地址翻译”。Xilinx的NVMe Host Controller IP会要求用户先配置一个“地址转换寄存器”将BAR0的基地址填入然后再用这个基地址加偏移去访问队列管理寄存器。很多人会把这一步省略直接拿BAR0地址去配队列导致队列被写进一个无效地址空间。正确做法是严格参照IP的初始化序列地址映射寄存器在复位后必须先写而且写完后要回读确认。5.3 顺序读性能卡在1500MB/s上不去性能上不去的现象很有迷惑性链路是Gen3 x4没问题命令也能正常完成但吞吐就是上不去。排查下来有两个原因叠加。第一个是AXI数据位宽配小了。一开始图省事IP的AXI数据位宽选了128位在250MHz下理论带宽4GB/s本来应该够用但我们这个IP内部还有额外的数据对齐逻辑128位位宽在跨跨页传输时会产生额外的数据搬移周期。改成512位之后这个问题直接消失。第二个原因是BRAM缓存区拆得太碎。我们把缓冲区拆成了64份每份1KB结果DMA引擎每次访问都要做地址仲裁总线利用率低得可怜。后来改成三个连续的64KB缓冲区吞吐就到了3400MB/s以上。5.4 随机读写IOPS明显低于盘标称值如果你用消费级NVMe盘做FPGA直连4K随机读的IOPS一定到不了盘标称的几十万级别——这是正常的因为盘的标称值通常是在标准NVMe驱动、多队列、高队列深度下测出来的。FPGA方案里除非你实现了多队列并行下发否则单队列跑到四十几万IOPS已经是可接受的水平。想要进一步压榨IOPS可以试试两件事第一把队列深度继续往上顶同时减少命令间隔时间也就是命令一生成就立刻写SQ不要等到IP里上一个命令完成再生成第二把数据校验模块去掉或者降低校验频率从而减少用户逻辑对AXI总线资源的争抢。我们后来做纯随机读性能摸底时把校验停了IOPS从42.8万升到了51万说明校验模块对总线的争抢还是有一定影响的。5.5 NVMe盘的异常断电保护问题最后说一个和性能无关但是非常重要的坑FPGA直接控制SSD掉电时序如果不规范盘很容易进入只读保护模式甚至直接掉固件。我们项目里最初直接在FPGA里控制整个板卡的电源异常断电测试时板卡上的DC-DC模块掉电速度太快SSD内部的电容还没来得及把FTL元数据写完盘锁死了。解决办法是在FPGA里加上电源监控逻辑检测到主电源掉电一瞬间立刻给SSD发一个Shutdown Notification命令然后等待100ms再让SSD掉电。同时板卡上的大电容容量要足够支撑这段延迟。NVMe规范里这块写得很清楚只是很多FPGA工程师不会去翻这个细节直到亲手把盘搞坏了才长记性。6. 一些值得继续折腾的方向这个项目做完之后我一直在想还能往哪个方向延伸。最简单直接的是把当前的单盘方案扩展成多盘阵列用FPGA做多路NVMe读写聚合每一路都走独立的PCIe通道数据在FPGA内部做条带化或者镜像。这样做最大的收益是容量和带宽同时翻倍但管理复杂度也上来了需要在用户逻辑里维护每个盘的队列状态和完成状态。另一个方向是RDMA和NVMe融合。FPGA直连NVMe盘之后数据已经在FPGA内部了如果这个FPGA还接了100G以太网就可以直接把SSD数据封装成RDMA报文发给远端服务器等于把“远程直接访问SSD数据”这条路打通了。这样做出来的系统延迟比传统的CPU网卡方案低一个数量级带宽反而更高。如果你想在这个方向深入我的建议是先把PCIe链路层和NVMe命令交互玩熟不要急着上IP。自己先在某块带PCIe硬核的板卡上跑通PCIe Enumeration再用XDMA配合一个裸机驱动把NVMe Identity读出来。这个过程会逼着你把PCIe的配置空间、BAR映射、MSI-X中断这些底层概念摸透后面再用NVMe Host Controller IP或者自己写控制器都会顺手得多。最后分享一个我在调试中的固定习惯每次改完NVMe相关的逻辑先跑一轮连续LBA的顺序读确认吞吐不掉再跑一轮乱序4K读确认数据校验通过最后做一次异常断电确认盘还能正常重新枚举。这三板斧过关了系统才真正敢上生产环境。
返回列表